github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/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/hashicorp/terraform/internal/addrs" 9 "github.com/hashicorp/terraform/internal/configs/configschema" 10 "github.com/hashicorp/terraform/internal/lang/marks" 11 "github.com/hashicorp/terraform/internal/plans" 12 "github.com/hashicorp/terraform/internal/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 "creation (object with quoted keys)": { 77 Action: plans.Create, 78 Mode: addrs.ManagedResourceMode, 79 Before: cty.NullVal(cty.EmptyObject), 80 After: cty.ObjectVal(map[string]cty.Value{ 81 "object": cty.ObjectVal(map[string]cty.Value{ 82 "unquoted": cty.StringVal("value"), 83 "quoted:key": cty.StringVal("some-value"), 84 }), 85 }), 86 Schema: &configschema.Block{ 87 Attributes: map[string]*configschema.Attribute{ 88 "object": {Type: cty.Object(map[string]cty.Type{ 89 "unquoted": cty.String, 90 "quoted:key": cty.String, 91 }), Optional: true}, 92 }, 93 }, 94 RequiredReplace: cty.NewPathSet(), 95 ExpectedOutput: ` # test_instance.example will be created 96 + resource "test_instance" "example" { 97 + object = { 98 + "quoted:key" = "some-value" 99 + unquoted = "value" 100 } 101 } 102 `, 103 }, 104 "deletion": { 105 Action: plans.Delete, 106 Mode: addrs.ManagedResourceMode, 107 Before: cty.ObjectVal(map[string]cty.Value{ 108 "id": cty.StringVal("i-02ae66f368e8518a9"), 109 }), 110 After: cty.NullVal(cty.EmptyObject), 111 Schema: &configschema.Block{ 112 Attributes: map[string]*configschema.Attribute{ 113 "id": {Type: cty.String, Computed: true}, 114 }, 115 }, 116 RequiredReplace: cty.NewPathSet(), 117 ExpectedOutput: ` # test_instance.example will be destroyed 118 - resource "test_instance" "example" { 119 - id = "i-02ae66f368e8518a9" -> null 120 } 121 `, 122 }, 123 "deletion of deposed object": { 124 Action: plans.Delete, 125 Mode: addrs.ManagedResourceMode, 126 DeposedKey: states.DeposedKey("byebye"), 127 Before: cty.ObjectVal(map[string]cty.Value{ 128 "id": cty.StringVal("i-02ae66f368e8518a9"), 129 }), 130 After: cty.NullVal(cty.EmptyObject), 131 Schema: &configschema.Block{ 132 Attributes: map[string]*configschema.Attribute{ 133 "id": {Type: cty.String, Computed: true}, 134 }, 135 }, 136 RequiredReplace: cty.NewPathSet(), 137 ExpectedOutput: ` # test_instance.example (deposed object byebye) will be destroyed 138 # (left over from a partially-failed replacement of this instance) 139 - resource "test_instance" "example" { 140 - id = "i-02ae66f368e8518a9" -> null 141 } 142 `, 143 }, 144 "deletion (empty string)": { 145 Action: plans.Delete, 146 Mode: addrs.ManagedResourceMode, 147 Before: cty.ObjectVal(map[string]cty.Value{ 148 "id": cty.StringVal("i-02ae66f368e8518a9"), 149 "intentionally_long": cty.StringVal(""), 150 }), 151 After: cty.NullVal(cty.EmptyObject), 152 Schema: &configschema.Block{ 153 Attributes: map[string]*configschema.Attribute{ 154 "id": {Type: cty.String, Computed: true}, 155 "intentionally_long": {Type: cty.String, Optional: true}, 156 }, 157 }, 158 RequiredReplace: cty.NewPathSet(), 159 ExpectedOutput: ` # test_instance.example will be destroyed 160 - resource "test_instance" "example" { 161 - id = "i-02ae66f368e8518a9" -> null 162 } 163 `, 164 }, 165 "string in-place update": { 166 Action: plans.Update, 167 Mode: addrs.ManagedResourceMode, 168 Before: cty.ObjectVal(map[string]cty.Value{ 169 "id": cty.StringVal("i-02ae66f368e8518a9"), 170 "ami": cty.StringVal("ami-BEFORE"), 171 }), 172 After: cty.ObjectVal(map[string]cty.Value{ 173 "id": cty.StringVal("i-02ae66f368e8518a9"), 174 "ami": cty.StringVal("ami-AFTER"), 175 }), 176 Schema: &configschema.Block{ 177 Attributes: map[string]*configschema.Attribute{ 178 "id": {Type: cty.String, Optional: true, Computed: true}, 179 "ami": {Type: cty.String, Optional: true}, 180 }, 181 }, 182 RequiredReplace: cty.NewPathSet(), 183 ExpectedOutput: ` # test_instance.example will be updated in-place 184 ~ resource "test_instance" "example" { 185 ~ ami = "ami-BEFORE" -> "ami-AFTER" 186 id = "i-02ae66f368e8518a9" 187 } 188 `, 189 }, 190 "update with quoted key": { 191 Action: plans.Update, 192 Mode: addrs.ManagedResourceMode, 193 Before: cty.ObjectVal(map[string]cty.Value{ 194 "id": cty.StringVal("i-02ae66f368e8518a9"), 195 "saml:aud": cty.StringVal("https://example.com/saml"), 196 "zeta": cty.StringVal("alpha"), 197 }), 198 After: cty.ObjectVal(map[string]cty.Value{ 199 "id": cty.StringVal("i-02ae66f368e8518a9"), 200 "saml:aud": cty.StringVal("https://saml.example.com"), 201 "zeta": cty.StringVal("alpha"), 202 }), 203 Schema: &configschema.Block{ 204 Attributes: map[string]*configschema.Attribute{ 205 "id": {Type: cty.String, Optional: true, Computed: true}, 206 "saml:aud": {Type: cty.String, Optional: true}, 207 "zeta": {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 id = "i-02ae66f368e8518a9" 214 ~ "saml:aud" = "https://example.com/saml" -> "https://saml.example.com" 215 # (1 unchanged attribute hidden) 216 } 217 `, 218 }, 219 "string force-new update": { 220 Action: plans.DeleteThenCreate, 221 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 222 Mode: addrs.ManagedResourceMode, 223 Before: cty.ObjectVal(map[string]cty.Value{ 224 "id": cty.StringVal("i-02ae66f368e8518a9"), 225 "ami": cty.StringVal("ami-BEFORE"), 226 }), 227 After: cty.ObjectVal(map[string]cty.Value{ 228 "id": cty.StringVal("i-02ae66f368e8518a9"), 229 "ami": cty.StringVal("ami-AFTER"), 230 }), 231 Schema: &configschema.Block{ 232 Attributes: map[string]*configschema.Attribute{ 233 "id": {Type: cty.String, Optional: true, Computed: true}, 234 "ami": {Type: cty.String, Optional: true}, 235 }, 236 }, 237 RequiredReplace: cty.NewPathSet(cty.Path{ 238 cty.GetAttrStep{Name: "ami"}, 239 }), 240 ExpectedOutput: ` # test_instance.example must be replaced 241 -/+ resource "test_instance" "example" { 242 ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement 243 id = "i-02ae66f368e8518a9" 244 } 245 `, 246 }, 247 "string in-place update (null values)": { 248 Action: plans.Update, 249 Mode: addrs.ManagedResourceMode, 250 Before: cty.ObjectVal(map[string]cty.Value{ 251 "id": cty.StringVal("i-02ae66f368e8518a9"), 252 "ami": cty.StringVal("ami-BEFORE"), 253 "unchanged": cty.NullVal(cty.String), 254 }), 255 After: cty.ObjectVal(map[string]cty.Value{ 256 "id": cty.StringVal("i-02ae66f368e8518a9"), 257 "ami": cty.StringVal("ami-AFTER"), 258 "unchanged": cty.NullVal(cty.String), 259 }), 260 Schema: &configschema.Block{ 261 Attributes: map[string]*configschema.Attribute{ 262 "id": {Type: cty.String, Optional: true, Computed: true}, 263 "ami": {Type: cty.String, Optional: true}, 264 "unchanged": {Type: cty.String, Optional: true}, 265 }, 266 }, 267 RequiredReplace: cty.NewPathSet(), 268 ExpectedOutput: ` # test_instance.example will be updated in-place 269 ~ resource "test_instance" "example" { 270 ~ ami = "ami-BEFORE" -> "ami-AFTER" 271 id = "i-02ae66f368e8518a9" 272 } 273 `, 274 }, 275 "in-place update of multi-line string field": { 276 Action: plans.Update, 277 Mode: addrs.ManagedResourceMode, 278 Before: cty.ObjectVal(map[string]cty.Value{ 279 "id": cty.StringVal("i-02ae66f368e8518a9"), 280 "more_lines": cty.StringVal(`original 281 long 282 multi-line 283 string 284 field 285 `), 286 }), 287 After: cty.ObjectVal(map[string]cty.Value{ 288 "id": cty.UnknownVal(cty.String), 289 "more_lines": cty.StringVal(`original 290 extremely long 291 multi-line 292 string 293 field 294 `), 295 }), 296 Schema: &configschema.Block{ 297 Attributes: map[string]*configschema.Attribute{ 298 "id": {Type: cty.String, Optional: true, Computed: true}, 299 "more_lines": {Type: cty.String, Optional: true}, 300 }, 301 }, 302 RequiredReplace: cty.NewPathSet(), 303 ExpectedOutput: ` # test_instance.example will be updated in-place 304 ~ resource "test_instance" "example" { 305 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 306 ~ more_lines = <<-EOT 307 original 308 - long 309 + extremely long 310 multi-line 311 string 312 field 313 EOT 314 } 315 `, 316 }, 317 "addition of multi-line string field": { 318 Action: plans.Update, 319 Mode: addrs.ManagedResourceMode, 320 Before: cty.ObjectVal(map[string]cty.Value{ 321 "id": cty.StringVal("i-02ae66f368e8518a9"), 322 "more_lines": cty.NullVal(cty.String), 323 }), 324 After: cty.ObjectVal(map[string]cty.Value{ 325 "id": cty.UnknownVal(cty.String), 326 "more_lines": cty.StringVal(`original 327 new line 328 `), 329 }), 330 Schema: &configschema.Block{ 331 Attributes: map[string]*configschema.Attribute{ 332 "id": {Type: cty.String, Optional: true, Computed: true}, 333 "more_lines": {Type: cty.String, Optional: true}, 334 }, 335 }, 336 RequiredReplace: cty.NewPathSet(), 337 ExpectedOutput: ` # test_instance.example will be updated in-place 338 ~ resource "test_instance" "example" { 339 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 340 + more_lines = <<-EOT 341 original 342 new line 343 EOT 344 } 345 `, 346 }, 347 "force-new update of multi-line string field": { 348 Action: plans.DeleteThenCreate, 349 Mode: addrs.ManagedResourceMode, 350 Before: cty.ObjectVal(map[string]cty.Value{ 351 "id": cty.StringVal("i-02ae66f368e8518a9"), 352 "more_lines": cty.StringVal(`original 353 `), 354 }), 355 After: cty.ObjectVal(map[string]cty.Value{ 356 "id": cty.UnknownVal(cty.String), 357 "more_lines": cty.StringVal(`original 358 new line 359 `), 360 }), 361 Schema: &configschema.Block{ 362 Attributes: map[string]*configschema.Attribute{ 363 "id": {Type: cty.String, Optional: true, Computed: true}, 364 "more_lines": {Type: cty.String, Optional: true}, 365 }, 366 }, 367 RequiredReplace: cty.NewPathSet(cty.Path{ 368 cty.GetAttrStep{Name: "more_lines"}, 369 }), 370 ExpectedOutput: ` # test_instance.example must be replaced 371 -/+ resource "test_instance" "example" { 372 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 373 ~ more_lines = <<-EOT # forces replacement 374 original 375 + new line 376 EOT 377 } 378 `, 379 }, 380 381 // Sensitive 382 383 "creation with sensitive field": { 384 Action: plans.Create, 385 Mode: addrs.ManagedResourceMode, 386 Before: cty.NullVal(cty.EmptyObject), 387 After: cty.ObjectVal(map[string]cty.Value{ 388 "id": cty.UnknownVal(cty.String), 389 "password": cty.StringVal("top-secret"), 390 "conn_info": cty.ObjectVal(map[string]cty.Value{ 391 "user": cty.StringVal("not-secret"), 392 "password": cty.StringVal("top-secret"), 393 }), 394 }), 395 Schema: &configschema.Block{ 396 Attributes: map[string]*configschema.Attribute{ 397 "id": {Type: cty.String, Computed: true}, 398 "password": {Type: cty.String, Optional: true, Sensitive: true}, 399 "conn_info": { 400 NestedType: &configschema.Object{ 401 Nesting: configschema.NestingSingle, 402 Attributes: map[string]*configschema.Attribute{ 403 "user": {Type: cty.String, Optional: true}, 404 "password": {Type: cty.String, Optional: true, Sensitive: true}, 405 }, 406 }, 407 }, 408 }, 409 }, 410 RequiredReplace: cty.NewPathSet(), 411 ExpectedOutput: ` # test_instance.example will be created 412 + resource "test_instance" "example" { 413 + conn_info = { 414 + password = (sensitive value) 415 + user = "not-secret" 416 } 417 + id = (known after apply) 418 + password = (sensitive value) 419 } 420 `, 421 }, 422 "update with equal sensitive field": { 423 Action: plans.Update, 424 Mode: addrs.ManagedResourceMode, 425 Before: cty.ObjectVal(map[string]cty.Value{ 426 "id": cty.StringVal("blah"), 427 "str": cty.StringVal("before"), 428 "password": cty.StringVal("top-secret"), 429 }), 430 After: cty.ObjectVal(map[string]cty.Value{ 431 "id": cty.UnknownVal(cty.String), 432 "str": cty.StringVal("after"), 433 "password": cty.StringVal("top-secret"), 434 }), 435 Schema: &configschema.Block{ 436 Attributes: map[string]*configschema.Attribute{ 437 "id": {Type: cty.String, Computed: true}, 438 "str": {Type: cty.String, Optional: true}, 439 "password": {Type: cty.String, Optional: true, Sensitive: true}, 440 }, 441 }, 442 RequiredReplace: cty.NewPathSet(), 443 ExpectedOutput: ` # test_instance.example will be updated in-place 444 ~ resource "test_instance" "example" { 445 ~ id = "blah" -> (known after apply) 446 ~ str = "before" -> "after" 447 # (1 unchanged attribute hidden) 448 } 449 `, 450 }, 451 452 // tainted objects 453 "replace tainted resource": { 454 Action: plans.DeleteThenCreate, 455 ActionReason: plans.ResourceInstanceReplaceBecauseTainted, 456 Mode: addrs.ManagedResourceMode, 457 Before: cty.ObjectVal(map[string]cty.Value{ 458 "id": cty.StringVal("i-02ae66f368e8518a9"), 459 "ami": cty.StringVal("ami-BEFORE"), 460 }), 461 After: cty.ObjectVal(map[string]cty.Value{ 462 "id": cty.UnknownVal(cty.String), 463 "ami": cty.StringVal("ami-AFTER"), 464 }), 465 Schema: &configschema.Block{ 466 Attributes: map[string]*configschema.Attribute{ 467 "id": {Type: cty.String, Optional: true, Computed: true}, 468 "ami": {Type: cty.String, Optional: true}, 469 }, 470 }, 471 RequiredReplace: cty.NewPathSet(cty.Path{ 472 cty.GetAttrStep{Name: "ami"}, 473 }), 474 ExpectedOutput: ` # test_instance.example is tainted, so must be replaced 475 -/+ resource "test_instance" "example" { 476 ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement 477 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 478 } 479 `, 480 }, 481 "force replacement with empty before value": { 482 Action: plans.DeleteThenCreate, 483 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 484 Mode: addrs.ManagedResourceMode, 485 Before: cty.ObjectVal(map[string]cty.Value{ 486 "name": cty.StringVal("name"), 487 "forced": cty.NullVal(cty.String), 488 }), 489 After: cty.ObjectVal(map[string]cty.Value{ 490 "name": cty.StringVal("name"), 491 "forced": cty.StringVal("example"), 492 }), 493 Schema: &configschema.Block{ 494 Attributes: map[string]*configschema.Attribute{ 495 "name": {Type: cty.String, Optional: true}, 496 "forced": {Type: cty.String, Optional: true}, 497 }, 498 }, 499 RequiredReplace: cty.NewPathSet(cty.Path{ 500 cty.GetAttrStep{Name: "forced"}, 501 }), 502 ExpectedOutput: ` # test_instance.example must be replaced 503 -/+ resource "test_instance" "example" { 504 + forced = "example" # forces replacement 505 name = "name" 506 } 507 `, 508 }, 509 "force replacement with empty before value legacy": { 510 Action: plans.DeleteThenCreate, 511 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 512 Mode: addrs.ManagedResourceMode, 513 Before: cty.ObjectVal(map[string]cty.Value{ 514 "name": cty.StringVal("name"), 515 "forced": cty.StringVal(""), 516 }), 517 After: cty.ObjectVal(map[string]cty.Value{ 518 "name": cty.StringVal("name"), 519 "forced": cty.StringVal("example"), 520 }), 521 Schema: &configschema.Block{ 522 Attributes: map[string]*configschema.Attribute{ 523 "name": {Type: cty.String, Optional: true}, 524 "forced": {Type: cty.String, Optional: true}, 525 }, 526 }, 527 RequiredReplace: cty.NewPathSet(cty.Path{ 528 cty.GetAttrStep{Name: "forced"}, 529 }), 530 ExpectedOutput: ` # test_instance.example must be replaced 531 -/+ resource "test_instance" "example" { 532 + forced = "example" # forces replacement 533 name = "name" 534 } 535 `, 536 }, 537 "read during apply because of unknown configuration": { 538 Action: plans.Read, 539 ActionReason: plans.ResourceInstanceReadBecauseConfigUnknown, 540 Mode: addrs.DataResourceMode, 541 Before: cty.ObjectVal(map[string]cty.Value{ 542 "name": cty.StringVal("name"), 543 }), 544 After: cty.ObjectVal(map[string]cty.Value{ 545 "name": cty.StringVal("name"), 546 }), 547 Schema: &configschema.Block{ 548 Attributes: map[string]*configschema.Attribute{ 549 "name": {Type: cty.String, Optional: true}, 550 }, 551 }, 552 ExpectedOutput: ` # data.test_instance.example will be read during apply 553 # (config refers to values not yet known) 554 <= data "test_instance" "example" { 555 name = "name" 556 } 557 `, 558 }, 559 "read during apply because of pending changes to upstream dependency": { 560 Action: plans.Read, 561 ActionReason: plans.ResourceInstanceReadBecauseDependencyPending, 562 Mode: addrs.DataResourceMode, 563 Before: cty.ObjectVal(map[string]cty.Value{ 564 "name": cty.StringVal("name"), 565 }), 566 After: cty.ObjectVal(map[string]cty.Value{ 567 "name": cty.StringVal("name"), 568 }), 569 Schema: &configschema.Block{ 570 Attributes: map[string]*configschema.Attribute{ 571 "name": {Type: cty.String, Optional: true}, 572 }, 573 }, 574 ExpectedOutput: ` # data.test_instance.example will be read during apply 575 # (depends on a resource or a module with changes pending) 576 <= data "test_instance" "example" { 577 name = "name" 578 } 579 `, 580 }, 581 "read during apply for unspecified reason": { 582 Action: plans.Read, 583 Mode: addrs.DataResourceMode, 584 Before: cty.ObjectVal(map[string]cty.Value{ 585 "name": cty.StringVal("name"), 586 }), 587 After: cty.ObjectVal(map[string]cty.Value{ 588 "name": cty.StringVal("name"), 589 }), 590 Schema: &configschema.Block{ 591 Attributes: map[string]*configschema.Attribute{ 592 "name": {Type: cty.String, Optional: true}, 593 }, 594 }, 595 ExpectedOutput: ` # data.test_instance.example will be read during apply 596 <= data "test_instance" "example" { 597 name = "name" 598 } 599 `, 600 }, 601 "show all identifying attributes even if unchanged": { 602 Action: plans.Update, 603 Mode: addrs.ManagedResourceMode, 604 Before: cty.ObjectVal(map[string]cty.Value{ 605 "id": cty.StringVal("i-02ae66f368e8518a9"), 606 "ami": cty.StringVal("ami-BEFORE"), 607 "bar": cty.StringVal("bar"), 608 "foo": cty.StringVal("foo"), 609 "name": cty.StringVal("alice"), 610 "tags": cty.MapVal(map[string]cty.Value{ 611 "name": cty.StringVal("bob"), 612 }), 613 }), 614 After: cty.ObjectVal(map[string]cty.Value{ 615 "id": cty.StringVal("i-02ae66f368e8518a9"), 616 "ami": cty.StringVal("ami-AFTER"), 617 "bar": cty.StringVal("bar"), 618 "foo": cty.StringVal("foo"), 619 "name": cty.StringVal("alice"), 620 "tags": cty.MapVal(map[string]cty.Value{ 621 "name": cty.StringVal("bob"), 622 }), 623 }), 624 Schema: &configschema.Block{ 625 Attributes: map[string]*configschema.Attribute{ 626 "id": {Type: cty.String, Optional: true, Computed: true}, 627 "ami": {Type: cty.String, Optional: true}, 628 "bar": {Type: cty.String, Optional: true}, 629 "foo": {Type: cty.String, Optional: true}, 630 "name": {Type: cty.String, Optional: true}, 631 "tags": {Type: cty.Map(cty.String), Optional: true}, 632 }, 633 }, 634 RequiredReplace: cty.NewPathSet(), 635 ExpectedOutput: ` # test_instance.example will be updated in-place 636 ~ resource "test_instance" "example" { 637 ~ ami = "ami-BEFORE" -> "ami-AFTER" 638 id = "i-02ae66f368e8518a9" 639 name = "alice" 640 tags = { 641 "name" = "bob" 642 } 643 # (2 unchanged attributes hidden) 644 } 645 `, 646 }, 647 } 648 649 runTestCases(t, testCases) 650 } 651 652 func TestResourceChange_JSON(t *testing.T) { 653 testCases := map[string]testCase{ 654 "creation": { 655 Action: plans.Create, 656 Mode: addrs.ManagedResourceMode, 657 Before: cty.NullVal(cty.EmptyObject), 658 After: cty.ObjectVal(map[string]cty.Value{ 659 "id": cty.UnknownVal(cty.String), 660 "json_field": cty.StringVal(`{ 661 "str": "value", 662 "list":["a","b", 234, true], 663 "obj": {"key": "val"} 664 }`), 665 }), 666 Schema: &configschema.Block{ 667 Attributes: map[string]*configschema.Attribute{ 668 "id": {Type: cty.String, Optional: true, Computed: true}, 669 "json_field": {Type: cty.String, Optional: true}, 670 }, 671 }, 672 RequiredReplace: cty.NewPathSet(), 673 ExpectedOutput: ` # test_instance.example will be created 674 + resource "test_instance" "example" { 675 + id = (known after apply) 676 + json_field = jsonencode( 677 { 678 + list = [ 679 + "a", 680 + "b", 681 + 234, 682 + true, 683 ] 684 + obj = { 685 + key = "val" 686 } 687 + str = "value" 688 } 689 ) 690 } 691 `, 692 }, 693 "in-place update of object": { 694 Action: plans.Update, 695 Mode: addrs.ManagedResourceMode, 696 Before: cty.ObjectVal(map[string]cty.Value{ 697 "id": cty.StringVal("i-02ae66f368e8518a9"), 698 "json_field": cty.StringVal(`{"aaa": "value","ccc": 5}`), 699 }), 700 After: cty.ObjectVal(map[string]cty.Value{ 701 "id": cty.UnknownVal(cty.String), 702 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), 703 }), 704 Schema: &configschema.Block{ 705 Attributes: map[string]*configschema.Attribute{ 706 "id": {Type: cty.String, Optional: true, Computed: true}, 707 "json_field": {Type: cty.String, Optional: true}, 708 }, 709 }, 710 RequiredReplace: cty.NewPathSet(), 711 ExpectedOutput: ` # test_instance.example will be updated in-place 712 ~ resource "test_instance" "example" { 713 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 714 ~ json_field = jsonencode( 715 ~ { 716 + bbb = "new_value" 717 - ccc = 5 -> null 718 # (1 unchanged element hidden) 719 } 720 ) 721 } 722 `, 723 }, 724 "in-place update of object with quoted keys": { 725 Action: plans.Update, 726 Mode: addrs.ManagedResourceMode, 727 Before: cty.ObjectVal(map[string]cty.Value{ 728 "id": cty.StringVal("i-02ae66f368e8518a9"), 729 "json_field": cty.StringVal(`{"aaa": "value", "c:c": "old_value"}`), 730 }), 731 After: cty.ObjectVal(map[string]cty.Value{ 732 "id": cty.UnknownVal(cty.String), 733 "json_field": cty.StringVal(`{"aaa": "value", "b:bb": "new_value"}`), 734 }), 735 Schema: &configschema.Block{ 736 Attributes: map[string]*configschema.Attribute{ 737 "id": {Type: cty.String, Optional: true, Computed: true}, 738 "json_field": {Type: cty.String, Optional: true}, 739 }, 740 }, 741 RequiredReplace: cty.NewPathSet(), 742 ExpectedOutput: ` # test_instance.example will be updated in-place 743 ~ resource "test_instance" "example" { 744 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 745 ~ json_field = jsonencode( 746 ~ { 747 + "b:bb" = "new_value" 748 - "c:c" = "old_value" -> null 749 # (1 unchanged element hidden) 750 } 751 ) 752 } 753 `, 754 }, 755 "in-place update (from empty tuple)": { 756 Action: plans.Update, 757 Mode: addrs.ManagedResourceMode, 758 Before: cty.ObjectVal(map[string]cty.Value{ 759 "id": cty.StringVal("i-02ae66f368e8518a9"), 760 "json_field": cty.StringVal(`{"aaa": []}`), 761 }), 762 After: cty.ObjectVal(map[string]cty.Value{ 763 "id": cty.UnknownVal(cty.String), 764 "json_field": cty.StringVal(`{"aaa": ["value"]}`), 765 }), 766 Schema: &configschema.Block{ 767 Attributes: map[string]*configschema.Attribute{ 768 "id": {Type: cty.String, Optional: true, Computed: true}, 769 "json_field": {Type: cty.String, Optional: true}, 770 }, 771 }, 772 RequiredReplace: cty.NewPathSet(), 773 ExpectedOutput: ` # test_instance.example will be updated in-place 774 ~ resource "test_instance" "example" { 775 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 776 ~ json_field = jsonencode( 777 ~ { 778 ~ aaa = [ 779 + "value", 780 ] 781 } 782 ) 783 } 784 `, 785 }, 786 "in-place update (to empty tuple)": { 787 Action: plans.Update, 788 Mode: addrs.ManagedResourceMode, 789 Before: cty.ObjectVal(map[string]cty.Value{ 790 "id": cty.StringVal("i-02ae66f368e8518a9"), 791 "json_field": cty.StringVal(`{"aaa": ["value"]}`), 792 }), 793 After: cty.ObjectVal(map[string]cty.Value{ 794 "id": cty.UnknownVal(cty.String), 795 "json_field": cty.StringVal(`{"aaa": []}`), 796 }), 797 Schema: &configschema.Block{ 798 Attributes: map[string]*configschema.Attribute{ 799 "id": {Type: cty.String, Optional: true, Computed: true}, 800 "json_field": {Type: cty.String, Optional: true}, 801 }, 802 }, 803 RequiredReplace: cty.NewPathSet(), 804 ExpectedOutput: ` # test_instance.example will be updated in-place 805 ~ resource "test_instance" "example" { 806 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 807 ~ json_field = jsonencode( 808 ~ { 809 ~ aaa = [ 810 - "value", 811 ] 812 } 813 ) 814 } 815 `, 816 }, 817 "in-place update (tuple of different types)": { 818 Action: plans.Update, 819 Mode: addrs.ManagedResourceMode, 820 Before: cty.ObjectVal(map[string]cty.Value{ 821 "id": cty.StringVal("i-02ae66f368e8518a9"), 822 "json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`), 823 }), 824 After: cty.ObjectVal(map[string]cty.Value{ 825 "id": cty.UnknownVal(cty.String), 826 "json_field": cty.StringVal(`{"aaa": [42, {"foo":"baz"}, "value"]}`), 827 }), 828 Schema: &configschema.Block{ 829 Attributes: map[string]*configschema.Attribute{ 830 "id": {Type: cty.String, Optional: true, Computed: true}, 831 "json_field": {Type: cty.String, Optional: true}, 832 }, 833 }, 834 RequiredReplace: cty.NewPathSet(), 835 ExpectedOutput: ` # test_instance.example will be updated in-place 836 ~ resource "test_instance" "example" { 837 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 838 ~ json_field = jsonencode( 839 ~ { 840 ~ aaa = [ 841 42, 842 ~ { 843 ~ foo = "bar" -> "baz" 844 }, 845 "value", 846 ] 847 } 848 ) 849 } 850 `, 851 }, 852 "force-new update": { 853 Action: plans.DeleteThenCreate, 854 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 855 Mode: addrs.ManagedResourceMode, 856 Before: cty.ObjectVal(map[string]cty.Value{ 857 "id": cty.StringVal("i-02ae66f368e8518a9"), 858 "json_field": cty.StringVal(`{"aaa": "value"}`), 859 }), 860 After: cty.ObjectVal(map[string]cty.Value{ 861 "id": cty.UnknownVal(cty.String), 862 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), 863 }), 864 Schema: &configschema.Block{ 865 Attributes: map[string]*configschema.Attribute{ 866 "id": {Type: cty.String, Optional: true, Computed: true}, 867 "json_field": {Type: cty.String, Optional: true}, 868 }, 869 }, 870 RequiredReplace: cty.NewPathSet(cty.Path{ 871 cty.GetAttrStep{Name: "json_field"}, 872 }), 873 ExpectedOutput: ` # test_instance.example must be replaced 874 -/+ resource "test_instance" "example" { 875 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 876 ~ json_field = jsonencode( 877 ~ { 878 + bbb = "new_value" 879 # (1 unchanged element hidden) 880 } # forces replacement 881 ) 882 } 883 `, 884 }, 885 "in-place update (whitespace change)": { 886 Action: plans.Update, 887 Mode: addrs.ManagedResourceMode, 888 Before: cty.ObjectVal(map[string]cty.Value{ 889 "id": cty.StringVal("i-02ae66f368e8518a9"), 890 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), 891 }), 892 After: cty.ObjectVal(map[string]cty.Value{ 893 "id": cty.UnknownVal(cty.String), 894 "json_field": cty.StringVal(`{"aaa":"value", 895 "bbb":"another"}`), 896 }), 897 Schema: &configschema.Block{ 898 Attributes: map[string]*configschema.Attribute{ 899 "id": {Type: cty.String, Optional: true, Computed: true}, 900 "json_field": {Type: cty.String, Optional: true}, 901 }, 902 }, 903 RequiredReplace: cty.NewPathSet(), 904 ExpectedOutput: ` # test_instance.example will be updated in-place 905 ~ resource "test_instance" "example" { 906 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 907 ~ json_field = jsonencode( # whitespace changes 908 { 909 aaa = "value" 910 bbb = "another" 911 } 912 ) 913 } 914 `, 915 }, 916 "force-new update (whitespace change)": { 917 Action: plans.DeleteThenCreate, 918 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 919 Mode: addrs.ManagedResourceMode, 920 Before: cty.ObjectVal(map[string]cty.Value{ 921 "id": cty.StringVal("i-02ae66f368e8518a9"), 922 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), 923 }), 924 After: cty.ObjectVal(map[string]cty.Value{ 925 "id": cty.UnknownVal(cty.String), 926 "json_field": cty.StringVal(`{"aaa":"value", 927 "bbb":"another"}`), 928 }), 929 Schema: &configschema.Block{ 930 Attributes: map[string]*configschema.Attribute{ 931 "id": {Type: cty.String, Optional: true, Computed: true}, 932 "json_field": {Type: cty.String, Optional: true}, 933 }, 934 }, 935 RequiredReplace: cty.NewPathSet(cty.Path{ 936 cty.GetAttrStep{Name: "json_field"}, 937 }), 938 ExpectedOutput: ` # test_instance.example must be replaced 939 -/+ resource "test_instance" "example" { 940 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 941 ~ json_field = jsonencode( # whitespace changes force replacement 942 { 943 aaa = "value" 944 bbb = "another" 945 } 946 ) 947 } 948 `, 949 }, 950 "creation (empty)": { 951 Action: plans.Create, 952 Mode: addrs.ManagedResourceMode, 953 Before: cty.NullVal(cty.EmptyObject), 954 After: cty.ObjectVal(map[string]cty.Value{ 955 "id": cty.UnknownVal(cty.String), 956 "json_field": cty.StringVal(`{}`), 957 }), 958 Schema: &configschema.Block{ 959 Attributes: map[string]*configschema.Attribute{ 960 "id": {Type: cty.String, Optional: true, Computed: true}, 961 "json_field": {Type: cty.String, Optional: true}, 962 }, 963 }, 964 RequiredReplace: cty.NewPathSet(), 965 ExpectedOutput: ` # test_instance.example will be created 966 + resource "test_instance" "example" { 967 + id = (known after apply) 968 + json_field = jsonencode({}) 969 } 970 `, 971 }, 972 "JSON list item removal": { 973 Action: plans.Update, 974 Mode: addrs.ManagedResourceMode, 975 Before: cty.ObjectVal(map[string]cty.Value{ 976 "id": cty.StringVal("i-02ae66f368e8518a9"), 977 "json_field": cty.StringVal(`["first","second","third"]`), 978 }), 979 After: cty.ObjectVal(map[string]cty.Value{ 980 "id": cty.UnknownVal(cty.String), 981 "json_field": cty.StringVal(`["first","second"]`), 982 }), 983 Schema: &configschema.Block{ 984 Attributes: map[string]*configschema.Attribute{ 985 "id": {Type: cty.String, Optional: true, Computed: true}, 986 "json_field": {Type: cty.String, Optional: true}, 987 }, 988 }, 989 RequiredReplace: cty.NewPathSet(), 990 ExpectedOutput: ` # test_instance.example will be updated in-place 991 ~ resource "test_instance" "example" { 992 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 993 ~ json_field = jsonencode( 994 ~ [ 995 # (1 unchanged element hidden) 996 "second", 997 - "third", 998 ] 999 ) 1000 } 1001 `, 1002 }, 1003 "JSON list item addition": { 1004 Action: plans.Update, 1005 Mode: addrs.ManagedResourceMode, 1006 Before: cty.ObjectVal(map[string]cty.Value{ 1007 "id": cty.StringVal("i-02ae66f368e8518a9"), 1008 "json_field": cty.StringVal(`["first","second"]`), 1009 }), 1010 After: cty.ObjectVal(map[string]cty.Value{ 1011 "id": cty.UnknownVal(cty.String), 1012 "json_field": cty.StringVal(`["first","second","third"]`), 1013 }), 1014 Schema: &configschema.Block{ 1015 Attributes: map[string]*configschema.Attribute{ 1016 "id": {Type: cty.String, Optional: true, Computed: true}, 1017 "json_field": {Type: cty.String, Optional: true}, 1018 }, 1019 }, 1020 RequiredReplace: cty.NewPathSet(), 1021 ExpectedOutput: ` # test_instance.example will be updated in-place 1022 ~ resource "test_instance" "example" { 1023 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1024 ~ json_field = jsonencode( 1025 ~ [ 1026 # (1 unchanged element hidden) 1027 "second", 1028 + "third", 1029 ] 1030 ) 1031 } 1032 `, 1033 }, 1034 "JSON list object addition": { 1035 Action: plans.Update, 1036 Mode: addrs.ManagedResourceMode, 1037 Before: cty.ObjectVal(map[string]cty.Value{ 1038 "id": cty.StringVal("i-02ae66f368e8518a9"), 1039 "json_field": cty.StringVal(`{"first":"111"}`), 1040 }), 1041 After: cty.ObjectVal(map[string]cty.Value{ 1042 "id": cty.UnknownVal(cty.String), 1043 "json_field": cty.StringVal(`{"first":"111","second":"222"}`), 1044 }), 1045 Schema: &configschema.Block{ 1046 Attributes: map[string]*configschema.Attribute{ 1047 "id": {Type: cty.String, Optional: true, Computed: true}, 1048 "json_field": {Type: cty.String, Optional: true}, 1049 }, 1050 }, 1051 RequiredReplace: cty.NewPathSet(), 1052 ExpectedOutput: ` # test_instance.example will be updated in-place 1053 ~ resource "test_instance" "example" { 1054 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1055 ~ json_field = jsonencode( 1056 ~ { 1057 + second = "222" 1058 # (1 unchanged element hidden) 1059 } 1060 ) 1061 } 1062 `, 1063 }, 1064 "JSON object with nested list": { 1065 Action: plans.Update, 1066 Mode: addrs.ManagedResourceMode, 1067 Before: cty.ObjectVal(map[string]cty.Value{ 1068 "id": cty.StringVal("i-02ae66f368e8518a9"), 1069 "json_field": cty.StringVal(`{ 1070 "Statement": ["first"] 1071 }`), 1072 }), 1073 After: cty.ObjectVal(map[string]cty.Value{ 1074 "id": cty.UnknownVal(cty.String), 1075 "json_field": cty.StringVal(`{ 1076 "Statement": ["first", "second"] 1077 }`), 1078 }), 1079 Schema: &configschema.Block{ 1080 Attributes: map[string]*configschema.Attribute{ 1081 "id": {Type: cty.String, Optional: true, Computed: true}, 1082 "json_field": {Type: cty.String, Optional: true}, 1083 }, 1084 }, 1085 RequiredReplace: cty.NewPathSet(), 1086 ExpectedOutput: ` # test_instance.example will be updated in-place 1087 ~ resource "test_instance" "example" { 1088 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1089 ~ json_field = jsonencode( 1090 ~ { 1091 ~ Statement = [ 1092 "first", 1093 + "second", 1094 ] 1095 } 1096 ) 1097 } 1098 `, 1099 }, 1100 "JSON list of objects - adding item": { 1101 Action: plans.Update, 1102 Mode: addrs.ManagedResourceMode, 1103 Before: cty.ObjectVal(map[string]cty.Value{ 1104 "id": cty.StringVal("i-02ae66f368e8518a9"), 1105 "json_field": cty.StringVal(`[{"one": "111"}]`), 1106 }), 1107 After: cty.ObjectVal(map[string]cty.Value{ 1108 "id": cty.UnknownVal(cty.String), 1109 "json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`), 1110 }), 1111 Schema: &configschema.Block{ 1112 Attributes: map[string]*configschema.Attribute{ 1113 "id": {Type: cty.String, Optional: true, Computed: true}, 1114 "json_field": {Type: cty.String, Optional: true}, 1115 }, 1116 }, 1117 RequiredReplace: cty.NewPathSet(), 1118 ExpectedOutput: ` # test_instance.example will be updated in-place 1119 ~ resource "test_instance" "example" { 1120 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1121 ~ json_field = jsonencode( 1122 ~ [ 1123 { 1124 one = "111" 1125 }, 1126 + { 1127 + two = "222" 1128 }, 1129 ] 1130 ) 1131 } 1132 `, 1133 }, 1134 "JSON list of objects - removing item": { 1135 Action: plans.Update, 1136 Mode: addrs.ManagedResourceMode, 1137 Before: cty.ObjectVal(map[string]cty.Value{ 1138 "id": cty.StringVal("i-02ae66f368e8518a9"), 1139 "json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}, {"three": "333"}]`), 1140 }), 1141 After: cty.ObjectVal(map[string]cty.Value{ 1142 "id": cty.UnknownVal(cty.String), 1143 "json_field": cty.StringVal(`[{"one": "111"}, {"three": "333"}]`), 1144 }), 1145 Schema: &configschema.Block{ 1146 Attributes: map[string]*configschema.Attribute{ 1147 "id": {Type: cty.String, Optional: true, Computed: true}, 1148 "json_field": {Type: cty.String, Optional: true}, 1149 }, 1150 }, 1151 RequiredReplace: cty.NewPathSet(), 1152 ExpectedOutput: ` # test_instance.example will be updated in-place 1153 ~ resource "test_instance" "example" { 1154 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1155 ~ json_field = jsonencode( 1156 ~ [ 1157 { 1158 one = "111" 1159 }, 1160 - { 1161 - two = "222" 1162 }, 1163 { 1164 three = "333" 1165 }, 1166 ] 1167 ) 1168 } 1169 `, 1170 }, 1171 "JSON object with list of objects": { 1172 Action: plans.Update, 1173 Mode: addrs.ManagedResourceMode, 1174 Before: cty.ObjectVal(map[string]cty.Value{ 1175 "id": cty.StringVal("i-02ae66f368e8518a9"), 1176 "json_field": cty.StringVal(`{"parent":[{"one": "111"}]}`), 1177 }), 1178 After: cty.ObjectVal(map[string]cty.Value{ 1179 "id": cty.UnknownVal(cty.String), 1180 "json_field": cty.StringVal(`{"parent":[{"one": "111"}, {"two": "222"}]}`), 1181 }), 1182 Schema: &configschema.Block{ 1183 Attributes: map[string]*configschema.Attribute{ 1184 "id": {Type: cty.String, Optional: true, Computed: true}, 1185 "json_field": {Type: cty.String, Optional: true}, 1186 }, 1187 }, 1188 RequiredReplace: cty.NewPathSet(), 1189 ExpectedOutput: ` # test_instance.example will be updated in-place 1190 ~ resource "test_instance" "example" { 1191 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1192 ~ json_field = jsonencode( 1193 ~ { 1194 ~ parent = [ 1195 { 1196 one = "111" 1197 }, 1198 + { 1199 + two = "222" 1200 }, 1201 ] 1202 } 1203 ) 1204 } 1205 `, 1206 }, 1207 "JSON object double nested lists": { 1208 Action: plans.Update, 1209 Mode: addrs.ManagedResourceMode, 1210 Before: cty.ObjectVal(map[string]cty.Value{ 1211 "id": cty.StringVal("i-02ae66f368e8518a9"), 1212 "json_field": cty.StringVal(`{"parent":[{"another_list": ["111"]}]}`), 1213 }), 1214 After: cty.ObjectVal(map[string]cty.Value{ 1215 "id": cty.UnknownVal(cty.String), 1216 "json_field": cty.StringVal(`{"parent":[{"another_list": ["111", "222"]}]}`), 1217 }), 1218 Schema: &configschema.Block{ 1219 Attributes: map[string]*configschema.Attribute{ 1220 "id": {Type: cty.String, Optional: true, Computed: true}, 1221 "json_field": {Type: cty.String, Optional: true}, 1222 }, 1223 }, 1224 RequiredReplace: cty.NewPathSet(), 1225 ExpectedOutput: ` # test_instance.example will be updated in-place 1226 ~ resource "test_instance" "example" { 1227 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1228 ~ json_field = jsonencode( 1229 ~ { 1230 ~ parent = [ 1231 ~ { 1232 ~ another_list = [ 1233 "111", 1234 + "222", 1235 ] 1236 }, 1237 ] 1238 } 1239 ) 1240 } 1241 `, 1242 }, 1243 "in-place update from object to tuple": { 1244 Action: plans.Update, 1245 Mode: addrs.ManagedResourceMode, 1246 Before: cty.ObjectVal(map[string]cty.Value{ 1247 "id": cty.StringVal("i-02ae66f368e8518a9"), 1248 "json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`), 1249 }), 1250 After: cty.ObjectVal(map[string]cty.Value{ 1251 "id": cty.UnknownVal(cty.String), 1252 "json_field": cty.StringVal(`["aaa", 42, "something"]`), 1253 }), 1254 Schema: &configschema.Block{ 1255 Attributes: map[string]*configschema.Attribute{ 1256 "id": {Type: cty.String, Optional: true, Computed: true}, 1257 "json_field": {Type: cty.String, Optional: true}, 1258 }, 1259 }, 1260 RequiredReplace: cty.NewPathSet(), 1261 ExpectedOutput: ` # test_instance.example will be updated in-place 1262 ~ resource "test_instance" "example" { 1263 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1264 ~ json_field = jsonencode( 1265 ~ { 1266 - aaa = [ 1267 - 42, 1268 - { 1269 - foo = "bar" 1270 }, 1271 - "value", 1272 ] 1273 } -> [ 1274 + "aaa", 1275 + 42, 1276 + "something", 1277 ] 1278 ) 1279 } 1280 `, 1281 }, 1282 } 1283 runTestCases(t, testCases) 1284 } 1285 1286 func TestResourceChange_listObject(t *testing.T) { 1287 testCases := map[string]testCase{ 1288 // https://github.com/hashicorp/terraform/issues/30641 1289 "updating non-identifying attribute": { 1290 Action: plans.Update, 1291 Mode: addrs.ManagedResourceMode, 1292 Before: cty.ObjectVal(map[string]cty.Value{ 1293 "id": cty.StringVal("i-02ae66f368e8518a9"), 1294 "accounts": cty.ListVal([]cty.Value{ 1295 cty.ObjectVal(map[string]cty.Value{ 1296 "id": cty.StringVal("1"), 1297 "name": cty.StringVal("production"), 1298 "status": cty.StringVal("ACTIVE"), 1299 }), 1300 cty.ObjectVal(map[string]cty.Value{ 1301 "id": cty.StringVal("2"), 1302 "name": cty.StringVal("staging"), 1303 "status": cty.StringVal("ACTIVE"), 1304 }), 1305 cty.ObjectVal(map[string]cty.Value{ 1306 "id": cty.StringVal("3"), 1307 "name": cty.StringVal("disaster-recovery"), 1308 "status": cty.StringVal("ACTIVE"), 1309 }), 1310 }), 1311 }), 1312 After: cty.ObjectVal(map[string]cty.Value{ 1313 "id": cty.UnknownVal(cty.String), 1314 "accounts": cty.ListVal([]cty.Value{ 1315 cty.ObjectVal(map[string]cty.Value{ 1316 "id": cty.StringVal("1"), 1317 "name": cty.StringVal("production"), 1318 "status": cty.StringVal("ACTIVE"), 1319 }), 1320 cty.ObjectVal(map[string]cty.Value{ 1321 "id": cty.StringVal("2"), 1322 "name": cty.StringVal("staging"), 1323 "status": cty.StringVal("EXPLODED"), 1324 }), 1325 cty.ObjectVal(map[string]cty.Value{ 1326 "id": cty.StringVal("3"), 1327 "name": cty.StringVal("disaster-recovery"), 1328 "status": cty.StringVal("ACTIVE"), 1329 }), 1330 }), 1331 }), 1332 Schema: &configschema.Block{ 1333 Attributes: map[string]*configschema.Attribute{ 1334 "id": {Type: cty.String, Optional: true, Computed: true}, 1335 "accounts": { 1336 Type: cty.List(cty.Object(map[string]cty.Type{ 1337 "id": cty.String, 1338 "name": cty.String, 1339 "status": cty.String, 1340 })), 1341 }, 1342 }, 1343 }, 1344 RequiredReplace: cty.NewPathSet(), 1345 ExpectedOutput: ` # test_instance.example will be updated in-place 1346 ~ resource "test_instance" "example" { 1347 ~ accounts = [ 1348 { 1349 id = "1" 1350 name = "production" 1351 status = "ACTIVE" 1352 }, 1353 ~ { 1354 id = "2" 1355 name = "staging" 1356 ~ status = "ACTIVE" -> "EXPLODED" 1357 }, 1358 { 1359 id = "3" 1360 name = "disaster-recovery" 1361 status = "ACTIVE" 1362 }, 1363 ] 1364 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1365 } 1366 `, 1367 }, 1368 } 1369 runTestCases(t, testCases) 1370 } 1371 1372 func TestResourceChange_primitiveList(t *testing.T) { 1373 testCases := map[string]testCase{ 1374 "in-place update - creation": { 1375 Action: plans.Update, 1376 Mode: addrs.ManagedResourceMode, 1377 Before: cty.ObjectVal(map[string]cty.Value{ 1378 "id": cty.StringVal("i-02ae66f368e8518a9"), 1379 "ami": cty.StringVal("ami-STATIC"), 1380 "list_field": cty.NullVal(cty.List(cty.String)), 1381 }), 1382 After: cty.ObjectVal(map[string]cty.Value{ 1383 "id": cty.UnknownVal(cty.String), 1384 "ami": cty.StringVal("ami-STATIC"), 1385 "list_field": cty.ListVal([]cty.Value{ 1386 cty.StringVal("new-element"), 1387 }), 1388 }), 1389 Schema: &configschema.Block{ 1390 Attributes: map[string]*configschema.Attribute{ 1391 "id": {Type: cty.String, Optional: true, Computed: true}, 1392 "ami": {Type: cty.String, Optional: true}, 1393 "list_field": {Type: cty.List(cty.String), Optional: true}, 1394 }, 1395 }, 1396 RequiredReplace: cty.NewPathSet(), 1397 ExpectedOutput: ` # test_instance.example will be updated in-place 1398 ~ resource "test_instance" "example" { 1399 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1400 + list_field = [ 1401 + "new-element", 1402 ] 1403 # (1 unchanged attribute hidden) 1404 } 1405 `, 1406 }, 1407 "in-place update - first addition": { 1408 Action: plans.Update, 1409 Mode: addrs.ManagedResourceMode, 1410 Before: cty.ObjectVal(map[string]cty.Value{ 1411 "id": cty.StringVal("i-02ae66f368e8518a9"), 1412 "ami": cty.StringVal("ami-STATIC"), 1413 "list_field": cty.ListValEmpty(cty.String), 1414 }), 1415 After: cty.ObjectVal(map[string]cty.Value{ 1416 "id": cty.UnknownVal(cty.String), 1417 "ami": cty.StringVal("ami-STATIC"), 1418 "list_field": cty.ListVal([]cty.Value{ 1419 cty.StringVal("new-element"), 1420 }), 1421 }), 1422 Schema: &configschema.Block{ 1423 Attributes: map[string]*configschema.Attribute{ 1424 "id": {Type: cty.String, Optional: true, Computed: true}, 1425 "ami": {Type: cty.String, Optional: true}, 1426 "list_field": {Type: cty.List(cty.String), Optional: true}, 1427 }, 1428 }, 1429 RequiredReplace: cty.NewPathSet(), 1430 ExpectedOutput: ` # test_instance.example will be updated in-place 1431 ~ resource "test_instance" "example" { 1432 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1433 ~ list_field = [ 1434 + "new-element", 1435 ] 1436 # (1 unchanged attribute hidden) 1437 } 1438 `, 1439 }, 1440 "in-place update - insertion": { 1441 Action: plans.Update, 1442 Mode: addrs.ManagedResourceMode, 1443 Before: cty.ObjectVal(map[string]cty.Value{ 1444 "id": cty.StringVal("i-02ae66f368e8518a9"), 1445 "ami": cty.StringVal("ami-STATIC"), 1446 "list_field": cty.ListVal([]cty.Value{ 1447 cty.StringVal("aaaa"), 1448 cty.StringVal("bbbb"), 1449 cty.StringVal("dddd"), 1450 cty.StringVal("eeee"), 1451 cty.StringVal("ffff"), 1452 }), 1453 }), 1454 After: cty.ObjectVal(map[string]cty.Value{ 1455 "id": cty.UnknownVal(cty.String), 1456 "ami": cty.StringVal("ami-STATIC"), 1457 "list_field": cty.ListVal([]cty.Value{ 1458 cty.StringVal("aaaa"), 1459 cty.StringVal("bbbb"), 1460 cty.StringVal("cccc"), 1461 cty.StringVal("dddd"), 1462 cty.StringVal("eeee"), 1463 cty.StringVal("ffff"), 1464 }), 1465 }), 1466 Schema: &configschema.Block{ 1467 Attributes: map[string]*configschema.Attribute{ 1468 "id": {Type: cty.String, Optional: true, Computed: true}, 1469 "ami": {Type: cty.String, Optional: true}, 1470 "list_field": {Type: cty.List(cty.String), Optional: true}, 1471 }, 1472 }, 1473 RequiredReplace: cty.NewPathSet(), 1474 ExpectedOutput: ` # test_instance.example will be updated in-place 1475 ~ resource "test_instance" "example" { 1476 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1477 ~ list_field = [ 1478 # (1 unchanged element hidden) 1479 "bbbb", 1480 + "cccc", 1481 "dddd", 1482 # (2 unchanged elements hidden) 1483 ] 1484 # (1 unchanged attribute hidden) 1485 } 1486 `, 1487 }, 1488 "force-new update - insertion": { 1489 Action: plans.DeleteThenCreate, 1490 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 1491 Mode: addrs.ManagedResourceMode, 1492 Before: cty.ObjectVal(map[string]cty.Value{ 1493 "id": cty.StringVal("i-02ae66f368e8518a9"), 1494 "ami": cty.StringVal("ami-STATIC"), 1495 "list_field": cty.ListVal([]cty.Value{ 1496 cty.StringVal("aaaa"), 1497 cty.StringVal("cccc"), 1498 }), 1499 }), 1500 After: cty.ObjectVal(map[string]cty.Value{ 1501 "id": cty.UnknownVal(cty.String), 1502 "ami": cty.StringVal("ami-STATIC"), 1503 "list_field": cty.ListVal([]cty.Value{ 1504 cty.StringVal("aaaa"), 1505 cty.StringVal("bbbb"), 1506 cty.StringVal("cccc"), 1507 }), 1508 }), 1509 Schema: &configschema.Block{ 1510 Attributes: map[string]*configschema.Attribute{ 1511 "id": {Type: cty.String, Optional: true, Computed: true}, 1512 "ami": {Type: cty.String, Optional: true}, 1513 "list_field": {Type: cty.List(cty.String), Optional: true}, 1514 }, 1515 }, 1516 RequiredReplace: cty.NewPathSet(cty.Path{ 1517 cty.GetAttrStep{Name: "list_field"}, 1518 }), 1519 ExpectedOutput: ` # test_instance.example must be replaced 1520 -/+ resource "test_instance" "example" { 1521 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1522 ~ list_field = [ # forces replacement 1523 "aaaa", 1524 + "bbbb", 1525 "cccc", 1526 ] 1527 # (1 unchanged attribute hidden) 1528 } 1529 `, 1530 }, 1531 "in-place update - deletion": { 1532 Action: plans.Update, 1533 Mode: addrs.ManagedResourceMode, 1534 Before: cty.ObjectVal(map[string]cty.Value{ 1535 "id": cty.StringVal("i-02ae66f368e8518a9"), 1536 "ami": cty.StringVal("ami-STATIC"), 1537 "list_field": cty.ListVal([]cty.Value{ 1538 cty.StringVal("aaaa"), 1539 cty.StringVal("bbbb"), 1540 cty.StringVal("cccc"), 1541 cty.StringVal("dddd"), 1542 cty.StringVal("eeee"), 1543 }), 1544 }), 1545 After: cty.ObjectVal(map[string]cty.Value{ 1546 "id": cty.UnknownVal(cty.String), 1547 "ami": cty.StringVal("ami-STATIC"), 1548 "list_field": cty.ListVal([]cty.Value{ 1549 cty.StringVal("bbbb"), 1550 cty.StringVal("dddd"), 1551 cty.StringVal("eeee"), 1552 }), 1553 }), 1554 Schema: &configschema.Block{ 1555 Attributes: map[string]*configschema.Attribute{ 1556 "id": {Type: cty.String, Optional: true, Computed: true}, 1557 "ami": {Type: cty.String, Optional: true}, 1558 "list_field": {Type: cty.List(cty.String), Optional: true}, 1559 }, 1560 }, 1561 RequiredReplace: cty.NewPathSet(), 1562 ExpectedOutput: ` # test_instance.example will be updated in-place 1563 ~ resource "test_instance" "example" { 1564 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1565 ~ list_field = [ 1566 - "aaaa", 1567 "bbbb", 1568 - "cccc", 1569 "dddd", 1570 # (1 unchanged element hidden) 1571 ] 1572 # (1 unchanged attribute hidden) 1573 } 1574 `, 1575 }, 1576 "creation - empty list": { 1577 Action: plans.Create, 1578 Mode: addrs.ManagedResourceMode, 1579 Before: cty.NullVal(cty.EmptyObject), 1580 After: cty.ObjectVal(map[string]cty.Value{ 1581 "id": cty.UnknownVal(cty.String), 1582 "ami": cty.StringVal("ami-STATIC"), 1583 "list_field": cty.ListValEmpty(cty.String), 1584 }), 1585 Schema: &configschema.Block{ 1586 Attributes: map[string]*configschema.Attribute{ 1587 "id": {Type: cty.String, Optional: true, Computed: true}, 1588 "ami": {Type: cty.String, Optional: true}, 1589 "list_field": {Type: cty.List(cty.String), Optional: true}, 1590 }, 1591 }, 1592 RequiredReplace: cty.NewPathSet(), 1593 ExpectedOutput: ` # test_instance.example will be created 1594 + resource "test_instance" "example" { 1595 + ami = "ami-STATIC" 1596 + id = (known after apply) 1597 + list_field = [] 1598 } 1599 `, 1600 }, 1601 "in-place update - full to empty": { 1602 Action: plans.Update, 1603 Mode: addrs.ManagedResourceMode, 1604 Before: cty.ObjectVal(map[string]cty.Value{ 1605 "id": cty.StringVal("i-02ae66f368e8518a9"), 1606 "ami": cty.StringVal("ami-STATIC"), 1607 "list_field": cty.ListVal([]cty.Value{ 1608 cty.StringVal("aaaa"), 1609 cty.StringVal("bbbb"), 1610 cty.StringVal("cccc"), 1611 }), 1612 }), 1613 After: cty.ObjectVal(map[string]cty.Value{ 1614 "id": cty.UnknownVal(cty.String), 1615 "ami": cty.StringVal("ami-STATIC"), 1616 "list_field": cty.ListValEmpty(cty.String), 1617 }), 1618 Schema: &configschema.Block{ 1619 Attributes: map[string]*configschema.Attribute{ 1620 "id": {Type: cty.String, Optional: true, Computed: true}, 1621 "ami": {Type: cty.String, Optional: true}, 1622 "list_field": {Type: cty.List(cty.String), Optional: true}, 1623 }, 1624 }, 1625 RequiredReplace: cty.NewPathSet(), 1626 ExpectedOutput: ` # test_instance.example will be updated in-place 1627 ~ resource "test_instance" "example" { 1628 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1629 ~ list_field = [ 1630 - "aaaa", 1631 - "bbbb", 1632 - "cccc", 1633 ] 1634 # (1 unchanged attribute hidden) 1635 } 1636 `, 1637 }, 1638 "in-place update - null to empty": { 1639 Action: plans.Update, 1640 Mode: addrs.ManagedResourceMode, 1641 Before: cty.ObjectVal(map[string]cty.Value{ 1642 "id": cty.StringVal("i-02ae66f368e8518a9"), 1643 "ami": cty.StringVal("ami-STATIC"), 1644 "list_field": cty.NullVal(cty.List(cty.String)), 1645 }), 1646 After: cty.ObjectVal(map[string]cty.Value{ 1647 "id": cty.UnknownVal(cty.String), 1648 "ami": cty.StringVal("ami-STATIC"), 1649 "list_field": cty.ListValEmpty(cty.String), 1650 }), 1651 Schema: &configschema.Block{ 1652 Attributes: map[string]*configschema.Attribute{ 1653 "id": {Type: cty.String, Optional: true, Computed: true}, 1654 "ami": {Type: cty.String, Optional: true}, 1655 "list_field": {Type: cty.List(cty.String), Optional: true}, 1656 }, 1657 }, 1658 RequiredReplace: cty.NewPathSet(), 1659 ExpectedOutput: ` # test_instance.example will be updated in-place 1660 ~ resource "test_instance" "example" { 1661 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1662 + list_field = [] 1663 # (1 unchanged attribute hidden) 1664 } 1665 `, 1666 }, 1667 "update to unknown element": { 1668 Action: plans.Update, 1669 Mode: addrs.ManagedResourceMode, 1670 Before: cty.ObjectVal(map[string]cty.Value{ 1671 "id": cty.StringVal("i-02ae66f368e8518a9"), 1672 "ami": cty.StringVal("ami-STATIC"), 1673 "list_field": cty.ListVal([]cty.Value{ 1674 cty.StringVal("aaaa"), 1675 cty.StringVal("bbbb"), 1676 cty.StringVal("cccc"), 1677 }), 1678 }), 1679 After: cty.ObjectVal(map[string]cty.Value{ 1680 "id": cty.UnknownVal(cty.String), 1681 "ami": cty.StringVal("ami-STATIC"), 1682 "list_field": cty.ListVal([]cty.Value{ 1683 cty.StringVal("aaaa"), 1684 cty.UnknownVal(cty.String), 1685 cty.StringVal("cccc"), 1686 }), 1687 }), 1688 Schema: &configschema.Block{ 1689 Attributes: map[string]*configschema.Attribute{ 1690 "id": {Type: cty.String, Optional: true, Computed: true}, 1691 "ami": {Type: cty.String, Optional: true}, 1692 "list_field": {Type: cty.List(cty.String), Optional: true}, 1693 }, 1694 }, 1695 RequiredReplace: cty.NewPathSet(), 1696 ExpectedOutput: ` # test_instance.example will be updated in-place 1697 ~ resource "test_instance" "example" { 1698 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1699 ~ list_field = [ 1700 "aaaa", 1701 - "bbbb", 1702 + (known after apply), 1703 "cccc", 1704 ] 1705 # (1 unchanged attribute hidden) 1706 } 1707 `, 1708 }, 1709 "update - two new unknown elements": { 1710 Action: plans.Update, 1711 Mode: addrs.ManagedResourceMode, 1712 Before: cty.ObjectVal(map[string]cty.Value{ 1713 "id": cty.StringVal("i-02ae66f368e8518a9"), 1714 "ami": cty.StringVal("ami-STATIC"), 1715 "list_field": cty.ListVal([]cty.Value{ 1716 cty.StringVal("aaaa"), 1717 cty.StringVal("bbbb"), 1718 cty.StringVal("cccc"), 1719 cty.StringVal("dddd"), 1720 cty.StringVal("eeee"), 1721 }), 1722 }), 1723 After: cty.ObjectVal(map[string]cty.Value{ 1724 "id": cty.UnknownVal(cty.String), 1725 "ami": cty.StringVal("ami-STATIC"), 1726 "list_field": cty.ListVal([]cty.Value{ 1727 cty.StringVal("aaaa"), 1728 cty.UnknownVal(cty.String), 1729 cty.UnknownVal(cty.String), 1730 cty.StringVal("cccc"), 1731 cty.StringVal("dddd"), 1732 cty.StringVal("eeee"), 1733 }), 1734 }), 1735 Schema: &configschema.Block{ 1736 Attributes: map[string]*configschema.Attribute{ 1737 "id": {Type: cty.String, Optional: true, Computed: true}, 1738 "ami": {Type: cty.String, Optional: true}, 1739 "list_field": {Type: cty.List(cty.String), Optional: true}, 1740 }, 1741 }, 1742 RequiredReplace: cty.NewPathSet(), 1743 ExpectedOutput: ` # test_instance.example will be updated in-place 1744 ~ resource "test_instance" "example" { 1745 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1746 ~ list_field = [ 1747 "aaaa", 1748 - "bbbb", 1749 + (known after apply), 1750 + (known after apply), 1751 "cccc", 1752 # (2 unchanged elements hidden) 1753 ] 1754 # (1 unchanged attribute hidden) 1755 } 1756 `, 1757 }, 1758 } 1759 runTestCases(t, testCases) 1760 } 1761 1762 func TestResourceChange_primitiveTuple(t *testing.T) { 1763 testCases := map[string]testCase{ 1764 "in-place update": { 1765 Action: plans.Update, 1766 Mode: addrs.ManagedResourceMode, 1767 Before: cty.ObjectVal(map[string]cty.Value{ 1768 "id": cty.StringVal("i-02ae66f368e8518a9"), 1769 "tuple_field": cty.TupleVal([]cty.Value{ 1770 cty.StringVal("aaaa"), 1771 cty.StringVal("bbbb"), 1772 cty.StringVal("dddd"), 1773 cty.StringVal("eeee"), 1774 cty.StringVal("ffff"), 1775 }), 1776 }), 1777 After: cty.ObjectVal(map[string]cty.Value{ 1778 "id": cty.StringVal("i-02ae66f368e8518a9"), 1779 "tuple_field": cty.TupleVal([]cty.Value{ 1780 cty.StringVal("aaaa"), 1781 cty.StringVal("bbbb"), 1782 cty.StringVal("cccc"), 1783 cty.StringVal("eeee"), 1784 cty.StringVal("ffff"), 1785 }), 1786 }), 1787 Schema: &configschema.Block{ 1788 Attributes: map[string]*configschema.Attribute{ 1789 "id": {Type: cty.String, Required: true}, 1790 "tuple_field": {Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.String, cty.String, cty.String}), Optional: true}, 1791 }, 1792 }, 1793 RequiredReplace: cty.NewPathSet(), 1794 ExpectedOutput: ` # test_instance.example will be updated in-place 1795 ~ resource "test_instance" "example" { 1796 id = "i-02ae66f368e8518a9" 1797 ~ tuple_field = [ 1798 # (1 unchanged element hidden) 1799 "bbbb", 1800 - "dddd", 1801 + "cccc", 1802 "eeee", 1803 # (1 unchanged element hidden) 1804 ] 1805 } 1806 `, 1807 }, 1808 } 1809 runTestCases(t, testCases) 1810 } 1811 1812 func TestResourceChange_primitiveSet(t *testing.T) { 1813 testCases := map[string]testCase{ 1814 "in-place update - creation": { 1815 Action: plans.Update, 1816 Mode: addrs.ManagedResourceMode, 1817 Before: cty.ObjectVal(map[string]cty.Value{ 1818 "id": cty.StringVal("i-02ae66f368e8518a9"), 1819 "ami": cty.StringVal("ami-STATIC"), 1820 "set_field": cty.NullVal(cty.Set(cty.String)), 1821 }), 1822 After: cty.ObjectVal(map[string]cty.Value{ 1823 "id": cty.UnknownVal(cty.String), 1824 "ami": cty.StringVal("ami-STATIC"), 1825 "set_field": cty.SetVal([]cty.Value{ 1826 cty.StringVal("new-element"), 1827 }), 1828 }), 1829 Schema: &configschema.Block{ 1830 Attributes: map[string]*configschema.Attribute{ 1831 "id": {Type: cty.String, Optional: true, Computed: true}, 1832 "ami": {Type: cty.String, Optional: true}, 1833 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1834 }, 1835 }, 1836 RequiredReplace: cty.NewPathSet(), 1837 ExpectedOutput: ` # test_instance.example will be updated in-place 1838 ~ resource "test_instance" "example" { 1839 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1840 + set_field = [ 1841 + "new-element", 1842 ] 1843 # (1 unchanged attribute hidden) 1844 } 1845 `, 1846 }, 1847 "in-place update - first insertion": { 1848 Action: plans.Update, 1849 Mode: addrs.ManagedResourceMode, 1850 Before: cty.ObjectVal(map[string]cty.Value{ 1851 "id": cty.StringVal("i-02ae66f368e8518a9"), 1852 "ami": cty.StringVal("ami-STATIC"), 1853 "set_field": cty.SetValEmpty(cty.String), 1854 }), 1855 After: cty.ObjectVal(map[string]cty.Value{ 1856 "id": cty.UnknownVal(cty.String), 1857 "ami": cty.StringVal("ami-STATIC"), 1858 "set_field": cty.SetVal([]cty.Value{ 1859 cty.StringVal("new-element"), 1860 }), 1861 }), 1862 Schema: &configschema.Block{ 1863 Attributes: map[string]*configschema.Attribute{ 1864 "id": {Type: cty.String, Optional: true, Computed: true}, 1865 "ami": {Type: cty.String, Optional: true}, 1866 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1867 }, 1868 }, 1869 RequiredReplace: cty.NewPathSet(), 1870 ExpectedOutput: ` # test_instance.example will be updated in-place 1871 ~ resource "test_instance" "example" { 1872 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1873 ~ set_field = [ 1874 + "new-element", 1875 ] 1876 # (1 unchanged attribute hidden) 1877 } 1878 `, 1879 }, 1880 "in-place update - insertion": { 1881 Action: plans.Update, 1882 Mode: addrs.ManagedResourceMode, 1883 Before: cty.ObjectVal(map[string]cty.Value{ 1884 "id": cty.StringVal("i-02ae66f368e8518a9"), 1885 "ami": cty.StringVal("ami-STATIC"), 1886 "set_field": cty.SetVal([]cty.Value{ 1887 cty.StringVal("aaaa"), 1888 cty.StringVal("cccc"), 1889 }), 1890 }), 1891 After: cty.ObjectVal(map[string]cty.Value{ 1892 "id": cty.UnknownVal(cty.String), 1893 "ami": cty.StringVal("ami-STATIC"), 1894 "set_field": cty.SetVal([]cty.Value{ 1895 cty.StringVal("aaaa"), 1896 cty.StringVal("bbbb"), 1897 cty.StringVal("cccc"), 1898 }), 1899 }), 1900 Schema: &configschema.Block{ 1901 Attributes: map[string]*configschema.Attribute{ 1902 "id": {Type: cty.String, Optional: true, Computed: true}, 1903 "ami": {Type: cty.String, Optional: true}, 1904 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1905 }, 1906 }, 1907 RequiredReplace: cty.NewPathSet(), 1908 ExpectedOutput: ` # test_instance.example will be updated in-place 1909 ~ resource "test_instance" "example" { 1910 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1911 ~ set_field = [ 1912 + "bbbb", 1913 # (2 unchanged elements hidden) 1914 ] 1915 # (1 unchanged attribute hidden) 1916 } 1917 `, 1918 }, 1919 "force-new update - insertion": { 1920 Action: plans.DeleteThenCreate, 1921 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 1922 Mode: addrs.ManagedResourceMode, 1923 Before: cty.ObjectVal(map[string]cty.Value{ 1924 "id": cty.StringVal("i-02ae66f368e8518a9"), 1925 "ami": cty.StringVal("ami-STATIC"), 1926 "set_field": cty.SetVal([]cty.Value{ 1927 cty.StringVal("aaaa"), 1928 cty.StringVal("cccc"), 1929 }), 1930 }), 1931 After: cty.ObjectVal(map[string]cty.Value{ 1932 "id": cty.UnknownVal(cty.String), 1933 "ami": cty.StringVal("ami-STATIC"), 1934 "set_field": cty.SetVal([]cty.Value{ 1935 cty.StringVal("aaaa"), 1936 cty.StringVal("bbbb"), 1937 cty.StringVal("cccc"), 1938 }), 1939 }), 1940 Schema: &configschema.Block{ 1941 Attributes: map[string]*configschema.Attribute{ 1942 "id": {Type: cty.String, Optional: true, Computed: true}, 1943 "ami": {Type: cty.String, Optional: true}, 1944 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1945 }, 1946 }, 1947 RequiredReplace: cty.NewPathSet(cty.Path{ 1948 cty.GetAttrStep{Name: "set_field"}, 1949 }), 1950 ExpectedOutput: ` # test_instance.example must be replaced 1951 -/+ resource "test_instance" "example" { 1952 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1953 ~ set_field = [ # forces replacement 1954 + "bbbb", 1955 # (2 unchanged elements hidden) 1956 ] 1957 # (1 unchanged attribute hidden) 1958 } 1959 `, 1960 }, 1961 "in-place update - deletion": { 1962 Action: plans.Update, 1963 Mode: addrs.ManagedResourceMode, 1964 Before: cty.ObjectVal(map[string]cty.Value{ 1965 "id": cty.StringVal("i-02ae66f368e8518a9"), 1966 "ami": cty.StringVal("ami-STATIC"), 1967 "set_field": cty.SetVal([]cty.Value{ 1968 cty.StringVal("aaaa"), 1969 cty.StringVal("bbbb"), 1970 cty.StringVal("cccc"), 1971 }), 1972 }), 1973 After: cty.ObjectVal(map[string]cty.Value{ 1974 "id": cty.UnknownVal(cty.String), 1975 "ami": cty.StringVal("ami-STATIC"), 1976 "set_field": cty.SetVal([]cty.Value{ 1977 cty.StringVal("bbbb"), 1978 }), 1979 }), 1980 Schema: &configschema.Block{ 1981 Attributes: map[string]*configschema.Attribute{ 1982 "id": {Type: cty.String, Optional: true, Computed: true}, 1983 "ami": {Type: cty.String, Optional: true}, 1984 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1985 }, 1986 }, 1987 RequiredReplace: cty.NewPathSet(), 1988 ExpectedOutput: ` # test_instance.example will be updated in-place 1989 ~ resource "test_instance" "example" { 1990 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1991 ~ set_field = [ 1992 - "aaaa", 1993 - "cccc", 1994 # (1 unchanged element hidden) 1995 ] 1996 # (1 unchanged attribute hidden) 1997 } 1998 `, 1999 }, 2000 "creation - empty set": { 2001 Action: plans.Create, 2002 Mode: addrs.ManagedResourceMode, 2003 Before: cty.NullVal(cty.EmptyObject), 2004 After: cty.ObjectVal(map[string]cty.Value{ 2005 "id": cty.UnknownVal(cty.String), 2006 "ami": cty.StringVal("ami-STATIC"), 2007 "set_field": cty.SetValEmpty(cty.String), 2008 }), 2009 Schema: &configschema.Block{ 2010 Attributes: map[string]*configschema.Attribute{ 2011 "id": {Type: cty.String, Optional: true, Computed: true}, 2012 "ami": {Type: cty.String, Optional: true}, 2013 "set_field": {Type: cty.Set(cty.String), Optional: true}, 2014 }, 2015 }, 2016 RequiredReplace: cty.NewPathSet(), 2017 ExpectedOutput: ` # test_instance.example will be created 2018 + resource "test_instance" "example" { 2019 + ami = "ami-STATIC" 2020 + id = (known after apply) 2021 + set_field = [] 2022 } 2023 `, 2024 }, 2025 "in-place update - full to empty set": { 2026 Action: plans.Update, 2027 Mode: addrs.ManagedResourceMode, 2028 Before: cty.ObjectVal(map[string]cty.Value{ 2029 "id": cty.StringVal("i-02ae66f368e8518a9"), 2030 "ami": cty.StringVal("ami-STATIC"), 2031 "set_field": cty.SetVal([]cty.Value{ 2032 cty.StringVal("aaaa"), 2033 cty.StringVal("bbbb"), 2034 }), 2035 }), 2036 After: cty.ObjectVal(map[string]cty.Value{ 2037 "id": cty.UnknownVal(cty.String), 2038 "ami": cty.StringVal("ami-STATIC"), 2039 "set_field": cty.SetValEmpty(cty.String), 2040 }), 2041 Schema: &configschema.Block{ 2042 Attributes: map[string]*configschema.Attribute{ 2043 "id": {Type: cty.String, Optional: true, Computed: true}, 2044 "ami": {Type: cty.String, Optional: true}, 2045 "set_field": {Type: cty.Set(cty.String), Optional: true}, 2046 }, 2047 }, 2048 RequiredReplace: cty.NewPathSet(), 2049 ExpectedOutput: ` # test_instance.example will be updated in-place 2050 ~ resource "test_instance" "example" { 2051 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2052 ~ set_field = [ 2053 - "aaaa", 2054 - "bbbb", 2055 ] 2056 # (1 unchanged attribute hidden) 2057 } 2058 `, 2059 }, 2060 "in-place update - null to empty set": { 2061 Action: plans.Update, 2062 Mode: addrs.ManagedResourceMode, 2063 Before: cty.ObjectVal(map[string]cty.Value{ 2064 "id": cty.StringVal("i-02ae66f368e8518a9"), 2065 "ami": cty.StringVal("ami-STATIC"), 2066 "set_field": cty.NullVal(cty.Set(cty.String)), 2067 }), 2068 After: cty.ObjectVal(map[string]cty.Value{ 2069 "id": cty.UnknownVal(cty.String), 2070 "ami": cty.StringVal("ami-STATIC"), 2071 "set_field": cty.SetValEmpty(cty.String), 2072 }), 2073 Schema: &configschema.Block{ 2074 Attributes: map[string]*configschema.Attribute{ 2075 "id": {Type: cty.String, Optional: true, Computed: true}, 2076 "ami": {Type: cty.String, Optional: true}, 2077 "set_field": {Type: cty.Set(cty.String), Optional: true}, 2078 }, 2079 }, 2080 RequiredReplace: cty.NewPathSet(), 2081 ExpectedOutput: ` # test_instance.example will be updated in-place 2082 ~ resource "test_instance" "example" { 2083 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2084 + set_field = [] 2085 # (1 unchanged attribute hidden) 2086 } 2087 `, 2088 }, 2089 "in-place update to unknown": { 2090 Action: plans.Update, 2091 Mode: addrs.ManagedResourceMode, 2092 Before: cty.ObjectVal(map[string]cty.Value{ 2093 "id": cty.StringVal("i-02ae66f368e8518a9"), 2094 "ami": cty.StringVal("ami-STATIC"), 2095 "set_field": cty.SetVal([]cty.Value{ 2096 cty.StringVal("aaaa"), 2097 cty.StringVal("bbbb"), 2098 }), 2099 }), 2100 After: cty.ObjectVal(map[string]cty.Value{ 2101 "id": cty.UnknownVal(cty.String), 2102 "ami": cty.StringVal("ami-STATIC"), 2103 "set_field": cty.UnknownVal(cty.Set(cty.String)), 2104 }), 2105 Schema: &configschema.Block{ 2106 Attributes: map[string]*configschema.Attribute{ 2107 "id": {Type: cty.String, Optional: true, Computed: true}, 2108 "ami": {Type: cty.String, Optional: true}, 2109 "set_field": {Type: cty.Set(cty.String), Optional: true}, 2110 }, 2111 }, 2112 RequiredReplace: cty.NewPathSet(), 2113 ExpectedOutput: ` # test_instance.example will be updated in-place 2114 ~ resource "test_instance" "example" { 2115 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2116 ~ set_field = [ 2117 - "aaaa", 2118 - "bbbb", 2119 ] -> (known after apply) 2120 # (1 unchanged attribute hidden) 2121 } 2122 `, 2123 }, 2124 "in-place update to unknown element": { 2125 Action: plans.Update, 2126 Mode: addrs.ManagedResourceMode, 2127 Before: cty.ObjectVal(map[string]cty.Value{ 2128 "id": cty.StringVal("i-02ae66f368e8518a9"), 2129 "ami": cty.StringVal("ami-STATIC"), 2130 "set_field": cty.SetVal([]cty.Value{ 2131 cty.StringVal("aaaa"), 2132 cty.StringVal("bbbb"), 2133 }), 2134 }), 2135 After: cty.ObjectVal(map[string]cty.Value{ 2136 "id": cty.UnknownVal(cty.String), 2137 "ami": cty.StringVal("ami-STATIC"), 2138 "set_field": cty.SetVal([]cty.Value{ 2139 cty.StringVal("aaaa"), 2140 cty.UnknownVal(cty.String), 2141 }), 2142 }), 2143 Schema: &configschema.Block{ 2144 Attributes: map[string]*configschema.Attribute{ 2145 "id": {Type: cty.String, Optional: true, Computed: true}, 2146 "ami": {Type: cty.String, Optional: true}, 2147 "set_field": {Type: cty.Set(cty.String), Optional: true}, 2148 }, 2149 }, 2150 RequiredReplace: cty.NewPathSet(), 2151 ExpectedOutput: ` # test_instance.example will be updated in-place 2152 ~ resource "test_instance" "example" { 2153 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2154 ~ set_field = [ 2155 - "bbbb", 2156 ~ (known after apply), 2157 # (1 unchanged element hidden) 2158 ] 2159 # (1 unchanged attribute hidden) 2160 } 2161 `, 2162 }, 2163 } 2164 runTestCases(t, testCases) 2165 } 2166 2167 func TestResourceChange_map(t *testing.T) { 2168 testCases := map[string]testCase{ 2169 "in-place update - creation": { 2170 Action: plans.Update, 2171 Mode: addrs.ManagedResourceMode, 2172 Before: cty.ObjectVal(map[string]cty.Value{ 2173 "id": cty.StringVal("i-02ae66f368e8518a9"), 2174 "ami": cty.StringVal("ami-STATIC"), 2175 "map_field": cty.NullVal(cty.Map(cty.String)), 2176 }), 2177 After: cty.ObjectVal(map[string]cty.Value{ 2178 "id": cty.UnknownVal(cty.String), 2179 "ami": cty.StringVal("ami-STATIC"), 2180 "map_field": cty.MapVal(map[string]cty.Value{ 2181 "new-key": cty.StringVal("new-element"), 2182 "be:ep": cty.StringVal("boop"), 2183 }), 2184 }), 2185 Schema: &configschema.Block{ 2186 Attributes: map[string]*configschema.Attribute{ 2187 "id": {Type: cty.String, Optional: true, Computed: true}, 2188 "ami": {Type: cty.String, Optional: true}, 2189 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2190 }, 2191 }, 2192 RequiredReplace: cty.NewPathSet(), 2193 ExpectedOutput: ` # test_instance.example will be updated in-place 2194 ~ resource "test_instance" "example" { 2195 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2196 + map_field = { 2197 + "be:ep" = "boop" 2198 + "new-key" = "new-element" 2199 } 2200 # (1 unchanged attribute hidden) 2201 } 2202 `, 2203 }, 2204 "in-place update - first insertion": { 2205 Action: plans.Update, 2206 Mode: addrs.ManagedResourceMode, 2207 Before: cty.ObjectVal(map[string]cty.Value{ 2208 "id": cty.StringVal("i-02ae66f368e8518a9"), 2209 "ami": cty.StringVal("ami-STATIC"), 2210 "map_field": cty.MapValEmpty(cty.String), 2211 }), 2212 After: cty.ObjectVal(map[string]cty.Value{ 2213 "id": cty.UnknownVal(cty.String), 2214 "ami": cty.StringVal("ami-STATIC"), 2215 "map_field": cty.MapVal(map[string]cty.Value{ 2216 "new-key": cty.StringVal("new-element"), 2217 "be:ep": cty.StringVal("boop"), 2218 }), 2219 }), 2220 Schema: &configschema.Block{ 2221 Attributes: map[string]*configschema.Attribute{ 2222 "id": {Type: cty.String, Optional: true, Computed: true}, 2223 "ami": {Type: cty.String, Optional: true}, 2224 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2225 }, 2226 }, 2227 RequiredReplace: cty.NewPathSet(), 2228 ExpectedOutput: ` # test_instance.example will be updated in-place 2229 ~ resource "test_instance" "example" { 2230 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2231 ~ map_field = { 2232 + "be:ep" = "boop" 2233 + "new-key" = "new-element" 2234 } 2235 # (1 unchanged attribute hidden) 2236 } 2237 `, 2238 }, 2239 "in-place update - insertion": { 2240 Action: plans.Update, 2241 Mode: addrs.ManagedResourceMode, 2242 Before: cty.ObjectVal(map[string]cty.Value{ 2243 "id": cty.StringVal("i-02ae66f368e8518a9"), 2244 "ami": cty.StringVal("ami-STATIC"), 2245 "map_field": cty.MapVal(map[string]cty.Value{ 2246 "a": cty.StringVal("aaaa"), 2247 "c": cty.StringVal("cccc"), 2248 }), 2249 }), 2250 After: cty.ObjectVal(map[string]cty.Value{ 2251 "id": cty.UnknownVal(cty.String), 2252 "ami": cty.StringVal("ami-STATIC"), 2253 "map_field": cty.MapVal(map[string]cty.Value{ 2254 "a": cty.StringVal("aaaa"), 2255 "b": cty.StringVal("bbbb"), 2256 "b:b": cty.StringVal("bbbb"), 2257 "c": cty.StringVal("cccc"), 2258 }), 2259 }), 2260 Schema: &configschema.Block{ 2261 Attributes: map[string]*configschema.Attribute{ 2262 "id": {Type: cty.String, Optional: true, Computed: true}, 2263 "ami": {Type: cty.String, Optional: true}, 2264 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2265 }, 2266 }, 2267 RequiredReplace: cty.NewPathSet(), 2268 ExpectedOutput: ` # test_instance.example will be updated in-place 2269 ~ resource "test_instance" "example" { 2270 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2271 ~ map_field = { 2272 + "b" = "bbbb" 2273 + "b:b" = "bbbb" 2274 # (2 unchanged elements hidden) 2275 } 2276 # (1 unchanged attribute hidden) 2277 } 2278 `, 2279 }, 2280 "force-new update - insertion": { 2281 Action: plans.DeleteThenCreate, 2282 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 2283 Mode: addrs.ManagedResourceMode, 2284 Before: cty.ObjectVal(map[string]cty.Value{ 2285 "id": cty.StringVal("i-02ae66f368e8518a9"), 2286 "ami": cty.StringVal("ami-STATIC"), 2287 "map_field": cty.MapVal(map[string]cty.Value{ 2288 "a": cty.StringVal("aaaa"), 2289 "c": cty.StringVal("cccc"), 2290 }), 2291 }), 2292 After: cty.ObjectVal(map[string]cty.Value{ 2293 "id": cty.UnknownVal(cty.String), 2294 "ami": cty.StringVal("ami-STATIC"), 2295 "map_field": cty.MapVal(map[string]cty.Value{ 2296 "a": cty.StringVal("aaaa"), 2297 "b": cty.StringVal("bbbb"), 2298 "c": cty.StringVal("cccc"), 2299 }), 2300 }), 2301 Schema: &configschema.Block{ 2302 Attributes: map[string]*configschema.Attribute{ 2303 "id": {Type: cty.String, Optional: true, Computed: true}, 2304 "ami": {Type: cty.String, Optional: true}, 2305 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2306 }, 2307 }, 2308 RequiredReplace: cty.NewPathSet(cty.Path{ 2309 cty.GetAttrStep{Name: "map_field"}, 2310 }), 2311 ExpectedOutput: ` # test_instance.example must be replaced 2312 -/+ resource "test_instance" "example" { 2313 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2314 ~ map_field = { # forces replacement 2315 + "b" = "bbbb" 2316 # (2 unchanged elements hidden) 2317 } 2318 # (1 unchanged attribute hidden) 2319 } 2320 `, 2321 }, 2322 "in-place update - deletion": { 2323 Action: plans.Update, 2324 Mode: addrs.ManagedResourceMode, 2325 Before: cty.ObjectVal(map[string]cty.Value{ 2326 "id": cty.StringVal("i-02ae66f368e8518a9"), 2327 "ami": cty.StringVal("ami-STATIC"), 2328 "map_field": cty.MapVal(map[string]cty.Value{ 2329 "a": cty.StringVal("aaaa"), 2330 "b": cty.StringVal("bbbb"), 2331 "c": cty.StringVal("cccc"), 2332 }), 2333 }), 2334 After: cty.ObjectVal(map[string]cty.Value{ 2335 "id": cty.UnknownVal(cty.String), 2336 "ami": cty.StringVal("ami-STATIC"), 2337 "map_field": cty.MapVal(map[string]cty.Value{ 2338 "b": cty.StringVal("bbbb"), 2339 }), 2340 }), 2341 Schema: &configschema.Block{ 2342 Attributes: map[string]*configschema.Attribute{ 2343 "id": {Type: cty.String, Optional: true, Computed: true}, 2344 "ami": {Type: cty.String, Optional: true}, 2345 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2346 }, 2347 }, 2348 RequiredReplace: cty.NewPathSet(), 2349 ExpectedOutput: ` # test_instance.example will be updated in-place 2350 ~ resource "test_instance" "example" { 2351 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2352 ~ map_field = { 2353 - "a" = "aaaa" -> null 2354 - "c" = "cccc" -> null 2355 # (1 unchanged element hidden) 2356 } 2357 # (1 unchanged attribute hidden) 2358 } 2359 `, 2360 }, 2361 "creation - empty": { 2362 Action: plans.Create, 2363 Mode: addrs.ManagedResourceMode, 2364 Before: cty.NullVal(cty.EmptyObject), 2365 After: cty.ObjectVal(map[string]cty.Value{ 2366 "id": cty.UnknownVal(cty.String), 2367 "ami": cty.StringVal("ami-STATIC"), 2368 "map_field": cty.MapValEmpty(cty.String), 2369 }), 2370 Schema: &configschema.Block{ 2371 Attributes: map[string]*configschema.Attribute{ 2372 "id": {Type: cty.String, Optional: true, Computed: true}, 2373 "ami": {Type: cty.String, Optional: true}, 2374 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2375 }, 2376 }, 2377 RequiredReplace: cty.NewPathSet(), 2378 ExpectedOutput: ` # test_instance.example will be created 2379 + resource "test_instance" "example" { 2380 + ami = "ami-STATIC" 2381 + id = (known after apply) 2382 + map_field = {} 2383 } 2384 `, 2385 }, 2386 "update to unknown element": { 2387 Action: plans.Update, 2388 Mode: addrs.ManagedResourceMode, 2389 Before: cty.ObjectVal(map[string]cty.Value{ 2390 "id": cty.StringVal("i-02ae66f368e8518a9"), 2391 "ami": cty.StringVal("ami-STATIC"), 2392 "map_field": cty.MapVal(map[string]cty.Value{ 2393 "a": cty.StringVal("aaaa"), 2394 "b": cty.StringVal("bbbb"), 2395 "c": cty.StringVal("cccc"), 2396 }), 2397 }), 2398 After: cty.ObjectVal(map[string]cty.Value{ 2399 "id": cty.UnknownVal(cty.String), 2400 "ami": cty.StringVal("ami-STATIC"), 2401 "map_field": cty.MapVal(map[string]cty.Value{ 2402 "a": cty.StringVal("aaaa"), 2403 "b": cty.UnknownVal(cty.String), 2404 "c": cty.StringVal("cccc"), 2405 }), 2406 }), 2407 Schema: &configschema.Block{ 2408 Attributes: map[string]*configschema.Attribute{ 2409 "id": {Type: cty.String, Optional: true, Computed: true}, 2410 "ami": {Type: cty.String, Optional: true}, 2411 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2412 }, 2413 }, 2414 RequiredReplace: cty.NewPathSet(), 2415 ExpectedOutput: ` # test_instance.example will be updated in-place 2416 ~ resource "test_instance" "example" { 2417 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2418 ~ map_field = { 2419 ~ "b" = "bbbb" -> (known after apply) 2420 # (2 unchanged elements hidden) 2421 } 2422 # (1 unchanged attribute hidden) 2423 } 2424 `, 2425 }, 2426 } 2427 runTestCases(t, testCases) 2428 } 2429 2430 func TestResourceChange_nestedList(t *testing.T) { 2431 testCases := map[string]testCase{ 2432 "in-place update - equal": { 2433 Action: plans.Update, 2434 Mode: addrs.ManagedResourceMode, 2435 Before: cty.ObjectVal(map[string]cty.Value{ 2436 "id": cty.StringVal("i-02ae66f368e8518a9"), 2437 "ami": cty.StringVal("ami-BEFORE"), 2438 "root_block_device": cty.ListVal([]cty.Value{ 2439 cty.ObjectVal(map[string]cty.Value{ 2440 "volume_type": cty.StringVal("gp2"), 2441 }), 2442 }), 2443 "disks": cty.ListVal([]cty.Value{ 2444 cty.ObjectVal(map[string]cty.Value{ 2445 "mount_point": cty.StringVal("/var/diska"), 2446 "size": cty.StringVal("50GB"), 2447 }), 2448 }), 2449 }), 2450 After: cty.ObjectVal(map[string]cty.Value{ 2451 "id": cty.StringVal("i-02ae66f368e8518a9"), 2452 "ami": cty.StringVal("ami-AFTER"), 2453 "root_block_device": cty.ListVal([]cty.Value{ 2454 cty.ObjectVal(map[string]cty.Value{ 2455 "volume_type": cty.StringVal("gp2"), 2456 }), 2457 }), 2458 "disks": cty.ListVal([]cty.Value{ 2459 cty.ObjectVal(map[string]cty.Value{ 2460 "mount_point": cty.StringVal("/var/diska"), 2461 "size": cty.StringVal("50GB"), 2462 }), 2463 }), 2464 }), 2465 RequiredReplace: cty.NewPathSet(), 2466 Schema: testSchema(configschema.NestingList), 2467 ExpectedOutput: ` # test_instance.example will be updated in-place 2468 ~ resource "test_instance" "example" { 2469 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2470 id = "i-02ae66f368e8518a9" 2471 # (1 unchanged attribute hidden) 2472 2473 # (1 unchanged block hidden) 2474 } 2475 `, 2476 }, 2477 "in-place update - creation": { 2478 Action: plans.Update, 2479 Mode: addrs.ManagedResourceMode, 2480 Before: cty.ObjectVal(map[string]cty.Value{ 2481 "id": cty.StringVal("i-02ae66f368e8518a9"), 2482 "ami": cty.StringVal("ami-BEFORE"), 2483 "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2484 "volume_type": cty.String, 2485 })), 2486 "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2487 "mount_point": cty.String, 2488 "size": cty.String, 2489 })), 2490 }), 2491 After: cty.ObjectVal(map[string]cty.Value{ 2492 "id": cty.StringVal("i-02ae66f368e8518a9"), 2493 "ami": cty.StringVal("ami-AFTER"), 2494 "disks": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 2495 "mount_point": cty.StringVal("/var/diska"), 2496 "size": cty.StringVal("50GB"), 2497 })}), 2498 "root_block_device": cty.ListVal([]cty.Value{ 2499 cty.ObjectVal(map[string]cty.Value{ 2500 "volume_type": cty.NullVal(cty.String), 2501 }), 2502 }), 2503 }), 2504 RequiredReplace: cty.NewPathSet(), 2505 Schema: testSchema(configschema.NestingList), 2506 ExpectedOutput: ` # test_instance.example will be updated in-place 2507 ~ resource "test_instance" "example" { 2508 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2509 ~ disks = [ 2510 + { 2511 + mount_point = "/var/diska" 2512 + size = "50GB" 2513 }, 2514 ] 2515 id = "i-02ae66f368e8518a9" 2516 2517 + root_block_device {} 2518 } 2519 `, 2520 }, 2521 "in-place update - first insertion": { 2522 Action: plans.Update, 2523 Mode: addrs.ManagedResourceMode, 2524 Before: cty.ObjectVal(map[string]cty.Value{ 2525 "id": cty.StringVal("i-02ae66f368e8518a9"), 2526 "ami": cty.StringVal("ami-BEFORE"), 2527 "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2528 "volume_type": cty.String, 2529 })), 2530 "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2531 "mount_point": cty.String, 2532 "size": cty.String, 2533 })), 2534 }), 2535 After: cty.ObjectVal(map[string]cty.Value{ 2536 "id": cty.StringVal("i-02ae66f368e8518a9"), 2537 "ami": cty.StringVal("ami-AFTER"), 2538 "disks": cty.ListVal([]cty.Value{ 2539 cty.ObjectVal(map[string]cty.Value{ 2540 "mount_point": cty.StringVal("/var/diska"), 2541 "size": cty.NullVal(cty.String), 2542 }), 2543 }), 2544 "root_block_device": cty.ListVal([]cty.Value{ 2545 cty.ObjectVal(map[string]cty.Value{ 2546 "volume_type": cty.StringVal("gp2"), 2547 }), 2548 }), 2549 }), 2550 RequiredReplace: cty.NewPathSet(), 2551 Schema: testSchema(configschema.NestingList), 2552 ExpectedOutput: ` # test_instance.example will be updated in-place 2553 ~ resource "test_instance" "example" { 2554 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2555 ~ disks = [ 2556 + { 2557 + mount_point = "/var/diska" 2558 }, 2559 ] 2560 id = "i-02ae66f368e8518a9" 2561 2562 + root_block_device { 2563 + volume_type = "gp2" 2564 } 2565 } 2566 `, 2567 }, 2568 "in-place update - insertion": { 2569 Action: plans.Update, 2570 Mode: addrs.ManagedResourceMode, 2571 Before: cty.ObjectVal(map[string]cty.Value{ 2572 "id": cty.StringVal("i-02ae66f368e8518a9"), 2573 "ami": cty.StringVal("ami-BEFORE"), 2574 "disks": cty.ListVal([]cty.Value{ 2575 cty.ObjectVal(map[string]cty.Value{ 2576 "mount_point": cty.StringVal("/var/diska"), 2577 "size": cty.NullVal(cty.String), 2578 }), 2579 cty.ObjectVal(map[string]cty.Value{ 2580 "mount_point": cty.StringVal("/var/diskb"), 2581 "size": cty.StringVal("50GB"), 2582 }), 2583 }), 2584 "root_block_device": cty.ListVal([]cty.Value{ 2585 cty.ObjectVal(map[string]cty.Value{ 2586 "volume_type": cty.StringVal("gp2"), 2587 "new_field": cty.NullVal(cty.String), 2588 }), 2589 }), 2590 }), 2591 After: cty.ObjectVal(map[string]cty.Value{ 2592 "id": cty.StringVal("i-02ae66f368e8518a9"), 2593 "ami": cty.StringVal("ami-AFTER"), 2594 "disks": cty.ListVal([]cty.Value{ 2595 cty.ObjectVal(map[string]cty.Value{ 2596 "mount_point": cty.StringVal("/var/diska"), 2597 "size": cty.StringVal("50GB"), 2598 }), 2599 cty.ObjectVal(map[string]cty.Value{ 2600 "mount_point": cty.StringVal("/var/diskb"), 2601 "size": cty.StringVal("50GB"), 2602 }), 2603 }), 2604 "root_block_device": cty.ListVal([]cty.Value{ 2605 cty.ObjectVal(map[string]cty.Value{ 2606 "volume_type": cty.StringVal("gp2"), 2607 "new_field": cty.StringVal("new_value"), 2608 }), 2609 }), 2610 }), 2611 RequiredReplace: cty.NewPathSet(), 2612 Schema: testSchemaPlus(configschema.NestingList), 2613 ExpectedOutput: ` # test_instance.example will be updated in-place 2614 ~ resource "test_instance" "example" { 2615 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2616 ~ disks = [ 2617 ~ { 2618 + size = "50GB" 2619 # (1 unchanged attribute hidden) 2620 }, 2621 # (1 unchanged element hidden) 2622 ] 2623 id = "i-02ae66f368e8518a9" 2624 2625 ~ root_block_device { 2626 + new_field = "new_value" 2627 # (1 unchanged attribute hidden) 2628 } 2629 } 2630 `, 2631 }, 2632 "force-new update (inside blocks)": { 2633 Action: plans.DeleteThenCreate, 2634 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 2635 Mode: addrs.ManagedResourceMode, 2636 Before: cty.ObjectVal(map[string]cty.Value{ 2637 "id": cty.StringVal("i-02ae66f368e8518a9"), 2638 "ami": cty.StringVal("ami-BEFORE"), 2639 "disks": cty.ListVal([]cty.Value{ 2640 cty.ObjectVal(map[string]cty.Value{ 2641 "mount_point": cty.StringVal("/var/diska"), 2642 "size": cty.StringVal("50GB"), 2643 }), 2644 }), 2645 "root_block_device": cty.ListVal([]cty.Value{ 2646 cty.ObjectVal(map[string]cty.Value{ 2647 "volume_type": cty.StringVal("gp2"), 2648 }), 2649 }), 2650 }), 2651 After: cty.ObjectVal(map[string]cty.Value{ 2652 "id": cty.StringVal("i-02ae66f368e8518a9"), 2653 "ami": cty.StringVal("ami-AFTER"), 2654 "disks": cty.ListVal([]cty.Value{ 2655 cty.ObjectVal(map[string]cty.Value{ 2656 "mount_point": cty.StringVal("/var/diskb"), 2657 "size": cty.StringVal("50GB"), 2658 }), 2659 }), 2660 "root_block_device": cty.ListVal([]cty.Value{ 2661 cty.ObjectVal(map[string]cty.Value{ 2662 "volume_type": cty.StringVal("different"), 2663 }), 2664 }), 2665 }), 2666 RequiredReplace: cty.NewPathSet( 2667 cty.Path{ 2668 cty.GetAttrStep{Name: "root_block_device"}, 2669 cty.IndexStep{Key: cty.NumberIntVal(0)}, 2670 cty.GetAttrStep{Name: "volume_type"}, 2671 }, 2672 cty.Path{ 2673 cty.GetAttrStep{Name: "disks"}, 2674 cty.IndexStep{Key: cty.NumberIntVal(0)}, 2675 cty.GetAttrStep{Name: "mount_point"}, 2676 }, 2677 ), 2678 Schema: testSchema(configschema.NestingList), 2679 ExpectedOutput: ` # test_instance.example must be replaced 2680 -/+ resource "test_instance" "example" { 2681 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2682 ~ disks = [ 2683 ~ { 2684 ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement 2685 # (1 unchanged attribute hidden) 2686 }, 2687 ] 2688 id = "i-02ae66f368e8518a9" 2689 2690 ~ root_block_device { 2691 ~ volume_type = "gp2" -> "different" # forces replacement 2692 } 2693 } 2694 `, 2695 }, 2696 "force-new update (whole block)": { 2697 Action: plans.DeleteThenCreate, 2698 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 2699 Mode: addrs.ManagedResourceMode, 2700 Before: cty.ObjectVal(map[string]cty.Value{ 2701 "id": cty.StringVal("i-02ae66f368e8518a9"), 2702 "ami": cty.StringVal("ami-BEFORE"), 2703 "disks": cty.ListVal([]cty.Value{ 2704 cty.ObjectVal(map[string]cty.Value{ 2705 "mount_point": cty.StringVal("/var/diska"), 2706 "size": cty.StringVal("50GB"), 2707 }), 2708 }), 2709 "root_block_device": cty.ListVal([]cty.Value{ 2710 cty.ObjectVal(map[string]cty.Value{ 2711 "volume_type": cty.StringVal("gp2"), 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/diskb"), 2721 "size": cty.StringVal("50GB"), 2722 }), 2723 }), 2724 "root_block_device": cty.ListVal([]cty.Value{ 2725 cty.ObjectVal(map[string]cty.Value{ 2726 "volume_type": cty.StringVal("different"), 2727 }), 2728 }), 2729 }), 2730 RequiredReplace: cty.NewPathSet( 2731 cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, 2732 cty.Path{cty.GetAttrStep{Name: "disks"}}, 2733 ), 2734 Schema: testSchema(configschema.NestingList), 2735 ExpectedOutput: ` # test_instance.example must be replaced 2736 -/+ resource "test_instance" "example" { 2737 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2738 ~ disks = [ # forces replacement 2739 ~ { 2740 ~ mount_point = "/var/diska" -> "/var/diskb" 2741 # (1 unchanged attribute hidden) 2742 }, 2743 ] 2744 id = "i-02ae66f368e8518a9" 2745 2746 ~ root_block_device { # forces replacement 2747 ~ volume_type = "gp2" -> "different" 2748 } 2749 } 2750 `, 2751 }, 2752 "in-place update - deletion": { 2753 Action: plans.Update, 2754 Mode: addrs.ManagedResourceMode, 2755 Before: cty.ObjectVal(map[string]cty.Value{ 2756 "id": cty.StringVal("i-02ae66f368e8518a9"), 2757 "ami": cty.StringVal("ami-BEFORE"), 2758 "disks": cty.ListVal([]cty.Value{ 2759 cty.ObjectVal(map[string]cty.Value{ 2760 "mount_point": cty.StringVal("/var/diska"), 2761 "size": cty.StringVal("50GB"), 2762 }), 2763 }), 2764 "root_block_device": cty.ListVal([]cty.Value{ 2765 cty.ObjectVal(map[string]cty.Value{ 2766 "volume_type": cty.StringVal("gp2"), 2767 }), 2768 }), 2769 }), 2770 After: cty.ObjectVal(map[string]cty.Value{ 2771 "id": cty.StringVal("i-02ae66f368e8518a9"), 2772 "ami": cty.StringVal("ami-AFTER"), 2773 "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2774 "mount_point": cty.String, 2775 "size": cty.String, 2776 })), 2777 "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2778 "volume_type": cty.String, 2779 })), 2780 }), 2781 RequiredReplace: cty.NewPathSet(), 2782 Schema: testSchema(configschema.NestingList), 2783 ExpectedOutput: ` # test_instance.example will be updated in-place 2784 ~ resource "test_instance" "example" { 2785 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2786 ~ disks = [ 2787 - { 2788 - mount_point = "/var/diska" -> null 2789 - size = "50GB" -> null 2790 }, 2791 ] 2792 id = "i-02ae66f368e8518a9" 2793 2794 - root_block_device { 2795 - volume_type = "gp2" -> null 2796 } 2797 } 2798 `, 2799 }, 2800 "with dynamically-typed attribute": { 2801 Action: plans.Update, 2802 Mode: addrs.ManagedResourceMode, 2803 Before: cty.ObjectVal(map[string]cty.Value{ 2804 "block": cty.EmptyTupleVal, 2805 }), 2806 After: cty.ObjectVal(map[string]cty.Value{ 2807 "block": cty.TupleVal([]cty.Value{ 2808 cty.ObjectVal(map[string]cty.Value{ 2809 "attr": cty.StringVal("foo"), 2810 }), 2811 cty.ObjectVal(map[string]cty.Value{ 2812 "attr": cty.True, 2813 }), 2814 }), 2815 }), 2816 RequiredReplace: cty.NewPathSet(), 2817 Schema: &configschema.Block{ 2818 BlockTypes: map[string]*configschema.NestedBlock{ 2819 "block": { 2820 Block: configschema.Block{ 2821 Attributes: map[string]*configschema.Attribute{ 2822 "attr": {Type: cty.DynamicPseudoType, Optional: true}, 2823 }, 2824 }, 2825 Nesting: configschema.NestingList, 2826 }, 2827 }, 2828 }, 2829 ExpectedOutput: ` # test_instance.example will be updated in-place 2830 ~ resource "test_instance" "example" { 2831 + block { 2832 + attr = "foo" 2833 } 2834 + block { 2835 + attr = true 2836 } 2837 } 2838 `, 2839 }, 2840 "in-place sequence update - deletion": { 2841 Action: plans.Update, 2842 Mode: addrs.ManagedResourceMode, 2843 Before: cty.ObjectVal(map[string]cty.Value{ 2844 "list": cty.ListVal([]cty.Value{ 2845 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("x")}), 2846 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}), 2847 }), 2848 }), 2849 After: cty.ObjectVal(map[string]cty.Value{ 2850 "list": cty.ListVal([]cty.Value{ 2851 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}), 2852 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("z")}), 2853 }), 2854 }), 2855 RequiredReplace: cty.NewPathSet(), 2856 Schema: &configschema.Block{ 2857 BlockTypes: map[string]*configschema.NestedBlock{ 2858 "list": { 2859 Block: configschema.Block{ 2860 Attributes: map[string]*configschema.Attribute{ 2861 "attr": { 2862 Type: cty.String, 2863 Required: true, 2864 }, 2865 }, 2866 }, 2867 Nesting: configschema.NestingList, 2868 }, 2869 }, 2870 }, 2871 ExpectedOutput: ` # test_instance.example will be updated in-place 2872 ~ resource "test_instance" "example" { 2873 ~ list { 2874 ~ attr = "x" -> "y" 2875 } 2876 ~ list { 2877 ~ attr = "y" -> "z" 2878 } 2879 } 2880 `, 2881 }, 2882 "in-place update - unknown": { 2883 Action: plans.Update, 2884 Mode: addrs.ManagedResourceMode, 2885 Before: cty.ObjectVal(map[string]cty.Value{ 2886 "id": cty.StringVal("i-02ae66f368e8518a9"), 2887 "ami": cty.StringVal("ami-BEFORE"), 2888 "disks": cty.ListVal([]cty.Value{ 2889 cty.ObjectVal(map[string]cty.Value{ 2890 "mount_point": cty.StringVal("/var/diska"), 2891 "size": cty.StringVal("50GB"), 2892 }), 2893 }), 2894 "root_block_device": cty.ListVal([]cty.Value{ 2895 cty.ObjectVal(map[string]cty.Value{ 2896 "volume_type": cty.StringVal("gp2"), 2897 "new_field": cty.StringVal("new_value"), 2898 }), 2899 }), 2900 }), 2901 After: cty.ObjectVal(map[string]cty.Value{ 2902 "id": cty.StringVal("i-02ae66f368e8518a9"), 2903 "ami": cty.StringVal("ami-AFTER"), 2904 "disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ 2905 "mount_point": cty.String, 2906 "size": cty.String, 2907 }))), 2908 "root_block_device": cty.ListVal([]cty.Value{ 2909 cty.ObjectVal(map[string]cty.Value{ 2910 "volume_type": cty.StringVal("gp2"), 2911 "new_field": cty.StringVal("new_value"), 2912 }), 2913 }), 2914 }), 2915 RequiredReplace: cty.NewPathSet(), 2916 Schema: testSchemaPlus(configschema.NestingList), 2917 ExpectedOutput: ` # test_instance.example will be updated in-place 2918 ~ resource "test_instance" "example" { 2919 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2920 ~ disks = [ 2921 - { 2922 - mount_point = "/var/diska" -> null 2923 - size = "50GB" -> null 2924 }, 2925 ] -> (known after apply) 2926 id = "i-02ae66f368e8518a9" 2927 2928 # (1 unchanged block hidden) 2929 } 2930 `, 2931 }, 2932 "in-place update - modification": { 2933 Action: plans.Update, 2934 Mode: addrs.ManagedResourceMode, 2935 Before: cty.ObjectVal(map[string]cty.Value{ 2936 "id": cty.StringVal("i-02ae66f368e8518a9"), 2937 "ami": cty.StringVal("ami-BEFORE"), 2938 "disks": cty.ListVal([]cty.Value{ 2939 cty.ObjectVal(map[string]cty.Value{ 2940 "mount_point": cty.StringVal("/var/diska"), 2941 "size": cty.StringVal("50GB"), 2942 }), 2943 cty.ObjectVal(map[string]cty.Value{ 2944 "mount_point": cty.StringVal("/var/diskb"), 2945 "size": cty.StringVal("50GB"), 2946 }), 2947 cty.ObjectVal(map[string]cty.Value{ 2948 "mount_point": cty.StringVal("/var/diskc"), 2949 "size": cty.StringVal("50GB"), 2950 }), 2951 }), 2952 "root_block_device": cty.ListVal([]cty.Value{ 2953 cty.ObjectVal(map[string]cty.Value{ 2954 "volume_type": cty.StringVal("gp2"), 2955 "new_field": cty.StringVal("new_value"), 2956 }), 2957 }), 2958 }), 2959 After: cty.ObjectVal(map[string]cty.Value{ 2960 "id": cty.StringVal("i-02ae66f368e8518a9"), 2961 "ami": cty.StringVal("ami-AFTER"), 2962 "disks": cty.ListVal([]cty.Value{ 2963 cty.ObjectVal(map[string]cty.Value{ 2964 "mount_point": cty.StringVal("/var/diska"), 2965 "size": cty.StringVal("50GB"), 2966 }), 2967 cty.ObjectVal(map[string]cty.Value{ 2968 "mount_point": cty.StringVal("/var/diskb"), 2969 "size": cty.StringVal("75GB"), 2970 }), 2971 cty.ObjectVal(map[string]cty.Value{ 2972 "mount_point": cty.StringVal("/var/diskc"), 2973 "size": cty.StringVal("25GB"), 2974 }), 2975 }), 2976 "root_block_device": cty.ListVal([]cty.Value{ 2977 cty.ObjectVal(map[string]cty.Value{ 2978 "volume_type": cty.StringVal("gp2"), 2979 "new_field": cty.StringVal("new_value"), 2980 }), 2981 }), 2982 }), 2983 RequiredReplace: cty.NewPathSet(), 2984 Schema: testSchemaPlus(configschema.NestingList), 2985 ExpectedOutput: ` # test_instance.example will be updated in-place 2986 ~ resource "test_instance" "example" { 2987 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2988 ~ disks = [ 2989 ~ { 2990 ~ size = "50GB" -> "75GB" 2991 # (1 unchanged attribute hidden) 2992 }, 2993 ~ { 2994 ~ size = "50GB" -> "25GB" 2995 # (1 unchanged attribute hidden) 2996 }, 2997 # (1 unchanged element hidden) 2998 ] 2999 id = "i-02ae66f368e8518a9" 3000 3001 # (1 unchanged block hidden) 3002 } 3003 `, 3004 }, 3005 } 3006 runTestCases(t, testCases) 3007 } 3008 3009 func TestResourceChange_nestedSet(t *testing.T) { 3010 testCases := map[string]testCase{ 3011 "creation from null - sensitive set": { 3012 Action: plans.Create, 3013 Mode: addrs.ManagedResourceMode, 3014 Before: cty.NullVal(cty.Object(map[string]cty.Type{ 3015 "id": cty.String, 3016 "ami": cty.String, 3017 "disks": cty.Set(cty.Object(map[string]cty.Type{ 3018 "mount_point": cty.String, 3019 "size": cty.String, 3020 })), 3021 "root_block_device": cty.Set(cty.Object(map[string]cty.Type{ 3022 "volume_type": cty.String, 3023 })), 3024 })), 3025 After: cty.ObjectVal(map[string]cty.Value{ 3026 "id": cty.StringVal("i-02ae66f368e8518a9"), 3027 "ami": cty.StringVal("ami-AFTER"), 3028 "disks": cty.SetVal([]cty.Value{ 3029 cty.ObjectVal(map[string]cty.Value{ 3030 "mount_point": cty.StringVal("/var/diska"), 3031 "size": cty.NullVal(cty.String), 3032 }), 3033 }), 3034 "root_block_device": cty.SetVal([]cty.Value{ 3035 cty.ObjectVal(map[string]cty.Value{ 3036 "volume_type": cty.StringVal("gp2"), 3037 }), 3038 }), 3039 }), 3040 AfterValMarks: []cty.PathValueMarks{ 3041 { 3042 Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, 3043 Marks: cty.NewValueMarks(marks.Sensitive), 3044 }, 3045 }, 3046 RequiredReplace: cty.NewPathSet(), 3047 Schema: testSchema(configschema.NestingSet), 3048 ExpectedOutput: ` # test_instance.example will be created 3049 + resource "test_instance" "example" { 3050 + ami = "ami-AFTER" 3051 + disks = (sensitive value) 3052 + id = "i-02ae66f368e8518a9" 3053 3054 + root_block_device { 3055 + volume_type = "gp2" 3056 } 3057 } 3058 `, 3059 }, 3060 "in-place update - creation": { 3061 Action: plans.Update, 3062 Mode: addrs.ManagedResourceMode, 3063 Before: cty.ObjectVal(map[string]cty.Value{ 3064 "id": cty.StringVal("i-02ae66f368e8518a9"), 3065 "ami": cty.StringVal("ami-BEFORE"), 3066 "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3067 "mount_point": cty.String, 3068 "size": cty.String, 3069 })), 3070 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3071 "volume_type": cty.String, 3072 })), 3073 }), 3074 After: cty.ObjectVal(map[string]cty.Value{ 3075 "id": cty.StringVal("i-02ae66f368e8518a9"), 3076 "ami": cty.StringVal("ami-AFTER"), 3077 "disks": cty.SetVal([]cty.Value{ 3078 cty.ObjectVal(map[string]cty.Value{ 3079 "mount_point": cty.StringVal("/var/diska"), 3080 "size": cty.NullVal(cty.String), 3081 }), 3082 }), 3083 "root_block_device": cty.SetVal([]cty.Value{ 3084 cty.ObjectVal(map[string]cty.Value{ 3085 "volume_type": cty.StringVal("gp2"), 3086 }), 3087 }), 3088 }), 3089 RequiredReplace: cty.NewPathSet(), 3090 Schema: testSchema(configschema.NestingSet), 3091 ExpectedOutput: ` # test_instance.example will be updated in-place 3092 ~ resource "test_instance" "example" { 3093 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3094 ~ disks = [ 3095 + { 3096 + mount_point = "/var/diska" 3097 }, 3098 ] 3099 id = "i-02ae66f368e8518a9" 3100 3101 + root_block_device { 3102 + volume_type = "gp2" 3103 } 3104 } 3105 `, 3106 }, 3107 "in-place update - creation - sensitive set": { 3108 Action: plans.Update, 3109 Mode: addrs.ManagedResourceMode, 3110 Before: cty.ObjectVal(map[string]cty.Value{ 3111 "id": cty.StringVal("i-02ae66f368e8518a9"), 3112 "ami": cty.StringVal("ami-BEFORE"), 3113 "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3114 "mount_point": cty.String, 3115 "size": cty.String, 3116 })), 3117 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3118 "volume_type": cty.String, 3119 })), 3120 }), 3121 After: cty.ObjectVal(map[string]cty.Value{ 3122 "id": cty.StringVal("i-02ae66f368e8518a9"), 3123 "ami": cty.StringVal("ami-AFTER"), 3124 "disks": cty.SetVal([]cty.Value{ 3125 cty.ObjectVal(map[string]cty.Value{ 3126 "mount_point": cty.StringVal("/var/diska"), 3127 "size": cty.NullVal(cty.String), 3128 }), 3129 }), 3130 "root_block_device": cty.SetVal([]cty.Value{ 3131 cty.ObjectVal(map[string]cty.Value{ 3132 "volume_type": cty.StringVal("gp2"), 3133 }), 3134 }), 3135 }), 3136 AfterValMarks: []cty.PathValueMarks{ 3137 { 3138 Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, 3139 Marks: cty.NewValueMarks(marks.Sensitive), 3140 }, 3141 }, 3142 RequiredReplace: cty.NewPathSet(), 3143 Schema: testSchema(configschema.NestingSet), 3144 ExpectedOutput: ` # test_instance.example will be updated in-place 3145 ~ resource "test_instance" "example" { 3146 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3147 # Warning: this attribute value will be marked as sensitive and will not 3148 # display in UI output after applying this change. 3149 ~ disks = (sensitive value) 3150 id = "i-02ae66f368e8518a9" 3151 3152 + root_block_device { 3153 + volume_type = "gp2" 3154 } 3155 } 3156 `, 3157 }, 3158 "in-place update - marking set sensitive": { 3159 Action: plans.Update, 3160 Mode: addrs.ManagedResourceMode, 3161 Before: cty.ObjectVal(map[string]cty.Value{ 3162 "id": cty.StringVal("i-02ae66f368e8518a9"), 3163 "ami": cty.StringVal("ami-BEFORE"), 3164 "disks": cty.SetVal([]cty.Value{ 3165 cty.ObjectVal(map[string]cty.Value{ 3166 "mount_point": cty.StringVal("/var/diska"), 3167 "size": cty.StringVal("50GB"), 3168 }), 3169 }), 3170 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3171 "volume_type": cty.String, 3172 })), 3173 }), 3174 After: cty.ObjectVal(map[string]cty.Value{ 3175 "id": cty.StringVal("i-02ae66f368e8518a9"), 3176 "ami": cty.StringVal("ami-AFTER"), 3177 "disks": cty.SetVal([]cty.Value{ 3178 cty.ObjectVal(map[string]cty.Value{ 3179 "mount_point": cty.StringVal("/var/diska"), 3180 "size": cty.StringVal("50GB"), 3181 }), 3182 }), 3183 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3184 "volume_type": cty.String, 3185 })), 3186 }), 3187 AfterValMarks: []cty.PathValueMarks{ 3188 { 3189 Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, 3190 Marks: cty.NewValueMarks(marks.Sensitive), 3191 }, 3192 }, 3193 RequiredReplace: cty.NewPathSet(), 3194 Schema: testSchema(configschema.NestingSet), 3195 ExpectedOutput: ` # test_instance.example will be updated in-place 3196 ~ resource "test_instance" "example" { 3197 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3198 # Warning: this attribute value will be marked as sensitive and will not 3199 # display in UI output after applying this change. The value is unchanged. 3200 ~ disks = (sensitive value) 3201 id = "i-02ae66f368e8518a9" 3202 } 3203 `, 3204 }, 3205 "in-place update - insertion": { 3206 Action: plans.Update, 3207 Mode: addrs.ManagedResourceMode, 3208 Before: cty.ObjectVal(map[string]cty.Value{ 3209 "id": cty.StringVal("i-02ae66f368e8518a9"), 3210 "ami": cty.StringVal("ami-BEFORE"), 3211 "disks": cty.SetVal([]cty.Value{ 3212 cty.ObjectVal(map[string]cty.Value{ 3213 "mount_point": cty.StringVal("/var/diska"), 3214 "size": cty.NullVal(cty.String), 3215 }), 3216 cty.ObjectVal(map[string]cty.Value{ 3217 "mount_point": cty.StringVal("/var/diskb"), 3218 "size": cty.StringVal("100GB"), 3219 }), 3220 }), 3221 "root_block_device": cty.SetVal([]cty.Value{ 3222 cty.ObjectVal(map[string]cty.Value{ 3223 "volume_type": cty.StringVal("gp2"), 3224 "new_field": cty.NullVal(cty.String), 3225 }), 3226 }), 3227 }), 3228 After: cty.ObjectVal(map[string]cty.Value{ 3229 "id": cty.StringVal("i-02ae66f368e8518a9"), 3230 "ami": cty.StringVal("ami-AFTER"), 3231 "disks": cty.SetVal([]cty.Value{ 3232 cty.ObjectVal(map[string]cty.Value{ 3233 "mount_point": cty.StringVal("/var/diska"), 3234 "size": cty.StringVal("50GB"), 3235 }), 3236 cty.ObjectVal(map[string]cty.Value{ 3237 "mount_point": cty.StringVal("/var/diskb"), 3238 "size": cty.StringVal("100GB"), 3239 }), 3240 }), 3241 "root_block_device": cty.SetVal([]cty.Value{ 3242 cty.ObjectVal(map[string]cty.Value{ 3243 "volume_type": cty.StringVal("gp2"), 3244 "new_field": cty.StringVal("new_value"), 3245 }), 3246 }), 3247 }), 3248 RequiredReplace: cty.NewPathSet(), 3249 Schema: testSchemaPlus(configschema.NestingSet), 3250 ExpectedOutput: ` # test_instance.example will be updated in-place 3251 ~ resource "test_instance" "example" { 3252 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3253 ~ disks = [ 3254 + { 3255 + mount_point = "/var/diska" 3256 + size = "50GB" 3257 }, 3258 - { 3259 - mount_point = "/var/diska" -> null 3260 }, 3261 # (1 unchanged element hidden) 3262 ] 3263 id = "i-02ae66f368e8518a9" 3264 3265 + root_block_device { 3266 + new_field = "new_value" 3267 + volume_type = "gp2" 3268 } 3269 - root_block_device { 3270 - volume_type = "gp2" -> null 3271 } 3272 } 3273 `, 3274 }, 3275 "force-new update (whole block)": { 3276 Action: plans.DeleteThenCreate, 3277 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 3278 Mode: addrs.ManagedResourceMode, 3279 Before: cty.ObjectVal(map[string]cty.Value{ 3280 "id": cty.StringVal("i-02ae66f368e8518a9"), 3281 "ami": cty.StringVal("ami-BEFORE"), 3282 "root_block_device": cty.SetVal([]cty.Value{ 3283 cty.ObjectVal(map[string]cty.Value{ 3284 "volume_type": cty.StringVal("gp2"), 3285 }), 3286 }), 3287 "disks": cty.SetVal([]cty.Value{ 3288 cty.ObjectVal(map[string]cty.Value{ 3289 "mount_point": cty.StringVal("/var/diska"), 3290 "size": cty.StringVal("50GB"), 3291 }), 3292 }), 3293 }), 3294 After: cty.ObjectVal(map[string]cty.Value{ 3295 "id": cty.StringVal("i-02ae66f368e8518a9"), 3296 "ami": cty.StringVal("ami-AFTER"), 3297 "root_block_device": cty.SetVal([]cty.Value{ 3298 cty.ObjectVal(map[string]cty.Value{ 3299 "volume_type": cty.StringVal("different"), 3300 }), 3301 }), 3302 "disks": cty.SetVal([]cty.Value{ 3303 cty.ObjectVal(map[string]cty.Value{ 3304 "mount_point": cty.StringVal("/var/diskb"), 3305 "size": cty.StringVal("50GB"), 3306 }), 3307 }), 3308 }), 3309 RequiredReplace: cty.NewPathSet( 3310 cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, 3311 cty.Path{cty.GetAttrStep{Name: "disks"}}, 3312 ), 3313 Schema: testSchema(configschema.NestingSet), 3314 ExpectedOutput: ` # test_instance.example must be replaced 3315 -/+ resource "test_instance" "example" { 3316 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3317 ~ disks = [ 3318 - { # forces replacement 3319 - mount_point = "/var/diska" -> null 3320 - size = "50GB" -> null 3321 }, 3322 + { # forces replacement 3323 + mount_point = "/var/diskb" 3324 + size = "50GB" 3325 }, 3326 ] 3327 id = "i-02ae66f368e8518a9" 3328 3329 + root_block_device { # forces replacement 3330 + volume_type = "different" 3331 } 3332 - root_block_device { # forces replacement 3333 - volume_type = "gp2" -> null 3334 } 3335 } 3336 `, 3337 }, 3338 "in-place update - deletion": { 3339 Action: plans.Update, 3340 Mode: addrs.ManagedResourceMode, 3341 Before: cty.ObjectVal(map[string]cty.Value{ 3342 "id": cty.StringVal("i-02ae66f368e8518a9"), 3343 "ami": cty.StringVal("ami-BEFORE"), 3344 "root_block_device": cty.SetVal([]cty.Value{ 3345 cty.ObjectVal(map[string]cty.Value{ 3346 "volume_type": cty.StringVal("gp2"), 3347 "new_field": cty.StringVal("new_value"), 3348 }), 3349 }), 3350 "disks": cty.SetVal([]cty.Value{ 3351 cty.ObjectVal(map[string]cty.Value{ 3352 "mount_point": cty.StringVal("/var/diska"), 3353 "size": cty.StringVal("50GB"), 3354 }), 3355 }), 3356 }), 3357 After: cty.ObjectVal(map[string]cty.Value{ 3358 "id": cty.StringVal("i-02ae66f368e8518a9"), 3359 "ami": cty.StringVal("ami-AFTER"), 3360 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3361 "volume_type": cty.String, 3362 "new_field": cty.String, 3363 })), 3364 "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3365 "mount_point": cty.String, 3366 "size": cty.String, 3367 })), 3368 }), 3369 RequiredReplace: cty.NewPathSet(), 3370 Schema: testSchemaPlus(configschema.NestingSet), 3371 ExpectedOutput: ` # test_instance.example will be updated in-place 3372 ~ resource "test_instance" "example" { 3373 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3374 ~ disks = [ 3375 - { 3376 - mount_point = "/var/diska" -> null 3377 - size = "50GB" -> null 3378 }, 3379 ] 3380 id = "i-02ae66f368e8518a9" 3381 3382 - root_block_device { 3383 - new_field = "new_value" -> null 3384 - volume_type = "gp2" -> null 3385 } 3386 } 3387 `, 3388 }, 3389 "in-place update - empty nested sets": { 3390 Action: plans.Update, 3391 Mode: addrs.ManagedResourceMode, 3392 Before: cty.ObjectVal(map[string]cty.Value{ 3393 "id": cty.StringVal("i-02ae66f368e8518a9"), 3394 "ami": cty.StringVal("ami-BEFORE"), 3395 "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 3396 "mount_point": cty.String, 3397 "size": cty.String, 3398 }))), 3399 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3400 "volume_type": cty.String, 3401 })), 3402 }), 3403 After: cty.ObjectVal(map[string]cty.Value{ 3404 "id": cty.StringVal("i-02ae66f368e8518a9"), 3405 "ami": cty.StringVal("ami-AFTER"), 3406 "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3407 "mount_point": cty.String, 3408 "size": cty.String, 3409 })), 3410 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 3411 "volume_type": cty.String, 3412 })), 3413 }), 3414 RequiredReplace: cty.NewPathSet(), 3415 Schema: testSchema(configschema.NestingSet), 3416 ExpectedOutput: ` # test_instance.example will be updated in-place 3417 ~ resource "test_instance" "example" { 3418 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3419 + disks = [ 3420 ] 3421 id = "i-02ae66f368e8518a9" 3422 } 3423 `, 3424 }, 3425 "in-place update - null insertion": { 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.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 3432 "mount_point": cty.String, 3433 "size": cty.String, 3434 }))), 3435 "root_block_device": cty.SetVal([]cty.Value{ 3436 cty.ObjectVal(map[string]cty.Value{ 3437 "volume_type": cty.StringVal("gp2"), 3438 "new_field": cty.NullVal(cty.String), 3439 }), 3440 }), 3441 }), 3442 After: cty.ObjectVal(map[string]cty.Value{ 3443 "id": cty.StringVal("i-02ae66f368e8518a9"), 3444 "ami": cty.StringVal("ami-AFTER"), 3445 "disks": cty.SetVal([]cty.Value{ 3446 cty.ObjectVal(map[string]cty.Value{ 3447 "mount_point": cty.StringVal("/var/diska"), 3448 "size": cty.StringVal("50GB"), 3449 }), 3450 }), 3451 "root_block_device": cty.SetVal([]cty.Value{ 3452 cty.ObjectVal(map[string]cty.Value{ 3453 "volume_type": cty.StringVal("gp2"), 3454 "new_field": cty.StringVal("new_value"), 3455 }), 3456 }), 3457 }), 3458 RequiredReplace: cty.NewPathSet(), 3459 Schema: testSchemaPlus(configschema.NestingSet), 3460 ExpectedOutput: ` # test_instance.example will be updated in-place 3461 ~ resource "test_instance" "example" { 3462 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3463 + disks = [ 3464 + { 3465 + mount_point = "/var/diska" 3466 + size = "50GB" 3467 }, 3468 ] 3469 id = "i-02ae66f368e8518a9" 3470 3471 + root_block_device { 3472 + new_field = "new_value" 3473 + volume_type = "gp2" 3474 } 3475 - root_block_device { 3476 - volume_type = "gp2" -> null 3477 } 3478 } 3479 `, 3480 }, 3481 "in-place update - unknown": { 3482 Action: plans.Update, 3483 Mode: addrs.ManagedResourceMode, 3484 Before: cty.ObjectVal(map[string]cty.Value{ 3485 "id": cty.StringVal("i-02ae66f368e8518a9"), 3486 "ami": cty.StringVal("ami-BEFORE"), 3487 "disks": cty.SetVal([]cty.Value{ 3488 cty.ObjectVal(map[string]cty.Value{ 3489 "mount_point": cty.StringVal("/var/diska"), 3490 "size": cty.StringVal("50GB"), 3491 }), 3492 }), 3493 "root_block_device": cty.SetVal([]cty.Value{ 3494 cty.ObjectVal(map[string]cty.Value{ 3495 "volume_type": cty.StringVal("gp2"), 3496 "new_field": cty.StringVal("new_value"), 3497 }), 3498 }), 3499 }), 3500 After: cty.ObjectVal(map[string]cty.Value{ 3501 "id": cty.StringVal("i-02ae66f368e8518a9"), 3502 "ami": cty.StringVal("ami-AFTER"), 3503 "disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ 3504 "mount_point": cty.String, 3505 "size": cty.String, 3506 }))), 3507 "root_block_device": cty.SetVal([]cty.Value{ 3508 cty.ObjectVal(map[string]cty.Value{ 3509 "volume_type": cty.StringVal("gp2"), 3510 "new_field": cty.StringVal("new_value"), 3511 }), 3512 }), 3513 }), 3514 RequiredReplace: cty.NewPathSet(), 3515 Schema: testSchemaPlus(configschema.NestingSet), 3516 ExpectedOutput: ` # test_instance.example will be updated in-place 3517 ~ resource "test_instance" "example" { 3518 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3519 ~ disks = [ 3520 - { 3521 - mount_point = "/var/diska" -> null 3522 - size = "50GB" -> null 3523 }, 3524 ] -> (known after apply) 3525 id = "i-02ae66f368e8518a9" 3526 3527 # (1 unchanged block hidden) 3528 } 3529 `, 3530 }, 3531 } 3532 runTestCases(t, testCases) 3533 } 3534 3535 func TestResourceChange_nestedMap(t *testing.T) { 3536 testCases := map[string]testCase{ 3537 "creation from null": { 3538 Action: plans.Update, 3539 Mode: addrs.ManagedResourceMode, 3540 Before: cty.ObjectVal(map[string]cty.Value{ 3541 "id": cty.NullVal(cty.String), 3542 "ami": cty.NullVal(cty.String), 3543 "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ 3544 "mount_point": cty.String, 3545 "size": cty.String, 3546 }))), 3547 "root_block_device": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ 3548 "volume_type": cty.String, 3549 }))), 3550 }), 3551 After: cty.ObjectVal(map[string]cty.Value{ 3552 "id": cty.StringVal("i-02ae66f368e8518a9"), 3553 "ami": cty.StringVal("ami-AFTER"), 3554 "disks": cty.MapVal(map[string]cty.Value{ 3555 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3556 "mount_point": cty.StringVal("/var/diska"), 3557 "size": cty.NullVal(cty.String), 3558 }), 3559 }), 3560 "root_block_device": cty.MapVal(map[string]cty.Value{ 3561 "a": cty.ObjectVal(map[string]cty.Value{ 3562 "volume_type": cty.StringVal("gp2"), 3563 }), 3564 }), 3565 }), 3566 RequiredReplace: cty.NewPathSet(), 3567 Schema: testSchema(configschema.NestingMap), 3568 ExpectedOutput: ` # test_instance.example will be updated in-place 3569 ~ resource "test_instance" "example" { 3570 + ami = "ami-AFTER" 3571 + disks = { 3572 + "disk_a" = { 3573 + mount_point = "/var/diska" 3574 }, 3575 } 3576 + id = "i-02ae66f368e8518a9" 3577 3578 + root_block_device "a" { 3579 + volume_type = "gp2" 3580 } 3581 } 3582 `, 3583 }, 3584 "in-place update - creation": { 3585 Action: plans.Update, 3586 Mode: addrs.ManagedResourceMode, 3587 Before: cty.ObjectVal(map[string]cty.Value{ 3588 "id": cty.StringVal("i-02ae66f368e8518a9"), 3589 "ami": cty.StringVal("ami-BEFORE"), 3590 "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 3591 "mount_point": cty.String, 3592 "size": cty.String, 3593 })), 3594 "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 3595 "volume_type": cty.String, 3596 })), 3597 }), 3598 After: cty.ObjectVal(map[string]cty.Value{ 3599 "id": cty.StringVal("i-02ae66f368e8518a9"), 3600 "ami": cty.StringVal("ami-AFTER"), 3601 "disks": cty.MapVal(map[string]cty.Value{ 3602 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3603 "mount_point": cty.StringVal("/var/diska"), 3604 "size": cty.NullVal(cty.String), 3605 }), 3606 }), 3607 "root_block_device": cty.MapVal(map[string]cty.Value{ 3608 "a": cty.ObjectVal(map[string]cty.Value{ 3609 "volume_type": cty.StringVal("gp2"), 3610 }), 3611 }), 3612 }), 3613 RequiredReplace: cty.NewPathSet(), 3614 Schema: testSchema(configschema.NestingMap), 3615 ExpectedOutput: ` # test_instance.example will be updated in-place 3616 ~ resource "test_instance" "example" { 3617 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3618 ~ disks = { 3619 + "disk_a" = { 3620 + mount_point = "/var/diska" 3621 }, 3622 } 3623 id = "i-02ae66f368e8518a9" 3624 3625 + root_block_device "a" { 3626 + volume_type = "gp2" 3627 } 3628 } 3629 `, 3630 }, 3631 "in-place update - change attr": { 3632 Action: plans.Update, 3633 Mode: addrs.ManagedResourceMode, 3634 Before: cty.ObjectVal(map[string]cty.Value{ 3635 "id": cty.StringVal("i-02ae66f368e8518a9"), 3636 "ami": cty.StringVal("ami-BEFORE"), 3637 "disks": cty.MapVal(map[string]cty.Value{ 3638 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3639 "mount_point": cty.StringVal("/var/diska"), 3640 "size": cty.NullVal(cty.String), 3641 }), 3642 }), 3643 "root_block_device": cty.MapVal(map[string]cty.Value{ 3644 "a": cty.ObjectVal(map[string]cty.Value{ 3645 "volume_type": cty.StringVal("gp2"), 3646 "new_field": cty.NullVal(cty.String), 3647 }), 3648 }), 3649 }), 3650 After: cty.ObjectVal(map[string]cty.Value{ 3651 "id": cty.StringVal("i-02ae66f368e8518a9"), 3652 "ami": cty.StringVal("ami-AFTER"), 3653 "disks": cty.MapVal(map[string]cty.Value{ 3654 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3655 "mount_point": cty.StringVal("/var/diska"), 3656 "size": cty.StringVal("50GB"), 3657 }), 3658 }), 3659 "root_block_device": cty.MapVal(map[string]cty.Value{ 3660 "a": cty.ObjectVal(map[string]cty.Value{ 3661 "volume_type": cty.StringVal("gp2"), 3662 "new_field": cty.StringVal("new_value"), 3663 }), 3664 }), 3665 }), 3666 RequiredReplace: cty.NewPathSet(), 3667 Schema: testSchemaPlus(configschema.NestingMap), 3668 ExpectedOutput: ` # test_instance.example will be updated in-place 3669 ~ resource "test_instance" "example" { 3670 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3671 ~ disks = { 3672 ~ "disk_a" = { 3673 + size = "50GB" 3674 # (1 unchanged attribute hidden) 3675 }, 3676 } 3677 id = "i-02ae66f368e8518a9" 3678 3679 ~ root_block_device "a" { 3680 + new_field = "new_value" 3681 # (1 unchanged attribute hidden) 3682 } 3683 } 3684 `, 3685 }, 3686 "in-place update - insertion": { 3687 Action: plans.Update, 3688 Mode: addrs.ManagedResourceMode, 3689 Before: cty.ObjectVal(map[string]cty.Value{ 3690 "id": cty.StringVal("i-02ae66f368e8518a9"), 3691 "ami": cty.StringVal("ami-BEFORE"), 3692 "disks": cty.MapVal(map[string]cty.Value{ 3693 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3694 "mount_point": cty.StringVal("/var/diska"), 3695 "size": cty.StringVal("50GB"), 3696 }), 3697 }), 3698 "root_block_device": cty.MapVal(map[string]cty.Value{ 3699 "a": cty.ObjectVal(map[string]cty.Value{ 3700 "volume_type": cty.StringVal("gp2"), 3701 "new_field": cty.NullVal(cty.String), 3702 }), 3703 }), 3704 }), 3705 After: cty.ObjectVal(map[string]cty.Value{ 3706 "id": cty.StringVal("i-02ae66f368e8518a9"), 3707 "ami": cty.StringVal("ami-AFTER"), 3708 "disks": cty.MapVal(map[string]cty.Value{ 3709 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3710 "mount_point": cty.StringVal("/var/diska"), 3711 "size": cty.StringVal("50GB"), 3712 }), 3713 "disk_2": cty.ObjectVal(map[string]cty.Value{ 3714 "mount_point": cty.StringVal("/var/disk2"), 3715 "size": cty.StringVal("50GB"), 3716 }), 3717 }), 3718 "root_block_device": cty.MapVal(map[string]cty.Value{ 3719 "a": cty.ObjectVal(map[string]cty.Value{ 3720 "volume_type": cty.StringVal("gp2"), 3721 "new_field": cty.NullVal(cty.String), 3722 }), 3723 "b": cty.ObjectVal(map[string]cty.Value{ 3724 "volume_type": cty.StringVal("gp2"), 3725 "new_field": cty.StringVal("new_value"), 3726 }), 3727 }), 3728 }), 3729 RequiredReplace: cty.NewPathSet(), 3730 Schema: testSchemaPlus(configschema.NestingMap), 3731 ExpectedOutput: ` # test_instance.example will be updated in-place 3732 ~ resource "test_instance" "example" { 3733 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3734 ~ disks = { 3735 + "disk_2" = { 3736 + mount_point = "/var/disk2" 3737 + size = "50GB" 3738 }, 3739 # (1 unchanged element hidden) 3740 } 3741 id = "i-02ae66f368e8518a9" 3742 3743 + root_block_device "b" { 3744 + new_field = "new_value" 3745 + volume_type = "gp2" 3746 } 3747 3748 # (1 unchanged block hidden) 3749 } 3750 `, 3751 }, 3752 "force-new update (whole block)": { 3753 Action: plans.DeleteThenCreate, 3754 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 3755 Mode: addrs.ManagedResourceMode, 3756 Before: cty.ObjectVal(map[string]cty.Value{ 3757 "id": cty.StringVal("i-02ae66f368e8518a9"), 3758 "ami": cty.StringVal("ami-BEFORE"), 3759 "disks": cty.MapVal(map[string]cty.Value{ 3760 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3761 "mount_point": cty.StringVal("/var/diska"), 3762 "size": cty.StringVal("50GB"), 3763 }), 3764 }), 3765 "root_block_device": cty.MapVal(map[string]cty.Value{ 3766 "a": cty.ObjectVal(map[string]cty.Value{ 3767 "volume_type": cty.StringVal("gp2"), 3768 }), 3769 "b": cty.ObjectVal(map[string]cty.Value{ 3770 "volume_type": cty.StringVal("standard"), 3771 }), 3772 }), 3773 }), 3774 After: cty.ObjectVal(map[string]cty.Value{ 3775 "id": cty.StringVal("i-02ae66f368e8518a9"), 3776 "ami": cty.StringVal("ami-AFTER"), 3777 "disks": cty.MapVal(map[string]cty.Value{ 3778 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3779 "mount_point": cty.StringVal("/var/diska"), 3780 "size": cty.StringVal("100GB"), 3781 }), 3782 }), 3783 "root_block_device": cty.MapVal(map[string]cty.Value{ 3784 "a": cty.ObjectVal(map[string]cty.Value{ 3785 "volume_type": cty.StringVal("different"), 3786 }), 3787 "b": cty.ObjectVal(map[string]cty.Value{ 3788 "volume_type": cty.StringVal("standard"), 3789 }), 3790 }), 3791 }), 3792 RequiredReplace: cty.NewPathSet(cty.Path{ 3793 cty.GetAttrStep{Name: "root_block_device"}, 3794 cty.IndexStep{Key: cty.StringVal("a")}, 3795 }, 3796 cty.Path{cty.GetAttrStep{Name: "disks"}}, 3797 ), 3798 Schema: testSchema(configschema.NestingMap), 3799 ExpectedOutput: ` # test_instance.example must be replaced 3800 -/+ resource "test_instance" "example" { 3801 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3802 ~ disks = { 3803 ~ "disk_a" = { # forces replacement 3804 ~ size = "50GB" -> "100GB" 3805 # (1 unchanged attribute hidden) 3806 }, 3807 } 3808 id = "i-02ae66f368e8518a9" 3809 3810 ~ root_block_device "a" { # forces replacement 3811 ~ volume_type = "gp2" -> "different" 3812 } 3813 3814 # (1 unchanged block hidden) 3815 } 3816 `, 3817 }, 3818 "in-place update - deletion": { 3819 Action: plans.Update, 3820 Mode: addrs.ManagedResourceMode, 3821 Before: cty.ObjectVal(map[string]cty.Value{ 3822 "id": cty.StringVal("i-02ae66f368e8518a9"), 3823 "ami": cty.StringVal("ami-BEFORE"), 3824 "disks": cty.MapVal(map[string]cty.Value{ 3825 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3826 "mount_point": cty.StringVal("/var/diska"), 3827 "size": cty.StringVal("50GB"), 3828 }), 3829 }), 3830 "root_block_device": cty.MapVal(map[string]cty.Value{ 3831 "a": cty.ObjectVal(map[string]cty.Value{ 3832 "volume_type": cty.StringVal("gp2"), 3833 "new_field": cty.StringVal("new_value"), 3834 }), 3835 }), 3836 }), 3837 After: cty.ObjectVal(map[string]cty.Value{ 3838 "id": cty.StringVal("i-02ae66f368e8518a9"), 3839 "ami": cty.StringVal("ami-AFTER"), 3840 "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 3841 "mount_point": cty.String, 3842 "size": cty.String, 3843 })), 3844 "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 3845 "volume_type": cty.String, 3846 "new_field": cty.String, 3847 })), 3848 }), 3849 RequiredReplace: cty.NewPathSet(), 3850 Schema: testSchemaPlus(configschema.NestingMap), 3851 ExpectedOutput: ` # test_instance.example will be updated in-place 3852 ~ resource "test_instance" "example" { 3853 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3854 ~ disks = { 3855 - "disk_a" = { 3856 - mount_point = "/var/diska" -> null 3857 - size = "50GB" -> null 3858 }, 3859 } 3860 id = "i-02ae66f368e8518a9" 3861 3862 - root_block_device "a" { 3863 - new_field = "new_value" -> null 3864 - volume_type = "gp2" -> null 3865 } 3866 } 3867 `, 3868 }, 3869 "in-place update - unknown": { 3870 Action: plans.Update, 3871 Mode: addrs.ManagedResourceMode, 3872 Before: cty.ObjectVal(map[string]cty.Value{ 3873 "id": cty.StringVal("i-02ae66f368e8518a9"), 3874 "ami": cty.StringVal("ami-BEFORE"), 3875 "disks": cty.MapVal(map[string]cty.Value{ 3876 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3877 "mount_point": cty.StringVal("/var/diska"), 3878 "size": cty.StringVal("50GB"), 3879 }), 3880 }), 3881 "root_block_device": cty.MapVal(map[string]cty.Value{ 3882 "a": cty.ObjectVal(map[string]cty.Value{ 3883 "volume_type": cty.StringVal("gp2"), 3884 "new_field": cty.StringVal("new_value"), 3885 }), 3886 }), 3887 }), 3888 After: cty.ObjectVal(map[string]cty.Value{ 3889 "id": cty.StringVal("i-02ae66f368e8518a9"), 3890 "ami": cty.StringVal("ami-AFTER"), 3891 "disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ 3892 "mount_point": cty.String, 3893 "size": cty.String, 3894 }))), 3895 "root_block_device": cty.MapVal(map[string]cty.Value{ 3896 "a": cty.ObjectVal(map[string]cty.Value{ 3897 "volume_type": cty.StringVal("gp2"), 3898 "new_field": cty.StringVal("new_value"), 3899 }), 3900 }), 3901 }), 3902 RequiredReplace: cty.NewPathSet(), 3903 Schema: testSchemaPlus(configschema.NestingMap), 3904 ExpectedOutput: ` # test_instance.example will be updated in-place 3905 ~ resource "test_instance" "example" { 3906 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3907 ~ disks = { 3908 - "disk_a" = { 3909 - mount_point = "/var/diska" -> null 3910 - size = "50GB" -> null 3911 }, 3912 } -> (known after apply) 3913 id = "i-02ae66f368e8518a9" 3914 3915 # (1 unchanged block hidden) 3916 } 3917 `, 3918 }, 3919 "in-place update - insertion sensitive": { 3920 Action: plans.Update, 3921 Mode: addrs.ManagedResourceMode, 3922 Before: cty.ObjectVal(map[string]cty.Value{ 3923 "id": cty.StringVal("i-02ae66f368e8518a9"), 3924 "ami": cty.StringVal("ami-BEFORE"), 3925 "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 3926 "mount_point": cty.String, 3927 "size": cty.String, 3928 })), 3929 "root_block_device": cty.MapVal(map[string]cty.Value{ 3930 "a": cty.ObjectVal(map[string]cty.Value{ 3931 "volume_type": cty.StringVal("gp2"), 3932 "new_field": cty.StringVal("new_value"), 3933 }), 3934 }), 3935 }), 3936 After: cty.ObjectVal(map[string]cty.Value{ 3937 "id": cty.StringVal("i-02ae66f368e8518a9"), 3938 "ami": cty.StringVal("ami-AFTER"), 3939 "disks": cty.MapVal(map[string]cty.Value{ 3940 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3941 "mount_point": cty.StringVal("/var/diska"), 3942 "size": cty.StringVal("50GB"), 3943 }), 3944 }), 3945 "root_block_device": cty.MapVal(map[string]cty.Value{ 3946 "a": cty.ObjectVal(map[string]cty.Value{ 3947 "volume_type": cty.StringVal("gp2"), 3948 "new_field": cty.StringVal("new_value"), 3949 }), 3950 }), 3951 }), 3952 AfterValMarks: []cty.PathValueMarks{ 3953 { 3954 Path: cty.Path{cty.GetAttrStep{Name: "disks"}, 3955 cty.IndexStep{Key: cty.StringVal("disk_a")}, 3956 cty.GetAttrStep{Name: "mount_point"}, 3957 }, 3958 Marks: cty.NewValueMarks(marks.Sensitive), 3959 }, 3960 }, 3961 RequiredReplace: cty.NewPathSet(), 3962 Schema: testSchemaPlus(configschema.NestingMap), 3963 ExpectedOutput: ` # test_instance.example will be updated in-place 3964 ~ resource "test_instance" "example" { 3965 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3966 ~ disks = { 3967 + "disk_a" = { 3968 + mount_point = (sensitive value) 3969 + size = "50GB" 3970 }, 3971 } 3972 id = "i-02ae66f368e8518a9" 3973 3974 # (1 unchanged block hidden) 3975 } 3976 `, 3977 }, 3978 "in-place update - multiple unchanged blocks": { 3979 Action: plans.Update, 3980 Mode: addrs.ManagedResourceMode, 3981 Before: cty.ObjectVal(map[string]cty.Value{ 3982 "id": cty.StringVal("i-02ae66f368e8518a9"), 3983 "ami": cty.StringVal("ami-BEFORE"), 3984 "disks": cty.MapVal(map[string]cty.Value{ 3985 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3986 "mount_point": cty.StringVal("/var/diska"), 3987 "size": cty.StringVal("50GB"), 3988 }), 3989 }), 3990 "root_block_device": cty.MapVal(map[string]cty.Value{ 3991 "a": cty.ObjectVal(map[string]cty.Value{ 3992 "volume_type": cty.StringVal("gp2"), 3993 }), 3994 "b": cty.ObjectVal(map[string]cty.Value{ 3995 "volume_type": cty.StringVal("gp2"), 3996 }), 3997 }), 3998 }), 3999 After: cty.ObjectVal(map[string]cty.Value{ 4000 "id": cty.StringVal("i-02ae66f368e8518a9"), 4001 "ami": cty.StringVal("ami-AFTER"), 4002 "disks": cty.MapVal(map[string]cty.Value{ 4003 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4004 "mount_point": cty.StringVal("/var/diska"), 4005 "size": cty.StringVal("50GB"), 4006 }), 4007 }), 4008 "root_block_device": cty.MapVal(map[string]cty.Value{ 4009 "a": cty.ObjectVal(map[string]cty.Value{ 4010 "volume_type": cty.StringVal("gp2"), 4011 }), 4012 "b": cty.ObjectVal(map[string]cty.Value{ 4013 "volume_type": cty.StringVal("gp2"), 4014 }), 4015 }), 4016 }), 4017 RequiredReplace: cty.NewPathSet(), 4018 Schema: testSchema(configschema.NestingMap), 4019 ExpectedOutput: ` # test_instance.example will be updated in-place 4020 ~ resource "test_instance" "example" { 4021 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4022 id = "i-02ae66f368e8518a9" 4023 # (1 unchanged attribute hidden) 4024 4025 # (2 unchanged blocks hidden) 4026 } 4027 `, 4028 }, 4029 "in-place update - multiple blocks first changed": { 4030 Action: plans.Update, 4031 Mode: addrs.ManagedResourceMode, 4032 Before: cty.ObjectVal(map[string]cty.Value{ 4033 "id": cty.StringVal("i-02ae66f368e8518a9"), 4034 "ami": cty.StringVal("ami-BEFORE"), 4035 "disks": cty.MapVal(map[string]cty.Value{ 4036 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4037 "mount_point": cty.StringVal("/var/diska"), 4038 "size": cty.StringVal("50GB"), 4039 }), 4040 }), 4041 "root_block_device": cty.MapVal(map[string]cty.Value{ 4042 "a": cty.ObjectVal(map[string]cty.Value{ 4043 "volume_type": cty.StringVal("gp2"), 4044 }), 4045 "b": cty.ObjectVal(map[string]cty.Value{ 4046 "volume_type": cty.StringVal("gp2"), 4047 }), 4048 }), 4049 }), 4050 After: cty.ObjectVal(map[string]cty.Value{ 4051 "id": cty.StringVal("i-02ae66f368e8518a9"), 4052 "ami": cty.StringVal("ami-AFTER"), 4053 "disks": cty.MapVal(map[string]cty.Value{ 4054 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4055 "mount_point": cty.StringVal("/var/diska"), 4056 "size": cty.StringVal("50GB"), 4057 }), 4058 }), 4059 "root_block_device": cty.MapVal(map[string]cty.Value{ 4060 "a": cty.ObjectVal(map[string]cty.Value{ 4061 "volume_type": cty.StringVal("gp2"), 4062 }), 4063 "b": cty.ObjectVal(map[string]cty.Value{ 4064 "volume_type": cty.StringVal("gp3"), 4065 }), 4066 }), 4067 }), 4068 RequiredReplace: cty.NewPathSet(), 4069 Schema: testSchema(configschema.NestingMap), 4070 ExpectedOutput: ` # test_instance.example will be updated in-place 4071 ~ resource "test_instance" "example" { 4072 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4073 id = "i-02ae66f368e8518a9" 4074 # (1 unchanged attribute hidden) 4075 4076 ~ root_block_device "b" { 4077 ~ volume_type = "gp2" -> "gp3" 4078 } 4079 4080 # (1 unchanged block hidden) 4081 } 4082 `, 4083 }, 4084 "in-place update - multiple blocks second changed": { 4085 Action: plans.Update, 4086 Mode: addrs.ManagedResourceMode, 4087 Before: cty.ObjectVal(map[string]cty.Value{ 4088 "id": cty.StringVal("i-02ae66f368e8518a9"), 4089 "ami": cty.StringVal("ami-BEFORE"), 4090 "disks": cty.MapVal(map[string]cty.Value{ 4091 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4092 "mount_point": cty.StringVal("/var/diska"), 4093 "size": cty.StringVal("50GB"), 4094 }), 4095 }), 4096 "root_block_device": cty.MapVal(map[string]cty.Value{ 4097 "a": cty.ObjectVal(map[string]cty.Value{ 4098 "volume_type": cty.StringVal("gp2"), 4099 }), 4100 "b": cty.ObjectVal(map[string]cty.Value{ 4101 "volume_type": cty.StringVal("gp2"), 4102 }), 4103 }), 4104 }), 4105 After: cty.ObjectVal(map[string]cty.Value{ 4106 "id": cty.StringVal("i-02ae66f368e8518a9"), 4107 "ami": cty.StringVal("ami-AFTER"), 4108 "disks": cty.MapVal(map[string]cty.Value{ 4109 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4110 "mount_point": cty.StringVal("/var/diska"), 4111 "size": cty.StringVal("50GB"), 4112 }), 4113 }), 4114 "root_block_device": cty.MapVal(map[string]cty.Value{ 4115 "a": cty.ObjectVal(map[string]cty.Value{ 4116 "volume_type": cty.StringVal("gp3"), 4117 }), 4118 "b": cty.ObjectVal(map[string]cty.Value{ 4119 "volume_type": cty.StringVal("gp2"), 4120 }), 4121 }), 4122 }), 4123 RequiredReplace: cty.NewPathSet(), 4124 Schema: testSchema(configschema.NestingMap), 4125 ExpectedOutput: ` # test_instance.example will be updated in-place 4126 ~ resource "test_instance" "example" { 4127 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4128 id = "i-02ae66f368e8518a9" 4129 # (1 unchanged attribute hidden) 4130 4131 ~ root_block_device "a" { 4132 ~ volume_type = "gp2" -> "gp3" 4133 } 4134 4135 # (1 unchanged block hidden) 4136 } 4137 `, 4138 }, 4139 "in-place update - multiple blocks changed": { 4140 Action: plans.Update, 4141 Mode: addrs.ManagedResourceMode, 4142 Before: cty.ObjectVal(map[string]cty.Value{ 4143 "id": cty.StringVal("i-02ae66f368e8518a9"), 4144 "ami": cty.StringVal("ami-BEFORE"), 4145 "disks": cty.MapVal(map[string]cty.Value{ 4146 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4147 "mount_point": cty.StringVal("/var/diska"), 4148 "size": cty.StringVal("50GB"), 4149 }), 4150 }), 4151 "root_block_device": cty.MapVal(map[string]cty.Value{ 4152 "a": cty.ObjectVal(map[string]cty.Value{ 4153 "volume_type": cty.StringVal("gp2"), 4154 }), 4155 "b": cty.ObjectVal(map[string]cty.Value{ 4156 "volume_type": cty.StringVal("gp2"), 4157 }), 4158 }), 4159 }), 4160 After: cty.ObjectVal(map[string]cty.Value{ 4161 "id": cty.StringVal("i-02ae66f368e8518a9"), 4162 "ami": cty.StringVal("ami-AFTER"), 4163 "disks": cty.MapVal(map[string]cty.Value{ 4164 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4165 "mount_point": cty.StringVal("/var/diska"), 4166 "size": cty.StringVal("50GB"), 4167 }), 4168 }), 4169 "root_block_device": cty.MapVal(map[string]cty.Value{ 4170 "a": cty.ObjectVal(map[string]cty.Value{ 4171 "volume_type": cty.StringVal("gp3"), 4172 }), 4173 "b": cty.ObjectVal(map[string]cty.Value{ 4174 "volume_type": cty.StringVal("gp3"), 4175 }), 4176 }), 4177 }), 4178 RequiredReplace: cty.NewPathSet(), 4179 Schema: testSchema(configschema.NestingMap), 4180 ExpectedOutput: ` # test_instance.example will be updated in-place 4181 ~ resource "test_instance" "example" { 4182 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4183 id = "i-02ae66f368e8518a9" 4184 # (1 unchanged attribute hidden) 4185 4186 ~ root_block_device "a" { 4187 ~ volume_type = "gp2" -> "gp3" 4188 } 4189 ~ root_block_device "b" { 4190 ~ volume_type = "gp2" -> "gp3" 4191 } 4192 } 4193 `, 4194 }, 4195 "in-place update - multiple different unchanged blocks": { 4196 Action: plans.Update, 4197 Mode: addrs.ManagedResourceMode, 4198 Before: cty.ObjectVal(map[string]cty.Value{ 4199 "id": cty.StringVal("i-02ae66f368e8518a9"), 4200 "ami": cty.StringVal("ami-BEFORE"), 4201 "disks": cty.MapVal(map[string]cty.Value{ 4202 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4203 "mount_point": cty.StringVal("/var/diska"), 4204 "size": cty.StringVal("50GB"), 4205 }), 4206 }), 4207 "root_block_device": cty.MapVal(map[string]cty.Value{ 4208 "a": cty.ObjectVal(map[string]cty.Value{ 4209 "volume_type": cty.StringVal("gp2"), 4210 }), 4211 }), 4212 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4213 "b": cty.ObjectVal(map[string]cty.Value{ 4214 "volume_type": cty.StringVal("gp2"), 4215 }), 4216 }), 4217 }), 4218 After: cty.ObjectVal(map[string]cty.Value{ 4219 "id": cty.StringVal("i-02ae66f368e8518a9"), 4220 "ami": cty.StringVal("ami-AFTER"), 4221 "disks": cty.MapVal(map[string]cty.Value{ 4222 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4223 "mount_point": cty.StringVal("/var/diska"), 4224 "size": cty.StringVal("50GB"), 4225 }), 4226 }), 4227 "root_block_device": cty.MapVal(map[string]cty.Value{ 4228 "a": cty.ObjectVal(map[string]cty.Value{ 4229 "volume_type": cty.StringVal("gp2"), 4230 }), 4231 }), 4232 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4233 "b": cty.ObjectVal(map[string]cty.Value{ 4234 "volume_type": cty.StringVal("gp2"), 4235 }), 4236 }), 4237 }), 4238 RequiredReplace: cty.NewPathSet(), 4239 Schema: testSchemaMultipleBlocks(configschema.NestingMap), 4240 ExpectedOutput: ` # test_instance.example will be updated in-place 4241 ~ resource "test_instance" "example" { 4242 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4243 id = "i-02ae66f368e8518a9" 4244 # (1 unchanged attribute hidden) 4245 4246 # (2 unchanged blocks hidden) 4247 } 4248 `, 4249 }, 4250 "in-place update - multiple different blocks first changed": { 4251 Action: plans.Update, 4252 Mode: addrs.ManagedResourceMode, 4253 Before: cty.ObjectVal(map[string]cty.Value{ 4254 "id": cty.StringVal("i-02ae66f368e8518a9"), 4255 "ami": cty.StringVal("ami-BEFORE"), 4256 "disks": cty.MapVal(map[string]cty.Value{ 4257 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4258 "mount_point": cty.StringVal("/var/diska"), 4259 "size": cty.StringVal("50GB"), 4260 }), 4261 }), 4262 "root_block_device": cty.MapVal(map[string]cty.Value{ 4263 "a": cty.ObjectVal(map[string]cty.Value{ 4264 "volume_type": cty.StringVal("gp2"), 4265 }), 4266 }), 4267 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4268 "b": cty.ObjectVal(map[string]cty.Value{ 4269 "volume_type": cty.StringVal("gp2"), 4270 }), 4271 }), 4272 }), 4273 After: cty.ObjectVal(map[string]cty.Value{ 4274 "id": cty.StringVal("i-02ae66f368e8518a9"), 4275 "ami": cty.StringVal("ami-AFTER"), 4276 "disks": cty.MapVal(map[string]cty.Value{ 4277 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4278 "mount_point": cty.StringVal("/var/diska"), 4279 "size": cty.StringVal("50GB"), 4280 }), 4281 }), 4282 "root_block_device": cty.MapVal(map[string]cty.Value{ 4283 "a": cty.ObjectVal(map[string]cty.Value{ 4284 "volume_type": cty.StringVal("gp2"), 4285 }), 4286 }), 4287 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4288 "b": cty.ObjectVal(map[string]cty.Value{ 4289 "volume_type": cty.StringVal("gp3"), 4290 }), 4291 }), 4292 }), 4293 RequiredReplace: cty.NewPathSet(), 4294 Schema: testSchemaMultipleBlocks(configschema.NestingMap), 4295 ExpectedOutput: ` # test_instance.example will be updated in-place 4296 ~ resource "test_instance" "example" { 4297 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4298 id = "i-02ae66f368e8518a9" 4299 # (1 unchanged attribute hidden) 4300 4301 ~ leaf_block_device "b" { 4302 ~ volume_type = "gp2" -> "gp3" 4303 } 4304 4305 # (1 unchanged block hidden) 4306 } 4307 `, 4308 }, 4309 "in-place update - multiple different blocks second changed": { 4310 Action: plans.Update, 4311 Mode: addrs.ManagedResourceMode, 4312 Before: cty.ObjectVal(map[string]cty.Value{ 4313 "id": cty.StringVal("i-02ae66f368e8518a9"), 4314 "ami": cty.StringVal("ami-BEFORE"), 4315 "disks": cty.MapVal(map[string]cty.Value{ 4316 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4317 "mount_point": cty.StringVal("/var/diska"), 4318 "size": cty.StringVal("50GB"), 4319 }), 4320 }), 4321 "root_block_device": cty.MapVal(map[string]cty.Value{ 4322 "a": cty.ObjectVal(map[string]cty.Value{ 4323 "volume_type": cty.StringVal("gp2"), 4324 }), 4325 }), 4326 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4327 "b": cty.ObjectVal(map[string]cty.Value{ 4328 "volume_type": cty.StringVal("gp2"), 4329 }), 4330 }), 4331 }), 4332 After: cty.ObjectVal(map[string]cty.Value{ 4333 "id": cty.StringVal("i-02ae66f368e8518a9"), 4334 "ami": cty.StringVal("ami-AFTER"), 4335 "disks": cty.MapVal(map[string]cty.Value{ 4336 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4337 "mount_point": cty.StringVal("/var/diska"), 4338 "size": cty.StringVal("50GB"), 4339 }), 4340 }), 4341 "root_block_device": cty.MapVal(map[string]cty.Value{ 4342 "a": cty.ObjectVal(map[string]cty.Value{ 4343 "volume_type": cty.StringVal("gp3"), 4344 }), 4345 }), 4346 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4347 "b": cty.ObjectVal(map[string]cty.Value{ 4348 "volume_type": cty.StringVal("gp2"), 4349 }), 4350 }), 4351 }), 4352 RequiredReplace: cty.NewPathSet(), 4353 Schema: testSchemaMultipleBlocks(configschema.NestingMap), 4354 ExpectedOutput: ` # test_instance.example will be updated in-place 4355 ~ resource "test_instance" "example" { 4356 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4357 id = "i-02ae66f368e8518a9" 4358 # (1 unchanged attribute hidden) 4359 4360 ~ root_block_device "a" { 4361 ~ volume_type = "gp2" -> "gp3" 4362 } 4363 4364 # (1 unchanged block hidden) 4365 } 4366 `, 4367 }, 4368 "in-place update - multiple different blocks changed": { 4369 Action: plans.Update, 4370 Mode: addrs.ManagedResourceMode, 4371 Before: cty.ObjectVal(map[string]cty.Value{ 4372 "id": cty.StringVal("i-02ae66f368e8518a9"), 4373 "ami": cty.StringVal("ami-BEFORE"), 4374 "disks": cty.MapVal(map[string]cty.Value{ 4375 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4376 "mount_point": cty.StringVal("/var/diska"), 4377 "size": cty.StringVal("50GB"), 4378 }), 4379 }), 4380 "root_block_device": cty.MapVal(map[string]cty.Value{ 4381 "a": cty.ObjectVal(map[string]cty.Value{ 4382 "volume_type": cty.StringVal("gp2"), 4383 }), 4384 }), 4385 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4386 "b": cty.ObjectVal(map[string]cty.Value{ 4387 "volume_type": cty.StringVal("gp2"), 4388 }), 4389 }), 4390 }), 4391 After: cty.ObjectVal(map[string]cty.Value{ 4392 "id": cty.StringVal("i-02ae66f368e8518a9"), 4393 "ami": cty.StringVal("ami-AFTER"), 4394 "disks": cty.MapVal(map[string]cty.Value{ 4395 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4396 "mount_point": cty.StringVal("/var/diska"), 4397 "size": cty.StringVal("50GB"), 4398 }), 4399 }), 4400 "root_block_device": cty.MapVal(map[string]cty.Value{ 4401 "a": cty.ObjectVal(map[string]cty.Value{ 4402 "volume_type": cty.StringVal("gp3"), 4403 }), 4404 }), 4405 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4406 "b": cty.ObjectVal(map[string]cty.Value{ 4407 "volume_type": cty.StringVal("gp3"), 4408 }), 4409 }), 4410 }), 4411 RequiredReplace: cty.NewPathSet(), 4412 Schema: testSchemaMultipleBlocks(configschema.NestingMap), 4413 ExpectedOutput: ` # test_instance.example will be updated in-place 4414 ~ resource "test_instance" "example" { 4415 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4416 id = "i-02ae66f368e8518a9" 4417 # (1 unchanged attribute hidden) 4418 4419 ~ leaf_block_device "b" { 4420 ~ volume_type = "gp2" -> "gp3" 4421 } 4422 4423 ~ root_block_device "a" { 4424 ~ volume_type = "gp2" -> "gp3" 4425 } 4426 } 4427 `, 4428 }, 4429 "in-place update - mixed blocks unchanged": { 4430 Action: plans.Update, 4431 Mode: addrs.ManagedResourceMode, 4432 Before: cty.ObjectVal(map[string]cty.Value{ 4433 "id": cty.StringVal("i-02ae66f368e8518a9"), 4434 "ami": cty.StringVal("ami-BEFORE"), 4435 "disks": cty.MapVal(map[string]cty.Value{ 4436 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4437 "mount_point": cty.StringVal("/var/diska"), 4438 "size": cty.StringVal("50GB"), 4439 }), 4440 }), 4441 "root_block_device": cty.MapVal(map[string]cty.Value{ 4442 "a": cty.ObjectVal(map[string]cty.Value{ 4443 "volume_type": cty.StringVal("gp2"), 4444 }), 4445 "b": cty.ObjectVal(map[string]cty.Value{ 4446 "volume_type": cty.StringVal("gp2"), 4447 }), 4448 }), 4449 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4450 "a": cty.ObjectVal(map[string]cty.Value{ 4451 "volume_type": cty.StringVal("gp2"), 4452 }), 4453 "b": cty.ObjectVal(map[string]cty.Value{ 4454 "volume_type": cty.StringVal("gp2"), 4455 }), 4456 }), 4457 }), 4458 After: cty.ObjectVal(map[string]cty.Value{ 4459 "id": cty.StringVal("i-02ae66f368e8518a9"), 4460 "ami": cty.StringVal("ami-AFTER"), 4461 "disks": cty.MapVal(map[string]cty.Value{ 4462 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4463 "mount_point": cty.StringVal("/var/diska"), 4464 "size": cty.StringVal("50GB"), 4465 }), 4466 }), 4467 "root_block_device": cty.MapVal(map[string]cty.Value{ 4468 "a": cty.ObjectVal(map[string]cty.Value{ 4469 "volume_type": cty.StringVal("gp2"), 4470 }), 4471 "b": cty.ObjectVal(map[string]cty.Value{ 4472 "volume_type": cty.StringVal("gp2"), 4473 }), 4474 }), 4475 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4476 "a": cty.ObjectVal(map[string]cty.Value{ 4477 "volume_type": cty.StringVal("gp2"), 4478 }), 4479 "b": cty.ObjectVal(map[string]cty.Value{ 4480 "volume_type": cty.StringVal("gp2"), 4481 }), 4482 }), 4483 }), 4484 RequiredReplace: cty.NewPathSet(), 4485 Schema: testSchemaMultipleBlocks(configschema.NestingMap), 4486 ExpectedOutput: ` # test_instance.example will be updated in-place 4487 ~ resource "test_instance" "example" { 4488 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4489 id = "i-02ae66f368e8518a9" 4490 # (1 unchanged attribute hidden) 4491 4492 # (4 unchanged blocks hidden) 4493 } 4494 `, 4495 }, 4496 "in-place update - mixed blocks changed": { 4497 Action: plans.Update, 4498 Mode: addrs.ManagedResourceMode, 4499 Before: cty.ObjectVal(map[string]cty.Value{ 4500 "id": cty.StringVal("i-02ae66f368e8518a9"), 4501 "ami": cty.StringVal("ami-BEFORE"), 4502 "disks": cty.MapVal(map[string]cty.Value{ 4503 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4504 "mount_point": cty.StringVal("/var/diska"), 4505 "size": cty.StringVal("50GB"), 4506 }), 4507 }), 4508 "root_block_device": cty.MapVal(map[string]cty.Value{ 4509 "a": cty.ObjectVal(map[string]cty.Value{ 4510 "volume_type": cty.StringVal("gp2"), 4511 }), 4512 "b": cty.ObjectVal(map[string]cty.Value{ 4513 "volume_type": cty.StringVal("gp2"), 4514 }), 4515 }), 4516 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4517 "a": cty.ObjectVal(map[string]cty.Value{ 4518 "volume_type": cty.StringVal("gp2"), 4519 }), 4520 "b": cty.ObjectVal(map[string]cty.Value{ 4521 "volume_type": cty.StringVal("gp2"), 4522 }), 4523 }), 4524 }), 4525 After: cty.ObjectVal(map[string]cty.Value{ 4526 "id": cty.StringVal("i-02ae66f368e8518a9"), 4527 "ami": cty.StringVal("ami-AFTER"), 4528 "disks": cty.MapVal(map[string]cty.Value{ 4529 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4530 "mount_point": cty.StringVal("/var/diska"), 4531 "size": cty.StringVal("50GB"), 4532 }), 4533 }), 4534 "root_block_device": cty.MapVal(map[string]cty.Value{ 4535 "a": cty.ObjectVal(map[string]cty.Value{ 4536 "volume_type": cty.StringVal("gp2"), 4537 }), 4538 "b": cty.ObjectVal(map[string]cty.Value{ 4539 "volume_type": cty.StringVal("gp3"), 4540 }), 4541 }), 4542 "leaf_block_device": cty.MapVal(map[string]cty.Value{ 4543 "a": cty.ObjectVal(map[string]cty.Value{ 4544 "volume_type": cty.StringVal("gp2"), 4545 }), 4546 "b": cty.ObjectVal(map[string]cty.Value{ 4547 "volume_type": cty.StringVal("gp3"), 4548 }), 4549 }), 4550 }), 4551 RequiredReplace: cty.NewPathSet(), 4552 Schema: testSchemaMultipleBlocks(configschema.NestingMap), 4553 ExpectedOutput: ` # test_instance.example will be updated in-place 4554 ~ resource "test_instance" "example" { 4555 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4556 id = "i-02ae66f368e8518a9" 4557 # (1 unchanged attribute hidden) 4558 4559 ~ leaf_block_device "b" { 4560 ~ volume_type = "gp2" -> "gp3" 4561 } 4562 4563 ~ root_block_device "b" { 4564 ~ volume_type = "gp2" -> "gp3" 4565 } 4566 4567 # (2 unchanged blocks hidden) 4568 } 4569 `, 4570 }, 4571 } 4572 runTestCases(t, testCases) 4573 } 4574 4575 func TestResourceChange_nestedSingle(t *testing.T) { 4576 testCases := map[string]testCase{ 4577 "in-place update - equal": { 4578 Action: plans.Update, 4579 Mode: addrs.ManagedResourceMode, 4580 Before: cty.ObjectVal(map[string]cty.Value{ 4581 "id": cty.StringVal("i-02ae66f368e8518a9"), 4582 "ami": cty.StringVal("ami-BEFORE"), 4583 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4584 "volume_type": cty.StringVal("gp2"), 4585 }), 4586 "disk": cty.ObjectVal(map[string]cty.Value{ 4587 "mount_point": cty.StringVal("/var/diska"), 4588 "size": cty.StringVal("50GB"), 4589 }), 4590 }), 4591 After: cty.ObjectVal(map[string]cty.Value{ 4592 "id": cty.StringVal("i-02ae66f368e8518a9"), 4593 "ami": cty.StringVal("ami-AFTER"), 4594 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4595 "volume_type": cty.StringVal("gp2"), 4596 }), 4597 "disk": cty.ObjectVal(map[string]cty.Value{ 4598 "mount_point": cty.StringVal("/var/diska"), 4599 "size": cty.StringVal("50GB"), 4600 }), 4601 }), 4602 RequiredReplace: cty.NewPathSet(), 4603 Schema: testSchema(configschema.NestingSingle), 4604 ExpectedOutput: ` # test_instance.example will be updated in-place 4605 ~ resource "test_instance" "example" { 4606 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4607 id = "i-02ae66f368e8518a9" 4608 # (1 unchanged attribute hidden) 4609 4610 # (1 unchanged block hidden) 4611 } 4612 `, 4613 }, 4614 "in-place update - creation": { 4615 Action: plans.Update, 4616 Mode: addrs.ManagedResourceMode, 4617 Before: cty.ObjectVal(map[string]cty.Value{ 4618 "id": cty.StringVal("i-02ae66f368e8518a9"), 4619 "ami": cty.StringVal("ami-BEFORE"), 4620 "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ 4621 "volume_type": cty.String, 4622 })), 4623 "disk": cty.NullVal(cty.Object(map[string]cty.Type{ 4624 "mount_point": cty.String, 4625 "size": cty.String, 4626 })), 4627 }), 4628 After: cty.ObjectVal(map[string]cty.Value{ 4629 "id": cty.StringVal("i-02ae66f368e8518a9"), 4630 "ami": cty.StringVal("ami-AFTER"), 4631 "disk": cty.ObjectVal(map[string]cty.Value{ 4632 "mount_point": cty.StringVal("/var/diska"), 4633 "size": cty.StringVal("50GB"), 4634 }), 4635 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4636 "volume_type": cty.NullVal(cty.String), 4637 }), 4638 }), 4639 RequiredReplace: cty.NewPathSet(), 4640 Schema: testSchema(configschema.NestingSingle), 4641 ExpectedOutput: ` # test_instance.example will be updated in-place 4642 ~ resource "test_instance" "example" { 4643 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4644 + disk = { 4645 + mount_point = "/var/diska" 4646 + size = "50GB" 4647 } 4648 id = "i-02ae66f368e8518a9" 4649 4650 + root_block_device {} 4651 } 4652 `, 4653 }, 4654 "force-new update (inside blocks)": { 4655 Action: plans.DeleteThenCreate, 4656 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 4657 Mode: addrs.ManagedResourceMode, 4658 Before: cty.ObjectVal(map[string]cty.Value{ 4659 "id": cty.StringVal("i-02ae66f368e8518a9"), 4660 "ami": cty.StringVal("ami-BEFORE"), 4661 "disk": cty.ObjectVal(map[string]cty.Value{ 4662 "mount_point": cty.StringVal("/var/diska"), 4663 "size": cty.StringVal("50GB"), 4664 }), 4665 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4666 "volume_type": cty.StringVal("gp2"), 4667 }), 4668 }), 4669 After: cty.ObjectVal(map[string]cty.Value{ 4670 "id": cty.StringVal("i-02ae66f368e8518a9"), 4671 "ami": cty.StringVal("ami-AFTER"), 4672 "disk": cty.ObjectVal(map[string]cty.Value{ 4673 "mount_point": cty.StringVal("/var/diskb"), 4674 "size": cty.StringVal("50GB"), 4675 }), 4676 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4677 "volume_type": cty.StringVal("different"), 4678 }), 4679 }), 4680 RequiredReplace: cty.NewPathSet( 4681 cty.Path{ 4682 cty.GetAttrStep{Name: "root_block_device"}, 4683 cty.GetAttrStep{Name: "volume_type"}, 4684 }, 4685 cty.Path{ 4686 cty.GetAttrStep{Name: "disk"}, 4687 cty.GetAttrStep{Name: "mount_point"}, 4688 }, 4689 ), 4690 Schema: testSchema(configschema.NestingSingle), 4691 ExpectedOutput: ` # test_instance.example must be replaced 4692 -/+ resource "test_instance" "example" { 4693 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4694 ~ disk = { 4695 ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement 4696 # (1 unchanged attribute hidden) 4697 } 4698 id = "i-02ae66f368e8518a9" 4699 4700 ~ root_block_device { 4701 ~ volume_type = "gp2" -> "different" # forces replacement 4702 } 4703 } 4704 `, 4705 }, 4706 "force-new update (whole block)": { 4707 Action: plans.DeleteThenCreate, 4708 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 4709 Mode: addrs.ManagedResourceMode, 4710 Before: cty.ObjectVal(map[string]cty.Value{ 4711 "id": cty.StringVal("i-02ae66f368e8518a9"), 4712 "ami": cty.StringVal("ami-BEFORE"), 4713 "disk": cty.ObjectVal(map[string]cty.Value{ 4714 "mount_point": cty.StringVal("/var/diska"), 4715 "size": cty.StringVal("50GB"), 4716 }), 4717 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4718 "volume_type": cty.StringVal("gp2"), 4719 }), 4720 }), 4721 After: cty.ObjectVal(map[string]cty.Value{ 4722 "id": cty.StringVal("i-02ae66f368e8518a9"), 4723 "ami": cty.StringVal("ami-AFTER"), 4724 "disk": cty.ObjectVal(map[string]cty.Value{ 4725 "mount_point": cty.StringVal("/var/diskb"), 4726 "size": cty.StringVal("50GB"), 4727 }), 4728 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4729 "volume_type": cty.StringVal("different"), 4730 }), 4731 }), 4732 RequiredReplace: cty.NewPathSet( 4733 cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, 4734 cty.Path{cty.GetAttrStep{Name: "disk"}}, 4735 ), 4736 Schema: testSchema(configschema.NestingSingle), 4737 ExpectedOutput: ` # test_instance.example must be replaced 4738 -/+ resource "test_instance" "example" { 4739 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4740 ~ disk = { # forces replacement 4741 ~ mount_point = "/var/diska" -> "/var/diskb" 4742 # (1 unchanged attribute hidden) 4743 } 4744 id = "i-02ae66f368e8518a9" 4745 4746 ~ root_block_device { # forces replacement 4747 ~ volume_type = "gp2" -> "different" 4748 } 4749 } 4750 `, 4751 }, 4752 "in-place update - deletion": { 4753 Action: plans.Update, 4754 Mode: addrs.ManagedResourceMode, 4755 Before: cty.ObjectVal(map[string]cty.Value{ 4756 "id": cty.StringVal("i-02ae66f368e8518a9"), 4757 "ami": cty.StringVal("ami-BEFORE"), 4758 "disk": cty.ObjectVal(map[string]cty.Value{ 4759 "mount_point": cty.StringVal("/var/diska"), 4760 "size": cty.StringVal("50GB"), 4761 }), 4762 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4763 "volume_type": cty.StringVal("gp2"), 4764 }), 4765 }), 4766 After: cty.ObjectVal(map[string]cty.Value{ 4767 "id": cty.StringVal("i-02ae66f368e8518a9"), 4768 "ami": cty.StringVal("ami-AFTER"), 4769 "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ 4770 "volume_type": cty.String, 4771 })), 4772 "disk": cty.NullVal(cty.Object(map[string]cty.Type{ 4773 "mount_point": cty.String, 4774 "size": cty.String, 4775 })), 4776 }), 4777 RequiredReplace: cty.NewPathSet(), 4778 Schema: testSchema(configschema.NestingSingle), 4779 ExpectedOutput: ` # test_instance.example will be updated in-place 4780 ~ resource "test_instance" "example" { 4781 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4782 - disk = { 4783 - mount_point = "/var/diska" -> null 4784 - size = "50GB" -> null 4785 } -> null 4786 id = "i-02ae66f368e8518a9" 4787 4788 - root_block_device { 4789 - volume_type = "gp2" -> null 4790 } 4791 } 4792 `, 4793 }, 4794 "with dynamically-typed attribute": { 4795 Action: plans.Update, 4796 Mode: addrs.ManagedResourceMode, 4797 Before: cty.ObjectVal(map[string]cty.Value{ 4798 "block": cty.NullVal(cty.Object(map[string]cty.Type{ 4799 "attr": cty.String, 4800 })), 4801 }), 4802 After: cty.ObjectVal(map[string]cty.Value{ 4803 "block": cty.ObjectVal(map[string]cty.Value{ 4804 "attr": cty.StringVal("foo"), 4805 }), 4806 }), 4807 RequiredReplace: cty.NewPathSet(), 4808 Schema: &configschema.Block{ 4809 BlockTypes: map[string]*configschema.NestedBlock{ 4810 "block": { 4811 Block: configschema.Block{ 4812 Attributes: map[string]*configschema.Attribute{ 4813 "attr": {Type: cty.DynamicPseudoType, Optional: true}, 4814 }, 4815 }, 4816 Nesting: configschema.NestingSingle, 4817 }, 4818 }, 4819 }, 4820 ExpectedOutput: ` # test_instance.example will be updated in-place 4821 ~ resource "test_instance" "example" { 4822 + block { 4823 + attr = "foo" 4824 } 4825 } 4826 `, 4827 }, 4828 "in-place update - unknown": { 4829 Action: plans.Update, 4830 Mode: addrs.ManagedResourceMode, 4831 Before: cty.ObjectVal(map[string]cty.Value{ 4832 "id": cty.StringVal("i-02ae66f368e8518a9"), 4833 "ami": cty.StringVal("ami-BEFORE"), 4834 "disk": cty.ObjectVal(map[string]cty.Value{ 4835 "mount_point": cty.StringVal("/var/diska"), 4836 "size": cty.StringVal("50GB"), 4837 }), 4838 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4839 "volume_type": cty.StringVal("gp2"), 4840 "new_field": cty.StringVal("new_value"), 4841 }), 4842 }), 4843 After: cty.ObjectVal(map[string]cty.Value{ 4844 "id": cty.StringVal("i-02ae66f368e8518a9"), 4845 "ami": cty.StringVal("ami-AFTER"), 4846 "disk": cty.UnknownVal(cty.Object(map[string]cty.Type{ 4847 "mount_point": cty.String, 4848 "size": cty.String, 4849 })), 4850 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4851 "volume_type": cty.StringVal("gp2"), 4852 "new_field": cty.StringVal("new_value"), 4853 }), 4854 }), 4855 RequiredReplace: cty.NewPathSet(), 4856 Schema: testSchemaPlus(configschema.NestingSingle), 4857 ExpectedOutput: ` # test_instance.example will be updated in-place 4858 ~ resource "test_instance" "example" { 4859 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4860 ~ disk = { 4861 ~ mount_point = "/var/diska" -> (known after apply) 4862 ~ size = "50GB" -> (known after apply) 4863 } -> (known after apply) 4864 id = "i-02ae66f368e8518a9" 4865 4866 # (1 unchanged block hidden) 4867 } 4868 `, 4869 }, 4870 "in-place update - modification": { 4871 Action: plans.Update, 4872 Mode: addrs.ManagedResourceMode, 4873 Before: cty.ObjectVal(map[string]cty.Value{ 4874 "id": cty.StringVal("i-02ae66f368e8518a9"), 4875 "ami": cty.StringVal("ami-BEFORE"), 4876 "disk": cty.ObjectVal(map[string]cty.Value{ 4877 "mount_point": cty.StringVal("/var/diska"), 4878 "size": cty.StringVal("50GB"), 4879 }), 4880 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4881 "volume_type": cty.StringVal("gp2"), 4882 "new_field": cty.StringVal("new_value"), 4883 }), 4884 }), 4885 After: cty.ObjectVal(map[string]cty.Value{ 4886 "id": cty.StringVal("i-02ae66f368e8518a9"), 4887 "ami": cty.StringVal("ami-AFTER"), 4888 "disk": cty.ObjectVal(map[string]cty.Value{ 4889 "mount_point": cty.StringVal("/var/diska"), 4890 "size": cty.StringVal("25GB"), 4891 }), 4892 "root_block_device": cty.ObjectVal(map[string]cty.Value{ 4893 "volume_type": cty.StringVal("gp2"), 4894 "new_field": cty.StringVal("new_value"), 4895 }), 4896 }), 4897 RequiredReplace: cty.NewPathSet(), 4898 Schema: testSchemaPlus(configschema.NestingSingle), 4899 ExpectedOutput: ` # test_instance.example will be updated in-place 4900 ~ resource "test_instance" "example" { 4901 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4902 ~ disk = { 4903 ~ size = "50GB" -> "25GB" 4904 # (1 unchanged attribute hidden) 4905 } 4906 id = "i-02ae66f368e8518a9" 4907 4908 # (1 unchanged block hidden) 4909 } 4910 `, 4911 }, 4912 } 4913 runTestCases(t, testCases) 4914 } 4915 4916 func TestResourceChange_nestedMapSensitiveSchema(t *testing.T) { 4917 testCases := map[string]testCase{ 4918 "creation from null": { 4919 Action: plans.Update, 4920 Mode: addrs.ManagedResourceMode, 4921 Before: cty.ObjectVal(map[string]cty.Value{ 4922 "id": cty.NullVal(cty.String), 4923 "ami": cty.NullVal(cty.String), 4924 "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ 4925 "mount_point": cty.String, 4926 "size": cty.String, 4927 }))), 4928 }), 4929 After: cty.ObjectVal(map[string]cty.Value{ 4930 "id": cty.StringVal("i-02ae66f368e8518a9"), 4931 "ami": cty.StringVal("ami-AFTER"), 4932 "disks": cty.MapVal(map[string]cty.Value{ 4933 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4934 "mount_point": cty.StringVal("/var/diska"), 4935 "size": cty.NullVal(cty.String), 4936 }), 4937 }), 4938 }), 4939 RequiredReplace: cty.NewPathSet(), 4940 Schema: testSchemaSensitive(configschema.NestingMap), 4941 ExpectedOutput: ` # test_instance.example will be updated in-place 4942 ~ resource "test_instance" "example" { 4943 + ami = "ami-AFTER" 4944 + disks = (sensitive value) 4945 + id = "i-02ae66f368e8518a9" 4946 } 4947 `, 4948 }, 4949 "in-place update": { 4950 Action: plans.Update, 4951 Mode: addrs.ManagedResourceMode, 4952 Before: cty.ObjectVal(map[string]cty.Value{ 4953 "id": cty.StringVal("i-02ae66f368e8518a9"), 4954 "ami": cty.StringVal("ami-BEFORE"), 4955 "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 4956 "mount_point": cty.String, 4957 "size": cty.String, 4958 })), 4959 }), 4960 After: cty.ObjectVal(map[string]cty.Value{ 4961 "id": cty.StringVal("i-02ae66f368e8518a9"), 4962 "ami": cty.StringVal("ami-AFTER"), 4963 "disks": cty.MapVal(map[string]cty.Value{ 4964 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4965 "mount_point": cty.StringVal("/var/diska"), 4966 "size": cty.NullVal(cty.String), 4967 }), 4968 }), 4969 }), 4970 RequiredReplace: cty.NewPathSet(), 4971 Schema: testSchemaSensitive(configschema.NestingMap), 4972 ExpectedOutput: ` # test_instance.example will be updated in-place 4973 ~ resource "test_instance" "example" { 4974 ~ ami = "ami-BEFORE" -> "ami-AFTER" 4975 ~ disks = (sensitive value) 4976 id = "i-02ae66f368e8518a9" 4977 } 4978 `, 4979 }, 4980 "force-new update (whole block)": { 4981 Action: plans.DeleteThenCreate, 4982 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 4983 Mode: addrs.ManagedResourceMode, 4984 Before: cty.ObjectVal(map[string]cty.Value{ 4985 "id": cty.StringVal("i-02ae66f368e8518a9"), 4986 "ami": cty.StringVal("ami-BEFORE"), 4987 "disks": cty.MapVal(map[string]cty.Value{ 4988 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4989 "mount_point": cty.StringVal("/var/diska"), 4990 "size": cty.StringVal("50GB"), 4991 }), 4992 }), 4993 }), 4994 After: cty.ObjectVal(map[string]cty.Value{ 4995 "id": cty.StringVal("i-02ae66f368e8518a9"), 4996 "ami": cty.StringVal("ami-AFTER"), 4997 "disks": cty.MapVal(map[string]cty.Value{ 4998 "disk_a": cty.ObjectVal(map[string]cty.Value{ 4999 "mount_point": cty.StringVal("/var/diska"), 5000 "size": cty.StringVal("100GB"), 5001 }), 5002 }), 5003 }), 5004 RequiredReplace: cty.NewPathSet( 5005 cty.Path{cty.GetAttrStep{Name: "disks"}}, 5006 ), 5007 Schema: testSchemaSensitive(configschema.NestingMap), 5008 ExpectedOutput: ` # test_instance.example must be replaced 5009 -/+ resource "test_instance" "example" { 5010 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5011 ~ disks = (sensitive value) # forces replacement 5012 id = "i-02ae66f368e8518a9" 5013 } 5014 `, 5015 }, 5016 "in-place update - deletion": { 5017 Action: plans.Update, 5018 Mode: addrs.ManagedResourceMode, 5019 Before: cty.ObjectVal(map[string]cty.Value{ 5020 "id": cty.StringVal("i-02ae66f368e8518a9"), 5021 "ami": cty.StringVal("ami-BEFORE"), 5022 "disks": cty.MapVal(map[string]cty.Value{ 5023 "disk_a": cty.ObjectVal(map[string]cty.Value{ 5024 "mount_point": cty.StringVal("/var/diska"), 5025 "size": cty.StringVal("50GB"), 5026 }), 5027 }), 5028 }), 5029 After: cty.ObjectVal(map[string]cty.Value{ 5030 "id": cty.StringVal("i-02ae66f368e8518a9"), 5031 "ami": cty.StringVal("ami-AFTER"), 5032 "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ 5033 "mount_point": cty.String, 5034 "size": cty.String, 5035 }))), 5036 }), 5037 RequiredReplace: cty.NewPathSet(), 5038 Schema: testSchemaSensitive(configschema.NestingMap), 5039 ExpectedOutput: ` # test_instance.example will be updated in-place 5040 ~ resource "test_instance" "example" { 5041 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5042 - disks = (sensitive value) 5043 id = "i-02ae66f368e8518a9" 5044 } 5045 `, 5046 }, 5047 "in-place update - unknown": { 5048 Action: plans.Update, 5049 Mode: addrs.ManagedResourceMode, 5050 Before: cty.ObjectVal(map[string]cty.Value{ 5051 "id": cty.StringVal("i-02ae66f368e8518a9"), 5052 "ami": cty.StringVal("ami-BEFORE"), 5053 "disks": cty.MapVal(map[string]cty.Value{ 5054 "disk_a": cty.ObjectVal(map[string]cty.Value{ 5055 "mount_point": cty.StringVal("/var/diska"), 5056 "size": cty.StringVal("50GB"), 5057 }), 5058 }), 5059 }), 5060 After: cty.ObjectVal(map[string]cty.Value{ 5061 "id": cty.StringVal("i-02ae66f368e8518a9"), 5062 "ami": cty.StringVal("ami-AFTER"), 5063 "disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ 5064 "mount_point": cty.String, 5065 "size": cty.String, 5066 }))), 5067 }), 5068 RequiredReplace: cty.NewPathSet(), 5069 Schema: testSchemaSensitive(configschema.NestingMap), 5070 ExpectedOutput: ` # test_instance.example will be updated in-place 5071 ~ resource "test_instance" "example" { 5072 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5073 ~ disks = (sensitive value) 5074 id = "i-02ae66f368e8518a9" 5075 } 5076 `, 5077 }, 5078 } 5079 runTestCases(t, testCases) 5080 } 5081 5082 func TestResourceChange_nestedListSensitiveSchema(t *testing.T) { 5083 testCases := map[string]testCase{ 5084 "creation from null": { 5085 Action: plans.Update, 5086 Mode: addrs.ManagedResourceMode, 5087 Before: cty.ObjectVal(map[string]cty.Value{ 5088 "id": cty.NullVal(cty.String), 5089 "ami": cty.NullVal(cty.String), 5090 "disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 5091 "mount_point": cty.String, 5092 "size": cty.String, 5093 }))), 5094 }), 5095 After: cty.ObjectVal(map[string]cty.Value{ 5096 "id": cty.StringVal("i-02ae66f368e8518a9"), 5097 "ami": cty.StringVal("ami-AFTER"), 5098 "disks": cty.ListVal([]cty.Value{ 5099 cty.ObjectVal(map[string]cty.Value{ 5100 "mount_point": cty.StringVal("/var/diska"), 5101 "size": cty.NullVal(cty.String), 5102 }), 5103 }), 5104 }), 5105 RequiredReplace: cty.NewPathSet(), 5106 Schema: testSchemaSensitive(configschema.NestingList), 5107 ExpectedOutput: ` # test_instance.example will be updated in-place 5108 ~ resource "test_instance" "example" { 5109 + ami = "ami-AFTER" 5110 + disks = (sensitive value) 5111 + id = "i-02ae66f368e8518a9" 5112 } 5113 `, 5114 }, 5115 "in-place update": { 5116 Action: plans.Update, 5117 Mode: addrs.ManagedResourceMode, 5118 Before: cty.ObjectVal(map[string]cty.Value{ 5119 "id": cty.StringVal("i-02ae66f368e8518a9"), 5120 "ami": cty.StringVal("ami-BEFORE"), 5121 "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 5122 "mount_point": cty.String, 5123 "size": cty.String, 5124 })), 5125 }), 5126 After: cty.ObjectVal(map[string]cty.Value{ 5127 "id": cty.StringVal("i-02ae66f368e8518a9"), 5128 "ami": cty.StringVal("ami-AFTER"), 5129 "disks": cty.ListVal([]cty.Value{ 5130 cty.ObjectVal(map[string]cty.Value{ 5131 "mount_point": cty.StringVal("/var/diska"), 5132 "size": cty.NullVal(cty.String), 5133 }), 5134 }), 5135 }), 5136 RequiredReplace: cty.NewPathSet(), 5137 Schema: testSchemaSensitive(configschema.NestingList), 5138 ExpectedOutput: ` # test_instance.example will be updated in-place 5139 ~ resource "test_instance" "example" { 5140 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5141 ~ disks = (sensitive value) 5142 id = "i-02ae66f368e8518a9" 5143 } 5144 `, 5145 }, 5146 "force-new update (whole block)": { 5147 Action: plans.DeleteThenCreate, 5148 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 5149 Mode: addrs.ManagedResourceMode, 5150 Before: cty.ObjectVal(map[string]cty.Value{ 5151 "id": cty.StringVal("i-02ae66f368e8518a9"), 5152 "ami": cty.StringVal("ami-BEFORE"), 5153 "disks": cty.ListVal([]cty.Value{ 5154 cty.ObjectVal(map[string]cty.Value{ 5155 "mount_point": cty.StringVal("/var/diska"), 5156 "size": cty.StringVal("50GB"), 5157 }), 5158 }), 5159 }), 5160 After: cty.ObjectVal(map[string]cty.Value{ 5161 "id": cty.StringVal("i-02ae66f368e8518a9"), 5162 "ami": cty.StringVal("ami-AFTER"), 5163 "disks": cty.ListVal([]cty.Value{ 5164 cty.ObjectVal(map[string]cty.Value{ 5165 "mount_point": cty.StringVal("/var/diska"), 5166 "size": cty.StringVal("100GB"), 5167 }), 5168 }), 5169 }), 5170 RequiredReplace: cty.NewPathSet( 5171 cty.Path{cty.GetAttrStep{Name: "disks"}}, 5172 ), 5173 Schema: testSchemaSensitive(configschema.NestingList), 5174 ExpectedOutput: ` # test_instance.example must be replaced 5175 -/+ resource "test_instance" "example" { 5176 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5177 ~ disks = (sensitive value) # forces replacement 5178 id = "i-02ae66f368e8518a9" 5179 } 5180 `, 5181 }, 5182 "in-place update - deletion": { 5183 Action: plans.Update, 5184 Mode: addrs.ManagedResourceMode, 5185 Before: cty.ObjectVal(map[string]cty.Value{ 5186 "id": cty.StringVal("i-02ae66f368e8518a9"), 5187 "ami": cty.StringVal("ami-BEFORE"), 5188 "disks": cty.ListVal([]cty.Value{ 5189 cty.ObjectVal(map[string]cty.Value{ 5190 "mount_point": cty.StringVal("/var/diska"), 5191 "size": cty.StringVal("50GB"), 5192 }), 5193 }), 5194 }), 5195 After: cty.ObjectVal(map[string]cty.Value{ 5196 "id": cty.StringVal("i-02ae66f368e8518a9"), 5197 "ami": cty.StringVal("ami-AFTER"), 5198 "disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 5199 "mount_point": cty.String, 5200 "size": cty.String, 5201 }))), 5202 }), 5203 RequiredReplace: cty.NewPathSet(), 5204 Schema: testSchemaSensitive(configschema.NestingList), 5205 ExpectedOutput: ` # test_instance.example will be updated in-place 5206 ~ resource "test_instance" "example" { 5207 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5208 - disks = (sensitive value) 5209 id = "i-02ae66f368e8518a9" 5210 } 5211 `, 5212 }, 5213 "in-place update - unknown": { 5214 Action: plans.Update, 5215 Mode: addrs.ManagedResourceMode, 5216 Before: cty.ObjectVal(map[string]cty.Value{ 5217 "id": cty.StringVal("i-02ae66f368e8518a9"), 5218 "ami": cty.StringVal("ami-BEFORE"), 5219 "disks": cty.ListVal([]cty.Value{ 5220 cty.ObjectVal(map[string]cty.Value{ 5221 "mount_point": cty.StringVal("/var/diska"), 5222 "size": cty.StringVal("50GB"), 5223 }), 5224 }), 5225 }), 5226 After: cty.ObjectVal(map[string]cty.Value{ 5227 "id": cty.StringVal("i-02ae66f368e8518a9"), 5228 "ami": cty.StringVal("ami-AFTER"), 5229 "disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ 5230 "mount_point": cty.String, 5231 "size": cty.String, 5232 }))), 5233 }), 5234 RequiredReplace: cty.NewPathSet(), 5235 Schema: testSchemaSensitive(configschema.NestingList), 5236 ExpectedOutput: ` # test_instance.example will be updated in-place 5237 ~ resource "test_instance" "example" { 5238 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5239 ~ disks = (sensitive value) 5240 id = "i-02ae66f368e8518a9" 5241 } 5242 `, 5243 }, 5244 } 5245 runTestCases(t, testCases) 5246 } 5247 5248 func TestResourceChange_nestedSetSensitiveSchema(t *testing.T) { 5249 testCases := map[string]testCase{ 5250 "creation from null": { 5251 Action: plans.Update, 5252 Mode: addrs.ManagedResourceMode, 5253 Before: cty.ObjectVal(map[string]cty.Value{ 5254 "id": cty.NullVal(cty.String), 5255 "ami": cty.NullVal(cty.String), 5256 "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 5257 "mount_point": cty.String, 5258 "size": cty.String, 5259 }))), 5260 }), 5261 After: cty.ObjectVal(map[string]cty.Value{ 5262 "id": cty.StringVal("i-02ae66f368e8518a9"), 5263 "ami": cty.StringVal("ami-AFTER"), 5264 "disks": cty.SetVal([]cty.Value{ 5265 cty.ObjectVal(map[string]cty.Value{ 5266 "mount_point": cty.StringVal("/var/diska"), 5267 "size": cty.NullVal(cty.String), 5268 }), 5269 }), 5270 }), 5271 RequiredReplace: cty.NewPathSet(), 5272 Schema: testSchemaSensitive(configschema.NestingSet), 5273 ExpectedOutput: ` # test_instance.example will be updated in-place 5274 ~ resource "test_instance" "example" { 5275 + ami = "ami-AFTER" 5276 + disks = (sensitive value) 5277 + id = "i-02ae66f368e8518a9" 5278 } 5279 `, 5280 }, 5281 "in-place update": { 5282 Action: plans.Update, 5283 Mode: addrs.ManagedResourceMode, 5284 Before: cty.ObjectVal(map[string]cty.Value{ 5285 "id": cty.StringVal("i-02ae66f368e8518a9"), 5286 "ami": cty.StringVal("ami-BEFORE"), 5287 "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 5288 "mount_point": cty.String, 5289 "size": cty.String, 5290 })), 5291 }), 5292 After: cty.ObjectVal(map[string]cty.Value{ 5293 "id": cty.StringVal("i-02ae66f368e8518a9"), 5294 "ami": cty.StringVal("ami-AFTER"), 5295 "disks": cty.SetVal([]cty.Value{ 5296 cty.ObjectVal(map[string]cty.Value{ 5297 "mount_point": cty.StringVal("/var/diska"), 5298 "size": cty.NullVal(cty.String), 5299 }), 5300 }), 5301 }), 5302 RequiredReplace: cty.NewPathSet(), 5303 Schema: testSchemaSensitive(configschema.NestingSet), 5304 ExpectedOutput: ` # test_instance.example will be updated in-place 5305 ~ resource "test_instance" "example" { 5306 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5307 ~ disks = (sensitive value) 5308 id = "i-02ae66f368e8518a9" 5309 } 5310 `, 5311 }, 5312 "force-new update (whole block)": { 5313 Action: plans.DeleteThenCreate, 5314 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 5315 Mode: addrs.ManagedResourceMode, 5316 Before: cty.ObjectVal(map[string]cty.Value{ 5317 "id": cty.StringVal("i-02ae66f368e8518a9"), 5318 "ami": cty.StringVal("ami-BEFORE"), 5319 "disks": cty.SetVal([]cty.Value{ 5320 cty.ObjectVal(map[string]cty.Value{ 5321 "mount_point": cty.StringVal("/var/diska"), 5322 "size": cty.StringVal("50GB"), 5323 }), 5324 }), 5325 }), 5326 After: cty.ObjectVal(map[string]cty.Value{ 5327 "id": cty.StringVal("i-02ae66f368e8518a9"), 5328 "ami": cty.StringVal("ami-AFTER"), 5329 "disks": cty.SetVal([]cty.Value{ 5330 cty.ObjectVal(map[string]cty.Value{ 5331 "mount_point": cty.StringVal("/var/diska"), 5332 "size": cty.StringVal("100GB"), 5333 }), 5334 }), 5335 }), 5336 RequiredReplace: cty.NewPathSet( 5337 cty.Path{cty.GetAttrStep{Name: "disks"}}, 5338 ), 5339 Schema: testSchemaSensitive(configschema.NestingSet), 5340 ExpectedOutput: ` # test_instance.example must be replaced 5341 -/+ resource "test_instance" "example" { 5342 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5343 ~ disks = (sensitive value) # forces replacement 5344 id = "i-02ae66f368e8518a9" 5345 } 5346 `, 5347 }, 5348 "in-place update - deletion": { 5349 Action: plans.Update, 5350 Mode: addrs.ManagedResourceMode, 5351 Before: cty.ObjectVal(map[string]cty.Value{ 5352 "id": cty.StringVal("i-02ae66f368e8518a9"), 5353 "ami": cty.StringVal("ami-BEFORE"), 5354 "disks": cty.SetVal([]cty.Value{ 5355 cty.ObjectVal(map[string]cty.Value{ 5356 "mount_point": cty.StringVal("/var/diska"), 5357 "size": cty.StringVal("50GB"), 5358 }), 5359 }), 5360 }), 5361 After: cty.ObjectVal(map[string]cty.Value{ 5362 "id": cty.StringVal("i-02ae66f368e8518a9"), 5363 "ami": cty.StringVal("ami-AFTER"), 5364 "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 5365 "mount_point": cty.String, 5366 "size": cty.String, 5367 }))), 5368 }), 5369 RequiredReplace: cty.NewPathSet(), 5370 Schema: testSchemaSensitive(configschema.NestingSet), 5371 ExpectedOutput: ` # test_instance.example will be updated in-place 5372 ~ resource "test_instance" "example" { 5373 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5374 - disks = (sensitive value) 5375 id = "i-02ae66f368e8518a9" 5376 } 5377 `, 5378 }, 5379 "in-place update - unknown": { 5380 Action: plans.Update, 5381 Mode: addrs.ManagedResourceMode, 5382 Before: cty.ObjectVal(map[string]cty.Value{ 5383 "id": cty.StringVal("i-02ae66f368e8518a9"), 5384 "ami": cty.StringVal("ami-BEFORE"), 5385 "disks": cty.SetVal([]cty.Value{ 5386 cty.ObjectVal(map[string]cty.Value{ 5387 "mount_point": cty.StringVal("/var/diska"), 5388 "size": cty.StringVal("50GB"), 5389 }), 5390 }), 5391 }), 5392 After: cty.ObjectVal(map[string]cty.Value{ 5393 "id": cty.StringVal("i-02ae66f368e8518a9"), 5394 "ami": cty.StringVal("ami-AFTER"), 5395 "disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ 5396 "mount_point": cty.String, 5397 "size": cty.String, 5398 }))), 5399 }), 5400 RequiredReplace: cty.NewPathSet(), 5401 Schema: testSchemaSensitive(configschema.NestingSet), 5402 ExpectedOutput: ` # test_instance.example will be updated in-place 5403 ~ resource "test_instance" "example" { 5404 ~ ami = "ami-BEFORE" -> "ami-AFTER" 5405 ~ disks = (sensitive value) 5406 id = "i-02ae66f368e8518a9" 5407 } 5408 `, 5409 }, 5410 } 5411 runTestCases(t, testCases) 5412 } 5413 5414 func TestResourceChange_actionReason(t *testing.T) { 5415 emptySchema := &configschema.Block{} 5416 nullVal := cty.NullVal(cty.EmptyObject) 5417 emptyVal := cty.EmptyObjectVal 5418 5419 testCases := map[string]testCase{ 5420 "delete for no particular reason": { 5421 Action: plans.Delete, 5422 ActionReason: plans.ResourceInstanceChangeNoReason, 5423 Mode: addrs.ManagedResourceMode, 5424 Before: emptyVal, 5425 After: nullVal, 5426 Schema: emptySchema, 5427 RequiredReplace: cty.NewPathSet(), 5428 ExpectedOutput: ` # test_instance.example will be destroyed 5429 - resource "test_instance" "example" {} 5430 `, 5431 }, 5432 "delete because of wrong repetition mode (NoKey)": { 5433 Action: plans.Delete, 5434 ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, 5435 Mode: addrs.ManagedResourceMode, 5436 InstanceKey: addrs.NoKey, 5437 Before: emptyVal, 5438 After: nullVal, 5439 Schema: emptySchema, 5440 RequiredReplace: cty.NewPathSet(), 5441 ExpectedOutput: ` # test_instance.example will be destroyed 5442 # (because resource uses count or for_each) 5443 - resource "test_instance" "example" {} 5444 `, 5445 }, 5446 "delete because of wrong repetition mode (IntKey)": { 5447 Action: plans.Delete, 5448 ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, 5449 Mode: addrs.ManagedResourceMode, 5450 InstanceKey: addrs.IntKey(1), 5451 Before: emptyVal, 5452 After: nullVal, 5453 Schema: emptySchema, 5454 RequiredReplace: cty.NewPathSet(), 5455 ExpectedOutput: ` # test_instance.example[1] will be destroyed 5456 # (because resource does not use count) 5457 - resource "test_instance" "example" {} 5458 `, 5459 }, 5460 "delete because of wrong repetition mode (StringKey)": { 5461 Action: plans.Delete, 5462 ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, 5463 Mode: addrs.ManagedResourceMode, 5464 InstanceKey: addrs.StringKey("a"), 5465 Before: emptyVal, 5466 After: nullVal, 5467 Schema: emptySchema, 5468 RequiredReplace: cty.NewPathSet(), 5469 ExpectedOutput: ` # test_instance.example["a"] will be destroyed 5470 # (because resource does not use for_each) 5471 - resource "test_instance" "example" {} 5472 `, 5473 }, 5474 "delete because no resource configuration": { 5475 Action: plans.Delete, 5476 ActionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig, 5477 ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.NoKey), 5478 Mode: addrs.ManagedResourceMode, 5479 Before: emptyVal, 5480 After: nullVal, 5481 Schema: emptySchema, 5482 RequiredReplace: cty.NewPathSet(), 5483 ExpectedOutput: ` # module.foo.test_instance.example will be destroyed 5484 # (because test_instance.example is not in configuration) 5485 - resource "test_instance" "example" {} 5486 `, 5487 }, 5488 "delete because no module": { 5489 Action: plans.Delete, 5490 ActionReason: plans.ResourceInstanceDeleteBecauseNoModule, 5491 ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)), 5492 Mode: addrs.ManagedResourceMode, 5493 Before: emptyVal, 5494 After: nullVal, 5495 Schema: emptySchema, 5496 RequiredReplace: cty.NewPathSet(), 5497 ExpectedOutput: ` # module.foo[1].test_instance.example will be destroyed 5498 # (because module.foo[1] is not in configuration) 5499 - resource "test_instance" "example" {} 5500 `, 5501 }, 5502 "delete because out of range for count": { 5503 Action: plans.Delete, 5504 ActionReason: plans.ResourceInstanceDeleteBecauseCountIndex, 5505 Mode: addrs.ManagedResourceMode, 5506 InstanceKey: addrs.IntKey(1), 5507 Before: emptyVal, 5508 After: nullVal, 5509 Schema: emptySchema, 5510 RequiredReplace: cty.NewPathSet(), 5511 ExpectedOutput: ` # test_instance.example[1] will be destroyed 5512 # (because index [1] is out of range for count) 5513 - resource "test_instance" "example" {} 5514 `, 5515 }, 5516 "delete because out of range for for_each": { 5517 Action: plans.Delete, 5518 ActionReason: plans.ResourceInstanceDeleteBecauseEachKey, 5519 Mode: addrs.ManagedResourceMode, 5520 InstanceKey: addrs.StringKey("boop"), 5521 Before: emptyVal, 5522 After: nullVal, 5523 Schema: emptySchema, 5524 RequiredReplace: cty.NewPathSet(), 5525 ExpectedOutput: ` # test_instance.example["boop"] will be destroyed 5526 # (because key ["boop"] is not in for_each map) 5527 - resource "test_instance" "example" {} 5528 `, 5529 }, 5530 "replace for no particular reason (delete first)": { 5531 Action: plans.DeleteThenCreate, 5532 ActionReason: plans.ResourceInstanceChangeNoReason, 5533 Mode: addrs.ManagedResourceMode, 5534 Before: emptyVal, 5535 After: nullVal, 5536 Schema: emptySchema, 5537 RequiredReplace: cty.NewPathSet(), 5538 ExpectedOutput: ` # test_instance.example must be replaced 5539 -/+ resource "test_instance" "example" {} 5540 `, 5541 }, 5542 "replace for no particular reason (create first)": { 5543 Action: plans.CreateThenDelete, 5544 ActionReason: plans.ResourceInstanceChangeNoReason, 5545 Mode: addrs.ManagedResourceMode, 5546 Before: emptyVal, 5547 After: nullVal, 5548 Schema: emptySchema, 5549 RequiredReplace: cty.NewPathSet(), 5550 ExpectedOutput: ` # test_instance.example must be replaced 5551 +/- resource "test_instance" "example" {} 5552 `, 5553 }, 5554 "replace by request (delete first)": { 5555 Action: plans.DeleteThenCreate, 5556 ActionReason: plans.ResourceInstanceReplaceByRequest, 5557 Mode: addrs.ManagedResourceMode, 5558 Before: emptyVal, 5559 After: nullVal, 5560 Schema: emptySchema, 5561 RequiredReplace: cty.NewPathSet(), 5562 ExpectedOutput: ` # test_instance.example will be replaced, as requested 5563 -/+ resource "test_instance" "example" {} 5564 `, 5565 }, 5566 "replace by request (create first)": { 5567 Action: plans.CreateThenDelete, 5568 ActionReason: plans.ResourceInstanceReplaceByRequest, 5569 Mode: addrs.ManagedResourceMode, 5570 Before: emptyVal, 5571 After: nullVal, 5572 Schema: emptySchema, 5573 RequiredReplace: cty.NewPathSet(), 5574 ExpectedOutput: ` # test_instance.example will be replaced, as requested 5575 +/- resource "test_instance" "example" {} 5576 `, 5577 }, 5578 "replace because tainted (delete first)": { 5579 Action: plans.DeleteThenCreate, 5580 ActionReason: plans.ResourceInstanceReplaceBecauseTainted, 5581 Mode: addrs.ManagedResourceMode, 5582 Before: emptyVal, 5583 After: nullVal, 5584 Schema: emptySchema, 5585 RequiredReplace: cty.NewPathSet(), 5586 ExpectedOutput: ` # test_instance.example is tainted, so must be replaced 5587 -/+ resource "test_instance" "example" {} 5588 `, 5589 }, 5590 "replace because tainted (create first)": { 5591 Action: plans.CreateThenDelete, 5592 ActionReason: plans.ResourceInstanceReplaceBecauseTainted, 5593 Mode: addrs.ManagedResourceMode, 5594 Before: emptyVal, 5595 After: nullVal, 5596 Schema: emptySchema, 5597 RequiredReplace: cty.NewPathSet(), 5598 ExpectedOutput: ` # test_instance.example is tainted, so must be replaced 5599 +/- resource "test_instance" "example" {} 5600 `, 5601 }, 5602 "replace because cannot update (delete first)": { 5603 Action: plans.DeleteThenCreate, 5604 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 5605 Mode: addrs.ManagedResourceMode, 5606 Before: emptyVal, 5607 After: nullVal, 5608 Schema: emptySchema, 5609 RequiredReplace: cty.NewPathSet(), 5610 // This one has no special message, because the fuller explanation 5611 // typically appears inline as a "# forces replacement" comment. 5612 // (not shown here) 5613 ExpectedOutput: ` # test_instance.example must be replaced 5614 -/+ resource "test_instance" "example" {} 5615 `, 5616 }, 5617 "replace because cannot update (create first)": { 5618 Action: plans.CreateThenDelete, 5619 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 5620 Mode: addrs.ManagedResourceMode, 5621 Before: emptyVal, 5622 After: nullVal, 5623 Schema: emptySchema, 5624 RequiredReplace: cty.NewPathSet(), 5625 // This one has no special message, because the fuller explanation 5626 // typically appears inline as a "# forces replacement" comment. 5627 // (not shown here) 5628 ExpectedOutput: ` # test_instance.example must be replaced 5629 +/- resource "test_instance" "example" {} 5630 `, 5631 }, 5632 } 5633 5634 runTestCases(t, testCases) 5635 } 5636 5637 func TestResourceChange_sensitiveVariable(t *testing.T) { 5638 testCases := map[string]testCase{ 5639 "creation": { 5640 Action: plans.Create, 5641 Mode: addrs.ManagedResourceMode, 5642 Before: cty.NullVal(cty.EmptyObject), 5643 After: cty.ObjectVal(map[string]cty.Value{ 5644 "id": cty.StringVal("i-02ae66f368e8518a9"), 5645 "ami": cty.StringVal("ami-123"), 5646 "map_key": cty.MapVal(map[string]cty.Value{ 5647 "breakfast": cty.NumberIntVal(800), 5648 "dinner": cty.NumberIntVal(2000), 5649 }), 5650 "map_whole": cty.MapVal(map[string]cty.Value{ 5651 "breakfast": cty.StringVal("pizza"), 5652 "dinner": cty.StringVal("pizza"), 5653 }), 5654 "list_field": cty.ListVal([]cty.Value{ 5655 cty.StringVal("hello"), 5656 cty.StringVal("friends"), 5657 cty.StringVal("!"), 5658 }), 5659 "nested_block_list": cty.ListVal([]cty.Value{ 5660 cty.ObjectVal(map[string]cty.Value{ 5661 "an_attr": cty.StringVal("secretval"), 5662 "another": cty.StringVal("not secret"), 5663 }), 5664 }), 5665 "nested_block_set": cty.ListVal([]cty.Value{ 5666 cty.ObjectVal(map[string]cty.Value{ 5667 "an_attr": cty.StringVal("secretval"), 5668 "another": cty.StringVal("not secret"), 5669 }), 5670 }), 5671 }), 5672 AfterValMarks: []cty.PathValueMarks{ 5673 { 5674 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 5675 Marks: cty.NewValueMarks(marks.Sensitive), 5676 }, 5677 { 5678 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, 5679 Marks: cty.NewValueMarks(marks.Sensitive), 5680 }, 5681 { 5682 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 5683 Marks: cty.NewValueMarks(marks.Sensitive), 5684 }, 5685 { 5686 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 5687 Marks: cty.NewValueMarks(marks.Sensitive), 5688 }, 5689 { 5690 // Nested blocks/sets will mark the whole set/block as sensitive 5691 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_list"}}, 5692 Marks: cty.NewValueMarks(marks.Sensitive), 5693 }, 5694 { 5695 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 5696 Marks: cty.NewValueMarks(marks.Sensitive), 5697 }, 5698 }, 5699 RequiredReplace: cty.NewPathSet(), 5700 Schema: &configschema.Block{ 5701 Attributes: map[string]*configschema.Attribute{ 5702 "id": {Type: cty.String, Optional: true, Computed: true}, 5703 "ami": {Type: cty.String, Optional: true}, 5704 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 5705 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 5706 "list_field": {Type: cty.List(cty.String), Optional: true}, 5707 }, 5708 BlockTypes: map[string]*configschema.NestedBlock{ 5709 "nested_block_list": { 5710 Block: configschema.Block{ 5711 Attributes: map[string]*configschema.Attribute{ 5712 "an_attr": {Type: cty.String, Optional: true}, 5713 "another": {Type: cty.String, Optional: true}, 5714 }, 5715 }, 5716 Nesting: configschema.NestingList, 5717 }, 5718 "nested_block_set": { 5719 Block: configschema.Block{ 5720 Attributes: map[string]*configschema.Attribute{ 5721 "an_attr": {Type: cty.String, Optional: true}, 5722 "another": {Type: cty.String, Optional: true}, 5723 }, 5724 }, 5725 Nesting: configschema.NestingSet, 5726 }, 5727 }, 5728 }, 5729 ExpectedOutput: ` # test_instance.example will be created 5730 + resource "test_instance" "example" { 5731 + ami = (sensitive value) 5732 + id = "i-02ae66f368e8518a9" 5733 + list_field = [ 5734 + "hello", 5735 + (sensitive value), 5736 + "!", 5737 ] 5738 + map_key = { 5739 + "breakfast" = 800 5740 + "dinner" = (sensitive value) 5741 } 5742 + map_whole = (sensitive value) 5743 5744 + nested_block_list { 5745 # At least one attribute in this block is (or was) sensitive, 5746 # so its contents will not be displayed. 5747 } 5748 5749 + nested_block_set { 5750 # At least one attribute in this block is (or was) sensitive, 5751 # so its contents will not be displayed. 5752 } 5753 } 5754 `, 5755 }, 5756 "in-place update - before sensitive": { 5757 Action: plans.Update, 5758 Mode: addrs.ManagedResourceMode, 5759 Before: cty.ObjectVal(map[string]cty.Value{ 5760 "id": cty.StringVal("i-02ae66f368e8518a9"), 5761 "ami": cty.StringVal("ami-BEFORE"), 5762 "special": cty.BoolVal(true), 5763 "some_number": cty.NumberIntVal(1), 5764 "list_field": cty.ListVal([]cty.Value{ 5765 cty.StringVal("hello"), 5766 cty.StringVal("friends"), 5767 cty.StringVal("!"), 5768 }), 5769 "map_key": cty.MapVal(map[string]cty.Value{ 5770 "breakfast": cty.NumberIntVal(800), 5771 "dinner": cty.NumberIntVal(2000), // sensitive key 5772 }), 5773 "map_whole": cty.MapVal(map[string]cty.Value{ 5774 "breakfast": cty.StringVal("pizza"), 5775 "dinner": cty.StringVal("pizza"), 5776 }), 5777 "nested_block": cty.ListVal([]cty.Value{ 5778 cty.ObjectVal(map[string]cty.Value{ 5779 "an_attr": cty.StringVal("secretval"), 5780 }), 5781 }), 5782 "nested_block_set": cty.ListVal([]cty.Value{ 5783 cty.ObjectVal(map[string]cty.Value{ 5784 "an_attr": cty.StringVal("secretval"), 5785 }), 5786 }), 5787 }), 5788 After: cty.ObjectVal(map[string]cty.Value{ 5789 "id": cty.StringVal("i-02ae66f368e8518a9"), 5790 "ami": cty.StringVal("ami-AFTER"), 5791 "special": cty.BoolVal(false), 5792 "some_number": cty.NumberIntVal(2), 5793 "list_field": cty.ListVal([]cty.Value{ 5794 cty.StringVal("hello"), 5795 cty.StringVal("friends"), 5796 cty.StringVal("."), 5797 }), 5798 "map_key": cty.MapVal(map[string]cty.Value{ 5799 "breakfast": cty.NumberIntVal(800), 5800 "dinner": cty.NumberIntVal(1900), 5801 }), 5802 "map_whole": cty.MapVal(map[string]cty.Value{ 5803 "breakfast": cty.StringVal("cereal"), 5804 "dinner": cty.StringVal("pizza"), 5805 }), 5806 "nested_block": cty.ListVal([]cty.Value{ 5807 cty.ObjectVal(map[string]cty.Value{ 5808 "an_attr": cty.StringVal("changed"), 5809 }), 5810 }), 5811 "nested_block_set": cty.ListVal([]cty.Value{ 5812 cty.ObjectVal(map[string]cty.Value{ 5813 "an_attr": cty.StringVal("changed"), 5814 }), 5815 }), 5816 }), 5817 BeforeValMarks: []cty.PathValueMarks{ 5818 { 5819 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 5820 Marks: cty.NewValueMarks(marks.Sensitive), 5821 }, 5822 { 5823 Path: cty.Path{cty.GetAttrStep{Name: "special"}}, 5824 Marks: cty.NewValueMarks(marks.Sensitive), 5825 }, 5826 { 5827 Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, 5828 Marks: cty.NewValueMarks(marks.Sensitive), 5829 }, 5830 { 5831 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, 5832 Marks: cty.NewValueMarks(marks.Sensitive), 5833 }, 5834 { 5835 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 5836 Marks: cty.NewValueMarks(marks.Sensitive), 5837 }, 5838 { 5839 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 5840 Marks: cty.NewValueMarks(marks.Sensitive), 5841 }, 5842 { 5843 Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, 5844 Marks: cty.NewValueMarks(marks.Sensitive), 5845 }, 5846 { 5847 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 5848 Marks: cty.NewValueMarks(marks.Sensitive), 5849 }, 5850 }, 5851 RequiredReplace: cty.NewPathSet(), 5852 Schema: &configschema.Block{ 5853 Attributes: map[string]*configschema.Attribute{ 5854 "id": {Type: cty.String, Optional: true, Computed: true}, 5855 "ami": {Type: cty.String, Optional: true}, 5856 "list_field": {Type: cty.List(cty.String), Optional: true}, 5857 "special": {Type: cty.Bool, Optional: true}, 5858 "some_number": {Type: cty.Number, Optional: true}, 5859 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 5860 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 5861 }, 5862 BlockTypes: map[string]*configschema.NestedBlock{ 5863 "nested_block": { 5864 Block: configschema.Block{ 5865 Attributes: map[string]*configschema.Attribute{ 5866 "an_attr": {Type: cty.String, Optional: true}, 5867 }, 5868 }, 5869 Nesting: configschema.NestingList, 5870 }, 5871 "nested_block_set": { 5872 Block: configschema.Block{ 5873 Attributes: map[string]*configschema.Attribute{ 5874 "an_attr": {Type: cty.String, Optional: true}, 5875 }, 5876 }, 5877 Nesting: configschema.NestingSet, 5878 }, 5879 }, 5880 }, 5881 ExpectedOutput: ` # test_instance.example will be updated in-place 5882 ~ resource "test_instance" "example" { 5883 # Warning: this attribute value will no longer be marked as sensitive 5884 # after applying this change. 5885 ~ ami = (sensitive value) 5886 id = "i-02ae66f368e8518a9" 5887 ~ list_field = [ 5888 # (1 unchanged element hidden) 5889 "friends", 5890 - (sensitive value), 5891 + ".", 5892 ] 5893 ~ map_key = { 5894 # Warning: this attribute value will no longer be marked as sensitive 5895 # after applying this change. 5896 ~ "dinner" = (sensitive value) 5897 # (1 unchanged element hidden) 5898 } 5899 # Warning: this attribute value will no longer be marked as sensitive 5900 # after applying this change. 5901 ~ map_whole = (sensitive value) 5902 # Warning: this attribute value will no longer be marked as sensitive 5903 # after applying this change. 5904 ~ some_number = (sensitive value) 5905 # Warning: this attribute value will no longer be marked as sensitive 5906 # after applying this change. 5907 ~ special = (sensitive value) 5908 5909 # Warning: this block will no longer be marked as sensitive 5910 # after applying this change. 5911 ~ nested_block { 5912 # At least one attribute in this block is (or was) sensitive, 5913 # so its contents will not be displayed. 5914 } 5915 5916 # Warning: this block will no longer be marked as sensitive 5917 # after applying this change. 5918 ~ nested_block_set { 5919 # At least one attribute in this block is (or was) sensitive, 5920 # so its contents will not be displayed. 5921 } 5922 } 5923 `, 5924 }, 5925 "in-place update - after sensitive": { 5926 Action: plans.Update, 5927 Mode: addrs.ManagedResourceMode, 5928 Before: cty.ObjectVal(map[string]cty.Value{ 5929 "id": cty.StringVal("i-02ae66f368e8518a9"), 5930 "list_field": cty.ListVal([]cty.Value{ 5931 cty.StringVal("hello"), 5932 cty.StringVal("friends"), 5933 }), 5934 "map_key": cty.MapVal(map[string]cty.Value{ 5935 "breakfast": cty.NumberIntVal(800), 5936 "dinner": cty.NumberIntVal(2000), // sensitive key 5937 }), 5938 "map_whole": cty.MapVal(map[string]cty.Value{ 5939 "breakfast": cty.StringVal("pizza"), 5940 "dinner": cty.StringVal("pizza"), 5941 }), 5942 "nested_block_single": cty.ObjectVal(map[string]cty.Value{ 5943 "an_attr": cty.StringVal("original"), 5944 }), 5945 }), 5946 After: cty.ObjectVal(map[string]cty.Value{ 5947 "id": cty.StringVal("i-02ae66f368e8518a9"), 5948 "list_field": cty.ListVal([]cty.Value{ 5949 cty.StringVal("goodbye"), 5950 cty.StringVal("friends"), 5951 }), 5952 "map_key": cty.MapVal(map[string]cty.Value{ 5953 "breakfast": cty.NumberIntVal(700), 5954 "dinner": cty.NumberIntVal(2100), // sensitive key 5955 }), 5956 "map_whole": cty.MapVal(map[string]cty.Value{ 5957 "breakfast": cty.StringVal("cereal"), 5958 "dinner": cty.StringVal("pizza"), 5959 }), 5960 "nested_block_single": cty.ObjectVal(map[string]cty.Value{ 5961 "an_attr": cty.StringVal("changed"), 5962 }), 5963 }), 5964 AfterValMarks: []cty.PathValueMarks{ 5965 { 5966 Path: cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}}, 5967 Marks: cty.NewValueMarks(marks.Sensitive), 5968 }, 5969 { 5970 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, 5971 Marks: cty.NewValueMarks(marks.Sensitive), 5972 }, 5973 { 5974 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 5975 Marks: cty.NewValueMarks(marks.Sensitive), 5976 }, 5977 { 5978 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 5979 Marks: cty.NewValueMarks(marks.Sensitive), 5980 }, 5981 { 5982 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_single"}}, 5983 Marks: cty.NewValueMarks(marks.Sensitive), 5984 }, 5985 }, 5986 RequiredReplace: cty.NewPathSet(), 5987 Schema: &configschema.Block{ 5988 Attributes: map[string]*configschema.Attribute{ 5989 "id": {Type: cty.String, Optional: true, Computed: true}, 5990 "list_field": {Type: cty.List(cty.String), Optional: true}, 5991 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 5992 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 5993 }, 5994 BlockTypes: map[string]*configschema.NestedBlock{ 5995 "nested_block_single": { 5996 Block: configschema.Block{ 5997 Attributes: map[string]*configschema.Attribute{ 5998 "an_attr": {Type: cty.String, Optional: true}, 5999 }, 6000 }, 6001 Nesting: configschema.NestingSingle, 6002 }, 6003 }, 6004 }, 6005 ExpectedOutput: ` # test_instance.example will be updated in-place 6006 ~ resource "test_instance" "example" { 6007 id = "i-02ae66f368e8518a9" 6008 ~ list_field = [ 6009 - "hello", 6010 + (sensitive value), 6011 "friends", 6012 ] 6013 ~ map_key = { 6014 ~ "breakfast" = 800 -> 700 6015 # Warning: this attribute value will be marked as sensitive and will not 6016 # display in UI output after applying this change. 6017 ~ "dinner" = (sensitive value) 6018 } 6019 # Warning: this attribute value will be marked as sensitive and will not 6020 # display in UI output after applying this change. 6021 ~ map_whole = (sensitive value) 6022 6023 # Warning: this block will be marked as sensitive and will not 6024 # display in UI output after applying this change. 6025 ~ nested_block_single { 6026 # At least one attribute in this block is (or was) sensitive, 6027 # so its contents will not be displayed. 6028 } 6029 } 6030 `, 6031 }, 6032 "in-place update - both sensitive": { 6033 Action: plans.Update, 6034 Mode: addrs.ManagedResourceMode, 6035 Before: cty.ObjectVal(map[string]cty.Value{ 6036 "id": cty.StringVal("i-02ae66f368e8518a9"), 6037 "ami": cty.StringVal("ami-BEFORE"), 6038 "list_field": cty.ListVal([]cty.Value{ 6039 cty.StringVal("hello"), 6040 cty.StringVal("friends"), 6041 }), 6042 "map_key": cty.MapVal(map[string]cty.Value{ 6043 "breakfast": cty.NumberIntVal(800), 6044 "dinner": cty.NumberIntVal(2000), // sensitive key 6045 }), 6046 "map_whole": cty.MapVal(map[string]cty.Value{ 6047 "breakfast": cty.StringVal("pizza"), 6048 "dinner": cty.StringVal("pizza"), 6049 }), 6050 "nested_block_map": cty.MapVal(map[string]cty.Value{ 6051 "foo": cty.ObjectVal(map[string]cty.Value{ 6052 "an_attr": cty.StringVal("original"), 6053 }), 6054 }), 6055 }), 6056 After: cty.ObjectVal(map[string]cty.Value{ 6057 "id": cty.StringVal("i-02ae66f368e8518a9"), 6058 "ami": cty.StringVal("ami-AFTER"), 6059 "list_field": cty.ListVal([]cty.Value{ 6060 cty.StringVal("goodbye"), 6061 cty.StringVal("friends"), 6062 }), 6063 "map_key": cty.MapVal(map[string]cty.Value{ 6064 "breakfast": cty.NumberIntVal(800), 6065 "dinner": cty.NumberIntVal(1800), // sensitive key 6066 }), 6067 "map_whole": cty.MapVal(map[string]cty.Value{ 6068 "breakfast": cty.StringVal("cereal"), 6069 "dinner": cty.StringVal("pizza"), 6070 }), 6071 "nested_block_map": cty.MapVal(map[string]cty.Value{ 6072 "foo": cty.ObjectVal(map[string]cty.Value{ 6073 "an_attr": cty.UnknownVal(cty.String), 6074 }), 6075 }), 6076 }), 6077 BeforeValMarks: []cty.PathValueMarks{ 6078 { 6079 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 6080 Marks: cty.NewValueMarks(marks.Sensitive), 6081 }, 6082 { 6083 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, 6084 Marks: cty.NewValueMarks(marks.Sensitive), 6085 }, 6086 { 6087 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 6088 Marks: cty.NewValueMarks(marks.Sensitive), 6089 }, 6090 { 6091 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 6092 Marks: cty.NewValueMarks(marks.Sensitive), 6093 }, 6094 { 6095 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, 6096 Marks: cty.NewValueMarks(marks.Sensitive), 6097 }, 6098 }, 6099 AfterValMarks: []cty.PathValueMarks{ 6100 { 6101 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 6102 Marks: cty.NewValueMarks(marks.Sensitive), 6103 }, 6104 { 6105 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, 6106 Marks: cty.NewValueMarks(marks.Sensitive), 6107 }, 6108 { 6109 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 6110 Marks: cty.NewValueMarks(marks.Sensitive), 6111 }, 6112 { 6113 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 6114 Marks: cty.NewValueMarks(marks.Sensitive), 6115 }, 6116 { 6117 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, 6118 Marks: cty.NewValueMarks(marks.Sensitive), 6119 }, 6120 }, 6121 RequiredReplace: cty.NewPathSet(), 6122 Schema: &configschema.Block{ 6123 Attributes: map[string]*configschema.Attribute{ 6124 "id": {Type: cty.String, Optional: true, Computed: true}, 6125 "ami": {Type: cty.String, Optional: true}, 6126 "list_field": {Type: cty.List(cty.String), Optional: true}, 6127 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 6128 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 6129 }, 6130 BlockTypes: map[string]*configschema.NestedBlock{ 6131 "nested_block_map": { 6132 Block: configschema.Block{ 6133 Attributes: map[string]*configschema.Attribute{ 6134 "an_attr": {Type: cty.String, Optional: true}, 6135 }, 6136 }, 6137 Nesting: configschema.NestingMap, 6138 }, 6139 }, 6140 }, 6141 ExpectedOutput: ` # test_instance.example will be updated in-place 6142 ~ resource "test_instance" "example" { 6143 ~ ami = (sensitive value) 6144 id = "i-02ae66f368e8518a9" 6145 ~ list_field = [ 6146 - (sensitive value), 6147 + (sensitive value), 6148 "friends", 6149 ] 6150 ~ map_key = { 6151 ~ "dinner" = (sensitive value) 6152 # (1 unchanged element hidden) 6153 } 6154 ~ map_whole = (sensitive value) 6155 6156 ~ nested_block_map { 6157 # At least one attribute in this block is (or was) sensitive, 6158 # so its contents will not be displayed. 6159 } 6160 } 6161 `, 6162 }, 6163 "in-place update - value unchanged, sensitivity changes": { 6164 Action: plans.Update, 6165 Mode: addrs.ManagedResourceMode, 6166 Before: cty.ObjectVal(map[string]cty.Value{ 6167 "id": cty.StringVal("i-02ae66f368e8518a9"), 6168 "ami": cty.StringVal("ami-BEFORE"), 6169 "special": cty.BoolVal(true), 6170 "some_number": cty.NumberIntVal(1), 6171 "list_field": cty.ListVal([]cty.Value{ 6172 cty.StringVal("hello"), 6173 cty.StringVal("friends"), 6174 cty.StringVal("!"), 6175 }), 6176 "map_key": cty.MapVal(map[string]cty.Value{ 6177 "breakfast": cty.NumberIntVal(800), 6178 "dinner": cty.NumberIntVal(2000), // sensitive key 6179 }), 6180 "map_whole": cty.MapVal(map[string]cty.Value{ 6181 "breakfast": cty.StringVal("pizza"), 6182 "dinner": cty.StringVal("pizza"), 6183 }), 6184 "nested_block": cty.ListVal([]cty.Value{ 6185 cty.ObjectVal(map[string]cty.Value{ 6186 "an_attr": cty.StringVal("secretval"), 6187 }), 6188 }), 6189 "nested_block_set": cty.ListVal([]cty.Value{ 6190 cty.ObjectVal(map[string]cty.Value{ 6191 "an_attr": cty.StringVal("secretval"), 6192 }), 6193 }), 6194 }), 6195 After: cty.ObjectVal(map[string]cty.Value{ 6196 "id": cty.StringVal("i-02ae66f368e8518a9"), 6197 "ami": cty.StringVal("ami-BEFORE"), 6198 "special": cty.BoolVal(true), 6199 "some_number": cty.NumberIntVal(1), 6200 "list_field": cty.ListVal([]cty.Value{ 6201 cty.StringVal("hello"), 6202 cty.StringVal("friends"), 6203 cty.StringVal("!"), 6204 }), 6205 "map_key": cty.MapVal(map[string]cty.Value{ 6206 "breakfast": cty.NumberIntVal(800), 6207 "dinner": cty.NumberIntVal(2000), // sensitive key 6208 }), 6209 "map_whole": cty.MapVal(map[string]cty.Value{ 6210 "breakfast": cty.StringVal("pizza"), 6211 "dinner": cty.StringVal("pizza"), 6212 }), 6213 "nested_block": cty.ListVal([]cty.Value{ 6214 cty.ObjectVal(map[string]cty.Value{ 6215 "an_attr": cty.StringVal("secretval"), 6216 }), 6217 }), 6218 "nested_block_set": cty.ListVal([]cty.Value{ 6219 cty.ObjectVal(map[string]cty.Value{ 6220 "an_attr": cty.StringVal("secretval"), 6221 }), 6222 }), 6223 }), 6224 BeforeValMarks: []cty.PathValueMarks{ 6225 { 6226 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 6227 Marks: cty.NewValueMarks(marks.Sensitive), 6228 }, 6229 { 6230 Path: cty.Path{cty.GetAttrStep{Name: "special"}}, 6231 Marks: cty.NewValueMarks(marks.Sensitive), 6232 }, 6233 { 6234 Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, 6235 Marks: cty.NewValueMarks(marks.Sensitive), 6236 }, 6237 { 6238 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, 6239 Marks: cty.NewValueMarks(marks.Sensitive), 6240 }, 6241 { 6242 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 6243 Marks: cty.NewValueMarks(marks.Sensitive), 6244 }, 6245 { 6246 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 6247 Marks: cty.NewValueMarks(marks.Sensitive), 6248 }, 6249 { 6250 Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, 6251 Marks: cty.NewValueMarks(marks.Sensitive), 6252 }, 6253 { 6254 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 6255 Marks: cty.NewValueMarks(marks.Sensitive), 6256 }, 6257 }, 6258 RequiredReplace: cty.NewPathSet(), 6259 Schema: &configschema.Block{ 6260 Attributes: map[string]*configschema.Attribute{ 6261 "id": {Type: cty.String, Optional: true, Computed: true}, 6262 "ami": {Type: cty.String, Optional: true}, 6263 "list_field": {Type: cty.List(cty.String), Optional: true}, 6264 "special": {Type: cty.Bool, Optional: true}, 6265 "some_number": {Type: cty.Number, Optional: true}, 6266 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 6267 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 6268 }, 6269 BlockTypes: map[string]*configschema.NestedBlock{ 6270 "nested_block": { 6271 Block: configschema.Block{ 6272 Attributes: map[string]*configschema.Attribute{ 6273 "an_attr": {Type: cty.String, Optional: true}, 6274 }, 6275 }, 6276 Nesting: configschema.NestingList, 6277 }, 6278 "nested_block_set": { 6279 Block: configschema.Block{ 6280 Attributes: map[string]*configschema.Attribute{ 6281 "an_attr": {Type: cty.String, Optional: true}, 6282 }, 6283 }, 6284 Nesting: configschema.NestingSet, 6285 }, 6286 }, 6287 }, 6288 ExpectedOutput: ` # test_instance.example will be updated in-place 6289 ~ resource "test_instance" "example" { 6290 # Warning: this attribute value will no longer be marked as sensitive 6291 # after applying this change. The value is unchanged. 6292 ~ ami = (sensitive value) 6293 id = "i-02ae66f368e8518a9" 6294 ~ list_field = [ 6295 # (1 unchanged element hidden) 6296 "friends", 6297 - (sensitive value), 6298 + "!", 6299 ] 6300 ~ map_key = { 6301 # Warning: this attribute value will no longer be marked as sensitive 6302 # after applying this change. The value is unchanged. 6303 ~ "dinner" = (sensitive value) 6304 # (1 unchanged element hidden) 6305 } 6306 # Warning: this attribute value will no longer be marked as sensitive 6307 # after applying this change. The value is unchanged. 6308 ~ map_whole = (sensitive value) 6309 # Warning: this attribute value will no longer be marked as sensitive 6310 # after applying this change. The value is unchanged. 6311 ~ some_number = (sensitive value) 6312 # Warning: this attribute value will no longer be marked as sensitive 6313 # after applying this change. The value is unchanged. 6314 ~ special = (sensitive value) 6315 6316 # Warning: this block will no longer be marked as sensitive 6317 # after applying this change. 6318 ~ nested_block { 6319 # At least one attribute in this block is (or was) sensitive, 6320 # so its contents will not be displayed. 6321 } 6322 6323 # Warning: this block will no longer be marked as sensitive 6324 # after applying this change. 6325 ~ nested_block_set { 6326 # At least one attribute in this block is (or was) sensitive, 6327 # so its contents will not be displayed. 6328 } 6329 } 6330 `, 6331 }, 6332 "deletion": { 6333 Action: plans.Delete, 6334 Mode: addrs.ManagedResourceMode, 6335 Before: cty.ObjectVal(map[string]cty.Value{ 6336 "id": cty.StringVal("i-02ae66f368e8518a9"), 6337 "ami": cty.StringVal("ami-BEFORE"), 6338 "list_field": cty.ListVal([]cty.Value{ 6339 cty.StringVal("hello"), 6340 cty.StringVal("friends"), 6341 }), 6342 "map_key": cty.MapVal(map[string]cty.Value{ 6343 "breakfast": cty.NumberIntVal(800), 6344 "dinner": cty.NumberIntVal(2000), // sensitive key 6345 }), 6346 "map_whole": cty.MapVal(map[string]cty.Value{ 6347 "breakfast": cty.StringVal("pizza"), 6348 "dinner": cty.StringVal("pizza"), 6349 }), 6350 "nested_block": cty.ListVal([]cty.Value{ 6351 cty.ObjectVal(map[string]cty.Value{ 6352 "an_attr": cty.StringVal("secret"), 6353 "another": cty.StringVal("not secret"), 6354 }), 6355 }), 6356 "nested_block_set": cty.ListVal([]cty.Value{ 6357 cty.ObjectVal(map[string]cty.Value{ 6358 "an_attr": cty.StringVal("secret"), 6359 "another": cty.StringVal("not secret"), 6360 }), 6361 }), 6362 }), 6363 After: cty.NullVal(cty.EmptyObject), 6364 BeforeValMarks: []cty.PathValueMarks{ 6365 { 6366 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 6367 Marks: cty.NewValueMarks(marks.Sensitive), 6368 }, 6369 { 6370 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, 6371 Marks: cty.NewValueMarks(marks.Sensitive), 6372 }, 6373 { 6374 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 6375 Marks: cty.NewValueMarks(marks.Sensitive), 6376 }, 6377 { 6378 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 6379 Marks: cty.NewValueMarks(marks.Sensitive), 6380 }, 6381 { 6382 Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, 6383 Marks: cty.NewValueMarks(marks.Sensitive), 6384 }, 6385 { 6386 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 6387 Marks: cty.NewValueMarks(marks.Sensitive), 6388 }, 6389 }, 6390 RequiredReplace: cty.NewPathSet(), 6391 Schema: &configschema.Block{ 6392 Attributes: map[string]*configschema.Attribute{ 6393 "id": {Type: cty.String, Optional: true, Computed: true}, 6394 "ami": {Type: cty.String, Optional: true}, 6395 "list_field": {Type: cty.List(cty.String), Optional: true}, 6396 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 6397 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 6398 }, 6399 BlockTypes: map[string]*configschema.NestedBlock{ 6400 "nested_block_set": { 6401 Block: configschema.Block{ 6402 Attributes: map[string]*configschema.Attribute{ 6403 "an_attr": {Type: cty.String, Optional: true}, 6404 "another": {Type: cty.String, Optional: true}, 6405 }, 6406 }, 6407 Nesting: configschema.NestingSet, 6408 }, 6409 }, 6410 }, 6411 ExpectedOutput: ` # test_instance.example will be destroyed 6412 - resource "test_instance" "example" { 6413 - ami = (sensitive value) -> null 6414 - id = "i-02ae66f368e8518a9" -> null 6415 - list_field = [ 6416 - "hello", 6417 - (sensitive value), 6418 ] -> null 6419 - map_key = { 6420 - "breakfast" = 800 6421 - "dinner" = (sensitive value) 6422 } -> null 6423 - map_whole = (sensitive value) -> null 6424 6425 - nested_block_set { 6426 # At least one attribute in this block is (or was) sensitive, 6427 # so its contents will not be displayed. 6428 } 6429 } 6430 `, 6431 }, 6432 "update with sensitive value forcing replacement": { 6433 Action: plans.DeleteThenCreate, 6434 Mode: addrs.ManagedResourceMode, 6435 Before: cty.ObjectVal(map[string]cty.Value{ 6436 "id": cty.StringVal("i-02ae66f368e8518a9"), 6437 "ami": cty.StringVal("ami-BEFORE"), 6438 "nested_block_set": cty.SetVal([]cty.Value{ 6439 cty.ObjectVal(map[string]cty.Value{ 6440 "an_attr": cty.StringVal("secret"), 6441 }), 6442 }), 6443 }), 6444 After: cty.ObjectVal(map[string]cty.Value{ 6445 "id": cty.StringVal("i-02ae66f368e8518a9"), 6446 "ami": cty.StringVal("ami-AFTER"), 6447 "nested_block_set": cty.SetVal([]cty.Value{ 6448 cty.ObjectVal(map[string]cty.Value{ 6449 "an_attr": cty.StringVal("changed"), 6450 }), 6451 }), 6452 }), 6453 BeforeValMarks: []cty.PathValueMarks{ 6454 { 6455 Path: cty.GetAttrPath("ami"), 6456 Marks: cty.NewValueMarks(marks.Sensitive), 6457 }, 6458 { 6459 Path: cty.GetAttrPath("nested_block_set"), 6460 Marks: cty.NewValueMarks(marks.Sensitive), 6461 }, 6462 }, 6463 AfterValMarks: []cty.PathValueMarks{ 6464 { 6465 Path: cty.GetAttrPath("ami"), 6466 Marks: cty.NewValueMarks(marks.Sensitive), 6467 }, 6468 { 6469 Path: cty.GetAttrPath("nested_block_set"), 6470 Marks: cty.NewValueMarks(marks.Sensitive), 6471 }, 6472 }, 6473 Schema: &configschema.Block{ 6474 Attributes: map[string]*configschema.Attribute{ 6475 "id": {Type: cty.String, Optional: true, Computed: true}, 6476 "ami": {Type: cty.String, Optional: true}, 6477 }, 6478 BlockTypes: map[string]*configschema.NestedBlock{ 6479 "nested_block_set": { 6480 Block: configschema.Block{ 6481 Attributes: map[string]*configschema.Attribute{ 6482 "an_attr": {Type: cty.String, Required: true}, 6483 }, 6484 }, 6485 Nesting: configschema.NestingSet, 6486 }, 6487 }, 6488 }, 6489 RequiredReplace: cty.NewPathSet( 6490 cty.GetAttrPath("ami"), 6491 cty.GetAttrPath("nested_block_set"), 6492 ), 6493 ExpectedOutput: ` # test_instance.example must be replaced 6494 -/+ resource "test_instance" "example" { 6495 ~ ami = (sensitive value) # forces replacement 6496 id = "i-02ae66f368e8518a9" 6497 6498 ~ nested_block_set { # forces replacement 6499 # At least one attribute in this block is (or was) sensitive, 6500 # so its contents will not be displayed. 6501 } 6502 } 6503 `, 6504 }, 6505 "update with sensitive attribute forcing replacement": { 6506 Action: plans.DeleteThenCreate, 6507 Mode: addrs.ManagedResourceMode, 6508 Before: cty.ObjectVal(map[string]cty.Value{ 6509 "id": cty.StringVal("i-02ae66f368e8518a9"), 6510 "ami": cty.StringVal("ami-BEFORE"), 6511 }), 6512 After: cty.ObjectVal(map[string]cty.Value{ 6513 "id": cty.StringVal("i-02ae66f368e8518a9"), 6514 "ami": cty.StringVal("ami-AFTER"), 6515 }), 6516 Schema: &configschema.Block{ 6517 Attributes: map[string]*configschema.Attribute{ 6518 "id": {Type: cty.String, Optional: true, Computed: true}, 6519 "ami": {Type: cty.String, Optional: true, Computed: true, Sensitive: true}, 6520 }, 6521 }, 6522 RequiredReplace: cty.NewPathSet( 6523 cty.GetAttrPath("ami"), 6524 ), 6525 ExpectedOutput: ` # test_instance.example must be replaced 6526 -/+ resource "test_instance" "example" { 6527 ~ ami = (sensitive value) # forces replacement 6528 id = "i-02ae66f368e8518a9" 6529 } 6530 `, 6531 }, 6532 "update with sensitive nested type attribute forcing replacement": { 6533 Action: plans.DeleteThenCreate, 6534 Mode: addrs.ManagedResourceMode, 6535 Before: cty.ObjectVal(map[string]cty.Value{ 6536 "id": cty.StringVal("i-02ae66f368e8518a9"), 6537 "conn_info": cty.ObjectVal(map[string]cty.Value{ 6538 "user": cty.StringVal("not-secret"), 6539 "password": cty.StringVal("top-secret"), 6540 }), 6541 }), 6542 After: cty.ObjectVal(map[string]cty.Value{ 6543 "id": cty.StringVal("i-02ae66f368e8518a9"), 6544 "conn_info": cty.ObjectVal(map[string]cty.Value{ 6545 "user": cty.StringVal("not-secret"), 6546 "password": cty.StringVal("new-secret"), 6547 }), 6548 }), 6549 Schema: &configschema.Block{ 6550 Attributes: map[string]*configschema.Attribute{ 6551 "id": {Type: cty.String, Optional: true, Computed: true}, 6552 "conn_info": { 6553 NestedType: &configschema.Object{ 6554 Nesting: configschema.NestingSingle, 6555 Attributes: map[string]*configschema.Attribute{ 6556 "user": {Type: cty.String, Optional: true}, 6557 "password": {Type: cty.String, Optional: true, Sensitive: true}, 6558 }, 6559 }, 6560 }, 6561 }, 6562 }, 6563 RequiredReplace: cty.NewPathSet( 6564 cty.GetAttrPath("conn_info"), 6565 cty.GetAttrPath("password"), 6566 ), 6567 ExpectedOutput: ` # test_instance.example must be replaced 6568 -/+ resource "test_instance" "example" { 6569 ~ conn_info = { # forces replacement 6570 ~ password = (sensitive value) 6571 # (1 unchanged attribute hidden) 6572 } 6573 id = "i-02ae66f368e8518a9" 6574 } 6575 `, 6576 }, 6577 } 6578 runTestCases(t, testCases) 6579 } 6580 6581 func TestResourceChange_moved(t *testing.T) { 6582 prevRunAddr := addrs.Resource{ 6583 Mode: addrs.ManagedResourceMode, 6584 Type: "test_instance", 6585 Name: "previous", 6586 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 6587 6588 testCases := map[string]testCase{ 6589 "moved and updated": { 6590 PrevRunAddr: prevRunAddr, 6591 Action: plans.Update, 6592 Mode: addrs.ManagedResourceMode, 6593 Before: cty.ObjectVal(map[string]cty.Value{ 6594 "id": cty.StringVal("12345"), 6595 "foo": cty.StringVal("hello"), 6596 "bar": cty.StringVal("baz"), 6597 }), 6598 After: cty.ObjectVal(map[string]cty.Value{ 6599 "id": cty.StringVal("12345"), 6600 "foo": cty.StringVal("hello"), 6601 "bar": cty.StringVal("boop"), 6602 }), 6603 Schema: &configschema.Block{ 6604 Attributes: map[string]*configschema.Attribute{ 6605 "id": {Type: cty.String, Computed: true}, 6606 "foo": {Type: cty.String, Optional: true}, 6607 "bar": {Type: cty.String, Optional: true}, 6608 }, 6609 }, 6610 RequiredReplace: cty.NewPathSet(), 6611 ExpectedOutput: ` # test_instance.example will be updated in-place 6612 # (moved from test_instance.previous) 6613 ~ resource "test_instance" "example" { 6614 ~ bar = "baz" -> "boop" 6615 id = "12345" 6616 # (1 unchanged attribute hidden) 6617 } 6618 `, 6619 }, 6620 "moved without changes": { 6621 PrevRunAddr: prevRunAddr, 6622 Action: plans.NoOp, 6623 Mode: addrs.ManagedResourceMode, 6624 Before: cty.ObjectVal(map[string]cty.Value{ 6625 "id": cty.StringVal("12345"), 6626 "foo": cty.StringVal("hello"), 6627 "bar": cty.StringVal("baz"), 6628 }), 6629 After: cty.ObjectVal(map[string]cty.Value{ 6630 "id": cty.StringVal("12345"), 6631 "foo": cty.StringVal("hello"), 6632 "bar": cty.StringVal("baz"), 6633 }), 6634 Schema: &configschema.Block{ 6635 Attributes: map[string]*configschema.Attribute{ 6636 "id": {Type: cty.String, Computed: true}, 6637 "foo": {Type: cty.String, Optional: true}, 6638 "bar": {Type: cty.String, Optional: true}, 6639 }, 6640 }, 6641 RequiredReplace: cty.NewPathSet(), 6642 ExpectedOutput: ` # test_instance.previous has moved to test_instance.example 6643 resource "test_instance" "example" { 6644 id = "12345" 6645 # (2 unchanged attributes hidden) 6646 } 6647 `, 6648 }, 6649 } 6650 6651 runTestCases(t, testCases) 6652 } 6653 6654 type testCase struct { 6655 Action plans.Action 6656 ActionReason plans.ResourceInstanceChangeActionReason 6657 ModuleInst addrs.ModuleInstance 6658 Mode addrs.ResourceMode 6659 InstanceKey addrs.InstanceKey 6660 DeposedKey states.DeposedKey 6661 Before cty.Value 6662 BeforeValMarks []cty.PathValueMarks 6663 AfterValMarks []cty.PathValueMarks 6664 After cty.Value 6665 Schema *configschema.Block 6666 RequiredReplace cty.PathSet 6667 ExpectedOutput string 6668 PrevRunAddr addrs.AbsResourceInstance 6669 } 6670 6671 func runTestCases(t *testing.T, testCases map[string]testCase) { 6672 color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} 6673 6674 for name, tc := range testCases { 6675 t.Run(name, func(t *testing.T) { 6676 ty := tc.Schema.ImpliedType() 6677 6678 beforeVal := tc.Before 6679 switch { // Some fixups to make the test cases a little easier to write 6680 case beforeVal.IsNull(): 6681 beforeVal = cty.NullVal(ty) // allow mistyped nulls 6682 case !beforeVal.IsKnown(): 6683 beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns 6684 } 6685 6686 afterVal := tc.After 6687 switch { // Some fixups to make the test cases a little easier to write 6688 case afterVal.IsNull(): 6689 afterVal = cty.NullVal(ty) // allow mistyped nulls 6690 case !afterVal.IsKnown(): 6691 afterVal = cty.UnknownVal(ty) // allow mistyped unknowns 6692 } 6693 6694 addr := addrs.Resource{ 6695 Mode: tc.Mode, 6696 Type: "test_instance", 6697 Name: "example", 6698 }.Instance(tc.InstanceKey).Absolute(tc.ModuleInst) 6699 6700 prevRunAddr := tc.PrevRunAddr 6701 // If no previous run address is given, reuse the current address 6702 // to make initialization easier 6703 if prevRunAddr.Resource.Resource.Type == "" { 6704 prevRunAddr = addr 6705 } 6706 6707 change := &plans.ResourceInstanceChange{ 6708 Addr: addr, 6709 PrevRunAddr: prevRunAddr, 6710 DeposedKey: tc.DeposedKey, 6711 ProviderAddr: addrs.AbsProviderConfig{ 6712 Provider: addrs.NewDefaultProvider("test"), 6713 Module: addrs.RootModule, 6714 }, 6715 Change: plans.Change{ 6716 Action: tc.Action, 6717 Before: beforeVal.MarkWithPaths(tc.BeforeValMarks), 6718 After: afterVal.MarkWithPaths(tc.AfterValMarks), 6719 }, 6720 ActionReason: tc.ActionReason, 6721 RequiredReplace: tc.RequiredReplace, 6722 } 6723 6724 output := ResourceChange(change, tc.Schema, color, DiffLanguageProposedChange) 6725 if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" { 6726 t.Errorf("wrong output\n%s", diff) 6727 } 6728 }) 6729 } 6730 } 6731 6732 func TestOutputChanges(t *testing.T) { 6733 color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} 6734 6735 testCases := map[string]struct { 6736 changes []*plans.OutputChangeSrc 6737 output string 6738 }{ 6739 "new output value": { 6740 []*plans.OutputChangeSrc{ 6741 outputChange( 6742 "foo", 6743 cty.NullVal(cty.DynamicPseudoType), 6744 cty.StringVal("bar"), 6745 false, 6746 ), 6747 }, 6748 ` 6749 + foo = "bar"`, 6750 }, 6751 "removed output": { 6752 []*plans.OutputChangeSrc{ 6753 outputChange( 6754 "foo", 6755 cty.StringVal("bar"), 6756 cty.NullVal(cty.DynamicPseudoType), 6757 false, 6758 ), 6759 }, 6760 ` 6761 - foo = "bar" -> null`, 6762 }, 6763 "single string change": { 6764 []*plans.OutputChangeSrc{ 6765 outputChange( 6766 "foo", 6767 cty.StringVal("bar"), 6768 cty.StringVal("baz"), 6769 false, 6770 ), 6771 }, 6772 ` 6773 ~ foo = "bar" -> "baz"`, 6774 }, 6775 "element added to list": { 6776 []*plans.OutputChangeSrc{ 6777 outputChange( 6778 "foo", 6779 cty.ListVal([]cty.Value{ 6780 cty.StringVal("alpha"), 6781 cty.StringVal("beta"), 6782 cty.StringVal("delta"), 6783 cty.StringVal("epsilon"), 6784 }), 6785 cty.ListVal([]cty.Value{ 6786 cty.StringVal("alpha"), 6787 cty.StringVal("beta"), 6788 cty.StringVal("gamma"), 6789 cty.StringVal("delta"), 6790 cty.StringVal("epsilon"), 6791 }), 6792 false, 6793 ), 6794 }, 6795 ` 6796 ~ foo = [ 6797 # (1 unchanged element hidden) 6798 "beta", 6799 + "gamma", 6800 "delta", 6801 # (1 unchanged element hidden) 6802 ]`, 6803 }, 6804 "multiple outputs changed, one sensitive": { 6805 []*plans.OutputChangeSrc{ 6806 outputChange( 6807 "a", 6808 cty.NumberIntVal(1), 6809 cty.NumberIntVal(2), 6810 false, 6811 ), 6812 outputChange( 6813 "b", 6814 cty.StringVal("hunter2"), 6815 cty.StringVal("correct-horse-battery-staple"), 6816 true, 6817 ), 6818 outputChange( 6819 "c", 6820 cty.BoolVal(false), 6821 cty.BoolVal(true), 6822 false, 6823 ), 6824 }, 6825 ` 6826 ~ a = 1 -> 2 6827 ~ b = (sensitive value) 6828 ~ c = false -> true`, 6829 }, 6830 } 6831 6832 for name, tc := range testCases { 6833 t.Run(name, func(t *testing.T) { 6834 output := OutputChanges(tc.changes, color) 6835 if output != tc.output { 6836 t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output) 6837 } 6838 }) 6839 } 6840 } 6841 6842 func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc { 6843 addr := addrs.AbsOutputValue{ 6844 OutputValue: addrs.OutputValue{Name: name}, 6845 } 6846 6847 change := &plans.OutputChange{ 6848 Addr: addr, Change: plans.Change{ 6849 Before: before, 6850 After: after, 6851 }, 6852 Sensitive: sensitive, 6853 } 6854 6855 changeSrc, err := change.Encode() 6856 if err != nil { 6857 panic(fmt.Sprintf("failed to encode change for %s: %s", addr, err)) 6858 } 6859 6860 return changeSrc 6861 } 6862 6863 // A basic test schema using a configurable NestingMode for one (NestedType) attribute and one block 6864 func testSchema(nesting configschema.NestingMode) *configschema.Block { 6865 var diskKey = "disks" 6866 if nesting == configschema.NestingSingle { 6867 diskKey = "disk" 6868 } 6869 6870 return &configschema.Block{ 6871 Attributes: map[string]*configschema.Attribute{ 6872 "id": {Type: cty.String, Optional: true, Computed: true}, 6873 "ami": {Type: cty.String, Optional: true}, 6874 diskKey: { 6875 NestedType: &configschema.Object{ 6876 Attributes: map[string]*configschema.Attribute{ 6877 "mount_point": {Type: cty.String, Optional: true}, 6878 "size": {Type: cty.String, Optional: true}, 6879 }, 6880 Nesting: nesting, 6881 }, 6882 }, 6883 }, 6884 BlockTypes: map[string]*configschema.NestedBlock{ 6885 "root_block_device": { 6886 Block: configschema.Block{ 6887 Attributes: map[string]*configschema.Attribute{ 6888 "volume_type": { 6889 Type: cty.String, 6890 Optional: true, 6891 Computed: true, 6892 }, 6893 }, 6894 }, 6895 Nesting: nesting, 6896 }, 6897 }, 6898 } 6899 } 6900 6901 // A basic test schema using a configurable NestingMode for one (NestedType) 6902 // attribute marked sensitive. 6903 func testSchemaSensitive(nesting configschema.NestingMode) *configschema.Block { 6904 return &configschema.Block{ 6905 Attributes: map[string]*configschema.Attribute{ 6906 "id": {Type: cty.String, Optional: true, Computed: true}, 6907 "ami": {Type: cty.String, Optional: true}, 6908 "disks": { 6909 Sensitive: true, 6910 NestedType: &configschema.Object{ 6911 Attributes: map[string]*configschema.Attribute{ 6912 "mount_point": {Type: cty.String, Optional: true}, 6913 "size": {Type: cty.String, Optional: true}, 6914 }, 6915 Nesting: nesting, 6916 }, 6917 }, 6918 }, 6919 } 6920 } 6921 6922 func testSchemaMultipleBlocks(nesting configschema.NestingMode) *configschema.Block { 6923 return &configschema.Block{ 6924 Attributes: map[string]*configschema.Attribute{ 6925 "id": {Type: cty.String, Optional: true, Computed: true}, 6926 "ami": {Type: cty.String, Optional: true}, 6927 "disks": { 6928 NestedType: &configschema.Object{ 6929 Attributes: map[string]*configschema.Attribute{ 6930 "mount_point": {Type: cty.String, Optional: true}, 6931 "size": {Type: cty.String, Optional: true}, 6932 }, 6933 Nesting: nesting, 6934 }, 6935 }, 6936 }, 6937 BlockTypes: map[string]*configschema.NestedBlock{ 6938 "root_block_device": { 6939 Block: configschema.Block{ 6940 Attributes: map[string]*configschema.Attribute{ 6941 "volume_type": { 6942 Type: cty.String, 6943 Optional: true, 6944 Computed: true, 6945 }, 6946 }, 6947 }, 6948 Nesting: nesting, 6949 }, 6950 "leaf_block_device": { 6951 Block: configschema.Block{ 6952 Attributes: map[string]*configschema.Attribute{ 6953 "volume_type": { 6954 Type: cty.String, 6955 Optional: true, 6956 Computed: true, 6957 }, 6958 }, 6959 }, 6960 Nesting: nesting, 6961 }, 6962 }, 6963 } 6964 } 6965 6966 // similar to testSchema with the addition of a "new_field" block 6967 func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block { 6968 var diskKey = "disks" 6969 if nesting == configschema.NestingSingle { 6970 diskKey = "disk" 6971 } 6972 6973 return &configschema.Block{ 6974 Attributes: map[string]*configschema.Attribute{ 6975 "id": {Type: cty.String, Optional: true, Computed: true}, 6976 "ami": {Type: cty.String, Optional: true}, 6977 diskKey: { 6978 NestedType: &configschema.Object{ 6979 Attributes: map[string]*configschema.Attribute{ 6980 "mount_point": {Type: cty.String, Optional: true}, 6981 "size": {Type: cty.String, Optional: true}, 6982 }, 6983 Nesting: nesting, 6984 }, 6985 }, 6986 }, 6987 BlockTypes: map[string]*configschema.NestedBlock{ 6988 "root_block_device": { 6989 Block: configschema.Block{ 6990 Attributes: map[string]*configschema.Attribute{ 6991 "volume_type": { 6992 Type: cty.String, 6993 Optional: true, 6994 Computed: true, 6995 }, 6996 "new_field": { 6997 Type: cty.String, 6998 Optional: true, 6999 Computed: true, 7000 }, 7001 }, 7002 }, 7003 Nesting: nesting, 7004 }, 7005 }, 7006 } 7007 }