github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/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/eliastor/durgaform/internal/addrs" 9 "github.com/eliastor/durgaform/internal/configs/configschema" 10 "github.com/eliastor/durgaform/internal/lang/marks" 11 "github.com/eliastor/durgaform/internal/plans" 12 "github.com/eliastor/durgaform/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/eliastor/durgaform/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) 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_actionReason(t *testing.T) { 4576 emptySchema := &configschema.Block{} 4577 nullVal := cty.NullVal(cty.EmptyObject) 4578 emptyVal := cty.EmptyObjectVal 4579 4580 testCases := map[string]testCase{ 4581 "delete for no particular reason": { 4582 Action: plans.Delete, 4583 ActionReason: plans.ResourceInstanceChangeNoReason, 4584 Mode: addrs.ManagedResourceMode, 4585 Before: emptyVal, 4586 After: nullVal, 4587 Schema: emptySchema, 4588 RequiredReplace: cty.NewPathSet(), 4589 ExpectedOutput: ` # test_instance.example will be destroyed 4590 - resource "test_instance" "example" {} 4591 `, 4592 }, 4593 "delete because of wrong repetition mode (NoKey)": { 4594 Action: plans.Delete, 4595 ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, 4596 Mode: addrs.ManagedResourceMode, 4597 InstanceKey: addrs.NoKey, 4598 Before: emptyVal, 4599 After: nullVal, 4600 Schema: emptySchema, 4601 RequiredReplace: cty.NewPathSet(), 4602 ExpectedOutput: ` # test_instance.example will be destroyed 4603 # (because resource uses count or for_each) 4604 - resource "test_instance" "example" {} 4605 `, 4606 }, 4607 "delete because of wrong repetition mode (IntKey)": { 4608 Action: plans.Delete, 4609 ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, 4610 Mode: addrs.ManagedResourceMode, 4611 InstanceKey: addrs.IntKey(1), 4612 Before: emptyVal, 4613 After: nullVal, 4614 Schema: emptySchema, 4615 RequiredReplace: cty.NewPathSet(), 4616 ExpectedOutput: ` # test_instance.example[1] will be destroyed 4617 # (because resource does not use count) 4618 - resource "test_instance" "example" {} 4619 `, 4620 }, 4621 "delete because of wrong repetition mode (StringKey)": { 4622 Action: plans.Delete, 4623 ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, 4624 Mode: addrs.ManagedResourceMode, 4625 InstanceKey: addrs.StringKey("a"), 4626 Before: emptyVal, 4627 After: nullVal, 4628 Schema: emptySchema, 4629 RequiredReplace: cty.NewPathSet(), 4630 ExpectedOutput: ` # test_instance.example["a"] will be destroyed 4631 # (because resource does not use for_each) 4632 - resource "test_instance" "example" {} 4633 `, 4634 }, 4635 "delete because no resource configuration": { 4636 Action: plans.Delete, 4637 ActionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig, 4638 ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.NoKey), 4639 Mode: addrs.ManagedResourceMode, 4640 Before: emptyVal, 4641 After: nullVal, 4642 Schema: emptySchema, 4643 RequiredReplace: cty.NewPathSet(), 4644 ExpectedOutput: ` # module.foo.test_instance.example will be destroyed 4645 # (because test_instance.example is not in configuration) 4646 - resource "test_instance" "example" {} 4647 `, 4648 }, 4649 "delete because no module": { 4650 Action: plans.Delete, 4651 ActionReason: plans.ResourceInstanceDeleteBecauseNoModule, 4652 ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)), 4653 Mode: addrs.ManagedResourceMode, 4654 Before: emptyVal, 4655 After: nullVal, 4656 Schema: emptySchema, 4657 RequiredReplace: cty.NewPathSet(), 4658 ExpectedOutput: ` # module.foo[1].test_instance.example will be destroyed 4659 # (because module.foo[1] is not in configuration) 4660 - resource "test_instance" "example" {} 4661 `, 4662 }, 4663 "delete because out of range for count": { 4664 Action: plans.Delete, 4665 ActionReason: plans.ResourceInstanceDeleteBecauseCountIndex, 4666 Mode: addrs.ManagedResourceMode, 4667 InstanceKey: addrs.IntKey(1), 4668 Before: emptyVal, 4669 After: nullVal, 4670 Schema: emptySchema, 4671 RequiredReplace: cty.NewPathSet(), 4672 ExpectedOutput: ` # test_instance.example[1] will be destroyed 4673 # (because index [1] is out of range for count) 4674 - resource "test_instance" "example" {} 4675 `, 4676 }, 4677 "delete because out of range for for_each": { 4678 Action: plans.Delete, 4679 ActionReason: plans.ResourceInstanceDeleteBecauseEachKey, 4680 Mode: addrs.ManagedResourceMode, 4681 InstanceKey: addrs.StringKey("boop"), 4682 Before: emptyVal, 4683 After: nullVal, 4684 Schema: emptySchema, 4685 RequiredReplace: cty.NewPathSet(), 4686 ExpectedOutput: ` # test_instance.example["boop"] will be destroyed 4687 # (because key ["boop"] is not in for_each map) 4688 - resource "test_instance" "example" {} 4689 `, 4690 }, 4691 "replace for no particular reason (delete first)": { 4692 Action: plans.DeleteThenCreate, 4693 ActionReason: plans.ResourceInstanceChangeNoReason, 4694 Mode: addrs.ManagedResourceMode, 4695 Before: emptyVal, 4696 After: nullVal, 4697 Schema: emptySchema, 4698 RequiredReplace: cty.NewPathSet(), 4699 ExpectedOutput: ` # test_instance.example must be replaced 4700 -/+ resource "test_instance" "example" {} 4701 `, 4702 }, 4703 "replace for no particular reason (create first)": { 4704 Action: plans.CreateThenDelete, 4705 ActionReason: plans.ResourceInstanceChangeNoReason, 4706 Mode: addrs.ManagedResourceMode, 4707 Before: emptyVal, 4708 After: nullVal, 4709 Schema: emptySchema, 4710 RequiredReplace: cty.NewPathSet(), 4711 ExpectedOutput: ` # test_instance.example must be replaced 4712 +/- resource "test_instance" "example" {} 4713 `, 4714 }, 4715 "replace by request (delete first)": { 4716 Action: plans.DeleteThenCreate, 4717 ActionReason: plans.ResourceInstanceReplaceByRequest, 4718 Mode: addrs.ManagedResourceMode, 4719 Before: emptyVal, 4720 After: nullVal, 4721 Schema: emptySchema, 4722 RequiredReplace: cty.NewPathSet(), 4723 ExpectedOutput: ` # test_instance.example will be replaced, as requested 4724 -/+ resource "test_instance" "example" {} 4725 `, 4726 }, 4727 "replace by request (create first)": { 4728 Action: plans.CreateThenDelete, 4729 ActionReason: plans.ResourceInstanceReplaceByRequest, 4730 Mode: addrs.ManagedResourceMode, 4731 Before: emptyVal, 4732 After: nullVal, 4733 Schema: emptySchema, 4734 RequiredReplace: cty.NewPathSet(), 4735 ExpectedOutput: ` # test_instance.example will be replaced, as requested 4736 +/- resource "test_instance" "example" {} 4737 `, 4738 }, 4739 "replace because tainted (delete first)": { 4740 Action: plans.DeleteThenCreate, 4741 ActionReason: plans.ResourceInstanceReplaceBecauseTainted, 4742 Mode: addrs.ManagedResourceMode, 4743 Before: emptyVal, 4744 After: nullVal, 4745 Schema: emptySchema, 4746 RequiredReplace: cty.NewPathSet(), 4747 ExpectedOutput: ` # test_instance.example is tainted, so must be replaced 4748 -/+ resource "test_instance" "example" {} 4749 `, 4750 }, 4751 "replace because tainted (create first)": { 4752 Action: plans.CreateThenDelete, 4753 ActionReason: plans.ResourceInstanceReplaceBecauseTainted, 4754 Mode: addrs.ManagedResourceMode, 4755 Before: emptyVal, 4756 After: nullVal, 4757 Schema: emptySchema, 4758 RequiredReplace: cty.NewPathSet(), 4759 ExpectedOutput: ` # test_instance.example is tainted, so must be replaced 4760 +/- resource "test_instance" "example" {} 4761 `, 4762 }, 4763 "replace because cannot update (delete first)": { 4764 Action: plans.DeleteThenCreate, 4765 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 4766 Mode: addrs.ManagedResourceMode, 4767 Before: emptyVal, 4768 After: nullVal, 4769 Schema: emptySchema, 4770 RequiredReplace: cty.NewPathSet(), 4771 // This one has no special message, because the fuller explanation 4772 // typically appears inline as a "# forces replacement" comment. 4773 // (not shown here) 4774 ExpectedOutput: ` # test_instance.example must be replaced 4775 -/+ resource "test_instance" "example" {} 4776 `, 4777 }, 4778 "replace because cannot update (create first)": { 4779 Action: plans.CreateThenDelete, 4780 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 4781 Mode: addrs.ManagedResourceMode, 4782 Before: emptyVal, 4783 After: nullVal, 4784 Schema: emptySchema, 4785 RequiredReplace: cty.NewPathSet(), 4786 // This one has no special message, because the fuller explanation 4787 // typically appears inline as a "# forces replacement" comment. 4788 // (not shown here) 4789 ExpectedOutput: ` # test_instance.example must be replaced 4790 +/- resource "test_instance" "example" {} 4791 `, 4792 }, 4793 } 4794 4795 runTestCases(t, testCases) 4796 } 4797 4798 func TestResourceChange_sensitiveVariable(t *testing.T) { 4799 testCases := map[string]testCase{ 4800 "creation": { 4801 Action: plans.Create, 4802 Mode: addrs.ManagedResourceMode, 4803 Before: cty.NullVal(cty.EmptyObject), 4804 After: cty.ObjectVal(map[string]cty.Value{ 4805 "id": cty.StringVal("i-02ae66f368e8518a9"), 4806 "ami": cty.StringVal("ami-123"), 4807 "map_key": cty.MapVal(map[string]cty.Value{ 4808 "breakfast": cty.NumberIntVal(800), 4809 "dinner": cty.NumberIntVal(2000), 4810 }), 4811 "map_whole": cty.MapVal(map[string]cty.Value{ 4812 "breakfast": cty.StringVal("pizza"), 4813 "dinner": cty.StringVal("pizza"), 4814 }), 4815 "list_field": cty.ListVal([]cty.Value{ 4816 cty.StringVal("hello"), 4817 cty.StringVal("friends"), 4818 cty.StringVal("!"), 4819 }), 4820 "nested_block_list": cty.ListVal([]cty.Value{ 4821 cty.ObjectVal(map[string]cty.Value{ 4822 "an_attr": cty.StringVal("secretval"), 4823 "another": cty.StringVal("not secret"), 4824 }), 4825 }), 4826 "nested_block_set": cty.ListVal([]cty.Value{ 4827 cty.ObjectVal(map[string]cty.Value{ 4828 "an_attr": cty.StringVal("secretval"), 4829 "another": cty.StringVal("not secret"), 4830 }), 4831 }), 4832 }), 4833 AfterValMarks: []cty.PathValueMarks{ 4834 { 4835 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 4836 Marks: cty.NewValueMarks(marks.Sensitive), 4837 }, 4838 { 4839 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, 4840 Marks: cty.NewValueMarks(marks.Sensitive), 4841 }, 4842 { 4843 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 4844 Marks: cty.NewValueMarks(marks.Sensitive), 4845 }, 4846 { 4847 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 4848 Marks: cty.NewValueMarks(marks.Sensitive), 4849 }, 4850 { 4851 // Nested blocks/sets will mark the whole set/block as sensitive 4852 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_list"}}, 4853 Marks: cty.NewValueMarks(marks.Sensitive), 4854 }, 4855 { 4856 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 4857 Marks: cty.NewValueMarks(marks.Sensitive), 4858 }, 4859 }, 4860 RequiredReplace: cty.NewPathSet(), 4861 Schema: &configschema.Block{ 4862 Attributes: map[string]*configschema.Attribute{ 4863 "id": {Type: cty.String, Optional: true, Computed: true}, 4864 "ami": {Type: cty.String, Optional: true}, 4865 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 4866 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 4867 "list_field": {Type: cty.List(cty.String), Optional: true}, 4868 }, 4869 BlockTypes: map[string]*configschema.NestedBlock{ 4870 "nested_block_list": { 4871 Block: configschema.Block{ 4872 Attributes: map[string]*configschema.Attribute{ 4873 "an_attr": {Type: cty.String, Optional: true}, 4874 "another": {Type: cty.String, Optional: true}, 4875 }, 4876 }, 4877 Nesting: configschema.NestingList, 4878 }, 4879 "nested_block_set": { 4880 Block: configschema.Block{ 4881 Attributes: map[string]*configschema.Attribute{ 4882 "an_attr": {Type: cty.String, Optional: true}, 4883 "another": {Type: cty.String, Optional: true}, 4884 }, 4885 }, 4886 Nesting: configschema.NestingSet, 4887 }, 4888 }, 4889 }, 4890 ExpectedOutput: ` # test_instance.example will be created 4891 + resource "test_instance" "example" { 4892 + ami = (sensitive) 4893 + id = "i-02ae66f368e8518a9" 4894 + list_field = [ 4895 + "hello", 4896 + (sensitive), 4897 + "!", 4898 ] 4899 + map_key = { 4900 + "breakfast" = 800 4901 + "dinner" = (sensitive) 4902 } 4903 + map_whole = (sensitive) 4904 4905 + nested_block_list { 4906 # At least one attribute in this block is (or was) sensitive, 4907 # so its contents will not be displayed. 4908 } 4909 4910 + nested_block_set { 4911 # At least one attribute in this block is (or was) sensitive, 4912 # so its contents will not be displayed. 4913 } 4914 } 4915 `, 4916 }, 4917 "in-place update - before sensitive": { 4918 Action: plans.Update, 4919 Mode: addrs.ManagedResourceMode, 4920 Before: cty.ObjectVal(map[string]cty.Value{ 4921 "id": cty.StringVal("i-02ae66f368e8518a9"), 4922 "ami": cty.StringVal("ami-BEFORE"), 4923 "special": cty.BoolVal(true), 4924 "some_number": cty.NumberIntVal(1), 4925 "list_field": cty.ListVal([]cty.Value{ 4926 cty.StringVal("hello"), 4927 cty.StringVal("friends"), 4928 cty.StringVal("!"), 4929 }), 4930 "map_key": cty.MapVal(map[string]cty.Value{ 4931 "breakfast": cty.NumberIntVal(800), 4932 "dinner": cty.NumberIntVal(2000), // sensitive key 4933 }), 4934 "map_whole": cty.MapVal(map[string]cty.Value{ 4935 "breakfast": cty.StringVal("pizza"), 4936 "dinner": cty.StringVal("pizza"), 4937 }), 4938 "nested_block": cty.ListVal([]cty.Value{ 4939 cty.ObjectVal(map[string]cty.Value{ 4940 "an_attr": cty.StringVal("secretval"), 4941 }), 4942 }), 4943 "nested_block_set": cty.ListVal([]cty.Value{ 4944 cty.ObjectVal(map[string]cty.Value{ 4945 "an_attr": cty.StringVal("secretval"), 4946 }), 4947 }), 4948 }), 4949 After: cty.ObjectVal(map[string]cty.Value{ 4950 "id": cty.StringVal("i-02ae66f368e8518a9"), 4951 "ami": cty.StringVal("ami-AFTER"), 4952 "special": cty.BoolVal(false), 4953 "some_number": cty.NumberIntVal(2), 4954 "list_field": cty.ListVal([]cty.Value{ 4955 cty.StringVal("hello"), 4956 cty.StringVal("friends"), 4957 cty.StringVal("."), 4958 }), 4959 "map_key": cty.MapVal(map[string]cty.Value{ 4960 "breakfast": cty.NumberIntVal(800), 4961 "dinner": cty.NumberIntVal(1900), 4962 }), 4963 "map_whole": cty.MapVal(map[string]cty.Value{ 4964 "breakfast": cty.StringVal("cereal"), 4965 "dinner": cty.StringVal("pizza"), 4966 }), 4967 "nested_block": cty.ListVal([]cty.Value{ 4968 cty.ObjectVal(map[string]cty.Value{ 4969 "an_attr": cty.StringVal("changed"), 4970 }), 4971 }), 4972 "nested_block_set": cty.ListVal([]cty.Value{ 4973 cty.ObjectVal(map[string]cty.Value{ 4974 "an_attr": cty.StringVal("changed"), 4975 }), 4976 }), 4977 }), 4978 BeforeValMarks: []cty.PathValueMarks{ 4979 { 4980 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 4981 Marks: cty.NewValueMarks(marks.Sensitive), 4982 }, 4983 { 4984 Path: cty.Path{cty.GetAttrStep{Name: "special"}}, 4985 Marks: cty.NewValueMarks(marks.Sensitive), 4986 }, 4987 { 4988 Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, 4989 Marks: cty.NewValueMarks(marks.Sensitive), 4990 }, 4991 { 4992 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, 4993 Marks: cty.NewValueMarks(marks.Sensitive), 4994 }, 4995 { 4996 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 4997 Marks: cty.NewValueMarks(marks.Sensitive), 4998 }, 4999 { 5000 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 5001 Marks: cty.NewValueMarks(marks.Sensitive), 5002 }, 5003 { 5004 Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, 5005 Marks: cty.NewValueMarks(marks.Sensitive), 5006 }, 5007 { 5008 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 5009 Marks: cty.NewValueMarks(marks.Sensitive), 5010 }, 5011 }, 5012 RequiredReplace: cty.NewPathSet(), 5013 Schema: &configschema.Block{ 5014 Attributes: map[string]*configschema.Attribute{ 5015 "id": {Type: cty.String, Optional: true, Computed: true}, 5016 "ami": {Type: cty.String, Optional: true}, 5017 "list_field": {Type: cty.List(cty.String), Optional: true}, 5018 "special": {Type: cty.Bool, Optional: true}, 5019 "some_number": {Type: cty.Number, Optional: true}, 5020 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 5021 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 5022 }, 5023 BlockTypes: map[string]*configschema.NestedBlock{ 5024 "nested_block": { 5025 Block: configschema.Block{ 5026 Attributes: map[string]*configschema.Attribute{ 5027 "an_attr": {Type: cty.String, Optional: true}, 5028 }, 5029 }, 5030 Nesting: configschema.NestingList, 5031 }, 5032 "nested_block_set": { 5033 Block: configschema.Block{ 5034 Attributes: map[string]*configschema.Attribute{ 5035 "an_attr": {Type: cty.String, Optional: true}, 5036 }, 5037 }, 5038 Nesting: configschema.NestingSet, 5039 }, 5040 }, 5041 }, 5042 ExpectedOutput: ` # test_instance.example will be updated in-place 5043 ~ resource "test_instance" "example" { 5044 # Warning: this attribute value will no longer be marked as sensitive 5045 # after applying this change. 5046 ~ ami = (sensitive) 5047 id = "i-02ae66f368e8518a9" 5048 ~ list_field = [ 5049 # (1 unchanged element hidden) 5050 "friends", 5051 - (sensitive), 5052 + ".", 5053 ] 5054 ~ map_key = { 5055 # Warning: this attribute value will no longer be marked as sensitive 5056 # after applying this change. 5057 ~ "dinner" = (sensitive) 5058 # (1 unchanged element hidden) 5059 } 5060 # Warning: this attribute value will no longer be marked as sensitive 5061 # after applying this change. 5062 ~ map_whole = (sensitive) 5063 # Warning: this attribute value will no longer be marked as sensitive 5064 # after applying this change. 5065 ~ some_number = (sensitive) 5066 # Warning: this attribute value will no longer be marked as sensitive 5067 # after applying this change. 5068 ~ special = (sensitive) 5069 5070 # Warning: this block will no longer be marked as sensitive 5071 # after applying this change. 5072 ~ nested_block { 5073 # At least one attribute in this block is (or was) sensitive, 5074 # so its contents will not be displayed. 5075 } 5076 5077 # Warning: this block will no longer be marked as sensitive 5078 # after applying this change. 5079 ~ nested_block_set { 5080 # At least one attribute in this block is (or was) sensitive, 5081 # so its contents will not be displayed. 5082 } 5083 } 5084 `, 5085 }, 5086 "in-place update - after sensitive": { 5087 Action: plans.Update, 5088 Mode: addrs.ManagedResourceMode, 5089 Before: cty.ObjectVal(map[string]cty.Value{ 5090 "id": cty.StringVal("i-02ae66f368e8518a9"), 5091 "list_field": cty.ListVal([]cty.Value{ 5092 cty.StringVal("hello"), 5093 cty.StringVal("friends"), 5094 }), 5095 "map_key": cty.MapVal(map[string]cty.Value{ 5096 "breakfast": cty.NumberIntVal(800), 5097 "dinner": cty.NumberIntVal(2000), // sensitive key 5098 }), 5099 "map_whole": cty.MapVal(map[string]cty.Value{ 5100 "breakfast": cty.StringVal("pizza"), 5101 "dinner": cty.StringVal("pizza"), 5102 }), 5103 "nested_block_single": cty.ObjectVal(map[string]cty.Value{ 5104 "an_attr": cty.StringVal("original"), 5105 }), 5106 }), 5107 After: cty.ObjectVal(map[string]cty.Value{ 5108 "id": cty.StringVal("i-02ae66f368e8518a9"), 5109 "list_field": cty.ListVal([]cty.Value{ 5110 cty.StringVal("goodbye"), 5111 cty.StringVal("friends"), 5112 }), 5113 "map_key": cty.MapVal(map[string]cty.Value{ 5114 "breakfast": cty.NumberIntVal(700), 5115 "dinner": cty.NumberIntVal(2100), // sensitive key 5116 }), 5117 "map_whole": cty.MapVal(map[string]cty.Value{ 5118 "breakfast": cty.StringVal("cereal"), 5119 "dinner": cty.StringVal("pizza"), 5120 }), 5121 "nested_block_single": cty.ObjectVal(map[string]cty.Value{ 5122 "an_attr": cty.StringVal("changed"), 5123 }), 5124 }), 5125 AfterValMarks: []cty.PathValueMarks{ 5126 { 5127 Path: cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}}, 5128 Marks: cty.NewValueMarks(marks.Sensitive), 5129 }, 5130 { 5131 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, 5132 Marks: cty.NewValueMarks(marks.Sensitive), 5133 }, 5134 { 5135 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 5136 Marks: cty.NewValueMarks(marks.Sensitive), 5137 }, 5138 { 5139 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 5140 Marks: cty.NewValueMarks(marks.Sensitive), 5141 }, 5142 { 5143 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_single"}}, 5144 Marks: cty.NewValueMarks(marks.Sensitive), 5145 }, 5146 }, 5147 RequiredReplace: cty.NewPathSet(), 5148 Schema: &configschema.Block{ 5149 Attributes: map[string]*configschema.Attribute{ 5150 "id": {Type: cty.String, Optional: true, Computed: true}, 5151 "list_field": {Type: cty.List(cty.String), Optional: true}, 5152 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 5153 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 5154 }, 5155 BlockTypes: map[string]*configschema.NestedBlock{ 5156 "nested_block_single": { 5157 Block: configschema.Block{ 5158 Attributes: map[string]*configschema.Attribute{ 5159 "an_attr": {Type: cty.String, Optional: true}, 5160 }, 5161 }, 5162 Nesting: configschema.NestingSingle, 5163 }, 5164 }, 5165 }, 5166 ExpectedOutput: ` # test_instance.example will be updated in-place 5167 ~ resource "test_instance" "example" { 5168 id = "i-02ae66f368e8518a9" 5169 ~ list_field = [ 5170 - "hello", 5171 + (sensitive), 5172 "friends", 5173 ] 5174 ~ map_key = { 5175 ~ "breakfast" = 800 -> 700 5176 # Warning: this attribute value will be marked as sensitive and will not 5177 # display in UI output after applying this change. 5178 ~ "dinner" = (sensitive) 5179 } 5180 # Warning: this attribute value will be marked as sensitive and will not 5181 # display in UI output after applying this change. 5182 ~ map_whole = (sensitive) 5183 5184 # Warning: this block will be marked as sensitive and will not 5185 # display in UI output after applying this change. 5186 ~ nested_block_single { 5187 # At least one attribute in this block is (or was) sensitive, 5188 # so its contents will not be displayed. 5189 } 5190 } 5191 `, 5192 }, 5193 "in-place update - both sensitive": { 5194 Action: plans.Update, 5195 Mode: addrs.ManagedResourceMode, 5196 Before: cty.ObjectVal(map[string]cty.Value{ 5197 "id": cty.StringVal("i-02ae66f368e8518a9"), 5198 "ami": cty.StringVal("ami-BEFORE"), 5199 "list_field": cty.ListVal([]cty.Value{ 5200 cty.StringVal("hello"), 5201 cty.StringVal("friends"), 5202 }), 5203 "map_key": cty.MapVal(map[string]cty.Value{ 5204 "breakfast": cty.NumberIntVal(800), 5205 "dinner": cty.NumberIntVal(2000), // sensitive key 5206 }), 5207 "map_whole": cty.MapVal(map[string]cty.Value{ 5208 "breakfast": cty.StringVal("pizza"), 5209 "dinner": cty.StringVal("pizza"), 5210 }), 5211 "nested_block_map": cty.MapVal(map[string]cty.Value{ 5212 "foo": cty.ObjectVal(map[string]cty.Value{ 5213 "an_attr": cty.StringVal("original"), 5214 }), 5215 }), 5216 }), 5217 After: cty.ObjectVal(map[string]cty.Value{ 5218 "id": cty.StringVal("i-02ae66f368e8518a9"), 5219 "ami": cty.StringVal("ami-AFTER"), 5220 "list_field": cty.ListVal([]cty.Value{ 5221 cty.StringVal("goodbye"), 5222 cty.StringVal("friends"), 5223 }), 5224 "map_key": cty.MapVal(map[string]cty.Value{ 5225 "breakfast": cty.NumberIntVal(800), 5226 "dinner": cty.NumberIntVal(1800), // sensitive key 5227 }), 5228 "map_whole": cty.MapVal(map[string]cty.Value{ 5229 "breakfast": cty.StringVal("cereal"), 5230 "dinner": cty.StringVal("pizza"), 5231 }), 5232 "nested_block_map": cty.MapVal(map[string]cty.Value{ 5233 "foo": cty.ObjectVal(map[string]cty.Value{ 5234 "an_attr": cty.UnknownVal(cty.String), 5235 }), 5236 }), 5237 }), 5238 BeforeValMarks: []cty.PathValueMarks{ 5239 { 5240 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 5241 Marks: cty.NewValueMarks(marks.Sensitive), 5242 }, 5243 { 5244 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, 5245 Marks: cty.NewValueMarks(marks.Sensitive), 5246 }, 5247 { 5248 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 5249 Marks: cty.NewValueMarks(marks.Sensitive), 5250 }, 5251 { 5252 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 5253 Marks: cty.NewValueMarks(marks.Sensitive), 5254 }, 5255 { 5256 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, 5257 Marks: cty.NewValueMarks(marks.Sensitive), 5258 }, 5259 }, 5260 AfterValMarks: []cty.PathValueMarks{ 5261 { 5262 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 5263 Marks: cty.NewValueMarks(marks.Sensitive), 5264 }, 5265 { 5266 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, 5267 Marks: cty.NewValueMarks(marks.Sensitive), 5268 }, 5269 { 5270 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 5271 Marks: cty.NewValueMarks(marks.Sensitive), 5272 }, 5273 { 5274 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 5275 Marks: cty.NewValueMarks(marks.Sensitive), 5276 }, 5277 { 5278 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, 5279 Marks: cty.NewValueMarks(marks.Sensitive), 5280 }, 5281 }, 5282 RequiredReplace: cty.NewPathSet(), 5283 Schema: &configschema.Block{ 5284 Attributes: map[string]*configschema.Attribute{ 5285 "id": {Type: cty.String, Optional: true, Computed: true}, 5286 "ami": {Type: cty.String, Optional: true}, 5287 "list_field": {Type: cty.List(cty.String), Optional: true}, 5288 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 5289 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 5290 }, 5291 BlockTypes: map[string]*configschema.NestedBlock{ 5292 "nested_block_map": { 5293 Block: configschema.Block{ 5294 Attributes: map[string]*configschema.Attribute{ 5295 "an_attr": {Type: cty.String, Optional: true}, 5296 }, 5297 }, 5298 Nesting: configschema.NestingMap, 5299 }, 5300 }, 5301 }, 5302 ExpectedOutput: ` # test_instance.example will be updated in-place 5303 ~ resource "test_instance" "example" { 5304 ~ ami = (sensitive) 5305 id = "i-02ae66f368e8518a9" 5306 ~ list_field = [ 5307 - (sensitive), 5308 + (sensitive), 5309 "friends", 5310 ] 5311 ~ map_key = { 5312 ~ "dinner" = (sensitive) 5313 # (1 unchanged element hidden) 5314 } 5315 ~ map_whole = (sensitive) 5316 5317 ~ nested_block_map { 5318 # At least one attribute in this block is (or was) sensitive, 5319 # so its contents will not be displayed. 5320 } 5321 } 5322 `, 5323 }, 5324 "in-place update - value unchanged, sensitivity changes": { 5325 Action: plans.Update, 5326 Mode: addrs.ManagedResourceMode, 5327 Before: cty.ObjectVal(map[string]cty.Value{ 5328 "id": cty.StringVal("i-02ae66f368e8518a9"), 5329 "ami": cty.StringVal("ami-BEFORE"), 5330 "special": cty.BoolVal(true), 5331 "some_number": cty.NumberIntVal(1), 5332 "list_field": cty.ListVal([]cty.Value{ 5333 cty.StringVal("hello"), 5334 cty.StringVal("friends"), 5335 cty.StringVal("!"), 5336 }), 5337 "map_key": cty.MapVal(map[string]cty.Value{ 5338 "breakfast": cty.NumberIntVal(800), 5339 "dinner": cty.NumberIntVal(2000), // sensitive key 5340 }), 5341 "map_whole": cty.MapVal(map[string]cty.Value{ 5342 "breakfast": cty.StringVal("pizza"), 5343 "dinner": cty.StringVal("pizza"), 5344 }), 5345 "nested_block": cty.ListVal([]cty.Value{ 5346 cty.ObjectVal(map[string]cty.Value{ 5347 "an_attr": cty.StringVal("secretval"), 5348 }), 5349 }), 5350 "nested_block_set": cty.ListVal([]cty.Value{ 5351 cty.ObjectVal(map[string]cty.Value{ 5352 "an_attr": cty.StringVal("secretval"), 5353 }), 5354 }), 5355 }), 5356 After: cty.ObjectVal(map[string]cty.Value{ 5357 "id": cty.StringVal("i-02ae66f368e8518a9"), 5358 "ami": cty.StringVal("ami-BEFORE"), 5359 "special": cty.BoolVal(true), 5360 "some_number": cty.NumberIntVal(1), 5361 "list_field": cty.ListVal([]cty.Value{ 5362 cty.StringVal("hello"), 5363 cty.StringVal("friends"), 5364 cty.StringVal("!"), 5365 }), 5366 "map_key": cty.MapVal(map[string]cty.Value{ 5367 "breakfast": cty.NumberIntVal(800), 5368 "dinner": cty.NumberIntVal(2000), // sensitive key 5369 }), 5370 "map_whole": cty.MapVal(map[string]cty.Value{ 5371 "breakfast": cty.StringVal("pizza"), 5372 "dinner": cty.StringVal("pizza"), 5373 }), 5374 "nested_block": cty.ListVal([]cty.Value{ 5375 cty.ObjectVal(map[string]cty.Value{ 5376 "an_attr": cty.StringVal("secretval"), 5377 }), 5378 }), 5379 "nested_block_set": cty.ListVal([]cty.Value{ 5380 cty.ObjectVal(map[string]cty.Value{ 5381 "an_attr": cty.StringVal("secretval"), 5382 }), 5383 }), 5384 }), 5385 BeforeValMarks: []cty.PathValueMarks{ 5386 { 5387 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 5388 Marks: cty.NewValueMarks(marks.Sensitive), 5389 }, 5390 { 5391 Path: cty.Path{cty.GetAttrStep{Name: "special"}}, 5392 Marks: cty.NewValueMarks(marks.Sensitive), 5393 }, 5394 { 5395 Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, 5396 Marks: cty.NewValueMarks(marks.Sensitive), 5397 }, 5398 { 5399 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, 5400 Marks: cty.NewValueMarks(marks.Sensitive), 5401 }, 5402 { 5403 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 5404 Marks: cty.NewValueMarks(marks.Sensitive), 5405 }, 5406 { 5407 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 5408 Marks: cty.NewValueMarks(marks.Sensitive), 5409 }, 5410 { 5411 Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, 5412 Marks: cty.NewValueMarks(marks.Sensitive), 5413 }, 5414 { 5415 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 5416 Marks: cty.NewValueMarks(marks.Sensitive), 5417 }, 5418 }, 5419 RequiredReplace: cty.NewPathSet(), 5420 Schema: &configschema.Block{ 5421 Attributes: map[string]*configschema.Attribute{ 5422 "id": {Type: cty.String, Optional: true, Computed: true}, 5423 "ami": {Type: cty.String, Optional: true}, 5424 "list_field": {Type: cty.List(cty.String), Optional: true}, 5425 "special": {Type: cty.Bool, Optional: true}, 5426 "some_number": {Type: cty.Number, Optional: true}, 5427 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 5428 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 5429 }, 5430 BlockTypes: map[string]*configschema.NestedBlock{ 5431 "nested_block": { 5432 Block: configschema.Block{ 5433 Attributes: map[string]*configschema.Attribute{ 5434 "an_attr": {Type: cty.String, Optional: true}, 5435 }, 5436 }, 5437 Nesting: configschema.NestingList, 5438 }, 5439 "nested_block_set": { 5440 Block: configschema.Block{ 5441 Attributes: map[string]*configschema.Attribute{ 5442 "an_attr": {Type: cty.String, Optional: true}, 5443 }, 5444 }, 5445 Nesting: configschema.NestingSet, 5446 }, 5447 }, 5448 }, 5449 ExpectedOutput: ` # test_instance.example will be updated in-place 5450 ~ resource "test_instance" "example" { 5451 # Warning: this attribute value will no longer be marked as sensitive 5452 # after applying this change. The value is unchanged. 5453 ~ ami = (sensitive) 5454 id = "i-02ae66f368e8518a9" 5455 ~ list_field = [ 5456 # (1 unchanged element hidden) 5457 "friends", 5458 - (sensitive), 5459 + "!", 5460 ] 5461 ~ map_key = { 5462 # Warning: this attribute value will no longer be marked as sensitive 5463 # after applying this change. The value is unchanged. 5464 ~ "dinner" = (sensitive) 5465 # (1 unchanged element hidden) 5466 } 5467 # Warning: this attribute value will no longer be marked as sensitive 5468 # after applying this change. The value is unchanged. 5469 ~ map_whole = (sensitive) 5470 # Warning: this attribute value will no longer be marked as sensitive 5471 # after applying this change. The value is unchanged. 5472 ~ some_number = (sensitive) 5473 # Warning: this attribute value will no longer be marked as sensitive 5474 # after applying this change. The value is unchanged. 5475 ~ special = (sensitive) 5476 5477 # Warning: this block will no longer be marked as sensitive 5478 # after applying this change. 5479 ~ nested_block { 5480 # At least one attribute in this block is (or was) sensitive, 5481 # so its contents will not be displayed. 5482 } 5483 5484 # Warning: this block will no longer be marked as sensitive 5485 # after applying this change. 5486 ~ nested_block_set { 5487 # At least one attribute in this block is (or was) sensitive, 5488 # so its contents will not be displayed. 5489 } 5490 } 5491 `, 5492 }, 5493 "deletion": { 5494 Action: plans.Delete, 5495 Mode: addrs.ManagedResourceMode, 5496 Before: cty.ObjectVal(map[string]cty.Value{ 5497 "id": cty.StringVal("i-02ae66f368e8518a9"), 5498 "ami": cty.StringVal("ami-BEFORE"), 5499 "list_field": cty.ListVal([]cty.Value{ 5500 cty.StringVal("hello"), 5501 cty.StringVal("friends"), 5502 }), 5503 "map_key": cty.MapVal(map[string]cty.Value{ 5504 "breakfast": cty.NumberIntVal(800), 5505 "dinner": cty.NumberIntVal(2000), // sensitive key 5506 }), 5507 "map_whole": cty.MapVal(map[string]cty.Value{ 5508 "breakfast": cty.StringVal("pizza"), 5509 "dinner": cty.StringVal("pizza"), 5510 }), 5511 "nested_block": cty.ListVal([]cty.Value{ 5512 cty.ObjectVal(map[string]cty.Value{ 5513 "an_attr": cty.StringVal("secret"), 5514 "another": cty.StringVal("not secret"), 5515 }), 5516 }), 5517 "nested_block_set": cty.ListVal([]cty.Value{ 5518 cty.ObjectVal(map[string]cty.Value{ 5519 "an_attr": cty.StringVal("secret"), 5520 "another": cty.StringVal("not secret"), 5521 }), 5522 }), 5523 }), 5524 After: cty.NullVal(cty.EmptyObject), 5525 BeforeValMarks: []cty.PathValueMarks{ 5526 { 5527 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 5528 Marks: cty.NewValueMarks(marks.Sensitive), 5529 }, 5530 { 5531 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, 5532 Marks: cty.NewValueMarks(marks.Sensitive), 5533 }, 5534 { 5535 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 5536 Marks: cty.NewValueMarks(marks.Sensitive), 5537 }, 5538 { 5539 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 5540 Marks: cty.NewValueMarks(marks.Sensitive), 5541 }, 5542 { 5543 Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, 5544 Marks: cty.NewValueMarks(marks.Sensitive), 5545 }, 5546 { 5547 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 5548 Marks: cty.NewValueMarks(marks.Sensitive), 5549 }, 5550 }, 5551 RequiredReplace: cty.NewPathSet(), 5552 Schema: &configschema.Block{ 5553 Attributes: map[string]*configschema.Attribute{ 5554 "id": {Type: cty.String, Optional: true, Computed: true}, 5555 "ami": {Type: cty.String, Optional: true}, 5556 "list_field": {Type: cty.List(cty.String), Optional: true}, 5557 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 5558 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 5559 }, 5560 BlockTypes: map[string]*configschema.NestedBlock{ 5561 "nested_block_set": { 5562 Block: configschema.Block{ 5563 Attributes: map[string]*configschema.Attribute{ 5564 "an_attr": {Type: cty.String, Optional: true}, 5565 "another": {Type: cty.String, Optional: true}, 5566 }, 5567 }, 5568 Nesting: configschema.NestingSet, 5569 }, 5570 }, 5571 }, 5572 ExpectedOutput: ` # test_instance.example will be destroyed 5573 - resource "test_instance" "example" { 5574 - ami = (sensitive) -> null 5575 - id = "i-02ae66f368e8518a9" -> null 5576 - list_field = [ 5577 - "hello", 5578 - (sensitive), 5579 ] -> null 5580 - map_key = { 5581 - "breakfast" = 800 5582 - "dinner" = (sensitive) 5583 } -> null 5584 - map_whole = (sensitive) -> null 5585 5586 - nested_block_set { 5587 # At least one attribute in this block is (or was) sensitive, 5588 # so its contents will not be displayed. 5589 } 5590 } 5591 `, 5592 }, 5593 "update with sensitive value forcing replacement": { 5594 Action: plans.DeleteThenCreate, 5595 Mode: addrs.ManagedResourceMode, 5596 Before: cty.ObjectVal(map[string]cty.Value{ 5597 "id": cty.StringVal("i-02ae66f368e8518a9"), 5598 "ami": cty.StringVal("ami-BEFORE"), 5599 "nested_block_set": cty.SetVal([]cty.Value{ 5600 cty.ObjectVal(map[string]cty.Value{ 5601 "an_attr": cty.StringVal("secret"), 5602 }), 5603 }), 5604 }), 5605 After: cty.ObjectVal(map[string]cty.Value{ 5606 "id": cty.StringVal("i-02ae66f368e8518a9"), 5607 "ami": cty.StringVal("ami-AFTER"), 5608 "nested_block_set": cty.SetVal([]cty.Value{ 5609 cty.ObjectVal(map[string]cty.Value{ 5610 "an_attr": cty.StringVal("changed"), 5611 }), 5612 }), 5613 }), 5614 BeforeValMarks: []cty.PathValueMarks{ 5615 { 5616 Path: cty.GetAttrPath("ami"), 5617 Marks: cty.NewValueMarks(marks.Sensitive), 5618 }, 5619 { 5620 Path: cty.GetAttrPath("nested_block_set"), 5621 Marks: cty.NewValueMarks(marks.Sensitive), 5622 }, 5623 }, 5624 AfterValMarks: []cty.PathValueMarks{ 5625 { 5626 Path: cty.GetAttrPath("ami"), 5627 Marks: cty.NewValueMarks(marks.Sensitive), 5628 }, 5629 { 5630 Path: cty.GetAttrPath("nested_block_set"), 5631 Marks: cty.NewValueMarks(marks.Sensitive), 5632 }, 5633 }, 5634 Schema: &configschema.Block{ 5635 Attributes: map[string]*configschema.Attribute{ 5636 "id": {Type: cty.String, Optional: true, Computed: true}, 5637 "ami": {Type: cty.String, Optional: true}, 5638 }, 5639 BlockTypes: map[string]*configschema.NestedBlock{ 5640 "nested_block_set": { 5641 Block: configschema.Block{ 5642 Attributes: map[string]*configschema.Attribute{ 5643 "an_attr": {Type: cty.String, Required: true}, 5644 }, 5645 }, 5646 Nesting: configschema.NestingSet, 5647 }, 5648 }, 5649 }, 5650 RequiredReplace: cty.NewPathSet( 5651 cty.GetAttrPath("ami"), 5652 cty.GetAttrPath("nested_block_set"), 5653 ), 5654 ExpectedOutput: ` # test_instance.example must be replaced 5655 -/+ resource "test_instance" "example" { 5656 ~ ami = (sensitive) # forces replacement 5657 id = "i-02ae66f368e8518a9" 5658 5659 ~ nested_block_set { # forces replacement 5660 # At least one attribute in this block is (or was) sensitive, 5661 # so its contents will not be displayed. 5662 } 5663 } 5664 `, 5665 }, 5666 "update with sensitive attribute forcing replacement": { 5667 Action: plans.DeleteThenCreate, 5668 Mode: addrs.ManagedResourceMode, 5669 Before: cty.ObjectVal(map[string]cty.Value{ 5670 "id": cty.StringVal("i-02ae66f368e8518a9"), 5671 "ami": cty.StringVal("ami-BEFORE"), 5672 }), 5673 After: cty.ObjectVal(map[string]cty.Value{ 5674 "id": cty.StringVal("i-02ae66f368e8518a9"), 5675 "ami": cty.StringVal("ami-AFTER"), 5676 }), 5677 Schema: &configschema.Block{ 5678 Attributes: map[string]*configschema.Attribute{ 5679 "id": {Type: cty.String, Optional: true, Computed: true}, 5680 "ami": {Type: cty.String, Optional: true, Computed: true, Sensitive: true}, 5681 }, 5682 }, 5683 RequiredReplace: cty.NewPathSet( 5684 cty.GetAttrPath("ami"), 5685 ), 5686 ExpectedOutput: ` # test_instance.example must be replaced 5687 -/+ resource "test_instance" "example" { 5688 ~ ami = (sensitive value) # forces replacement 5689 id = "i-02ae66f368e8518a9" 5690 } 5691 `, 5692 }, 5693 "update with sensitive nested type attribute forcing replacement": { 5694 Action: plans.DeleteThenCreate, 5695 Mode: addrs.ManagedResourceMode, 5696 Before: cty.ObjectVal(map[string]cty.Value{ 5697 "id": cty.StringVal("i-02ae66f368e8518a9"), 5698 "conn_info": cty.ObjectVal(map[string]cty.Value{ 5699 "user": cty.StringVal("not-secret"), 5700 "password": cty.StringVal("top-secret"), 5701 }), 5702 }), 5703 After: cty.ObjectVal(map[string]cty.Value{ 5704 "id": cty.StringVal("i-02ae66f368e8518a9"), 5705 "conn_info": cty.ObjectVal(map[string]cty.Value{ 5706 "user": cty.StringVal("not-secret"), 5707 "password": cty.StringVal("new-secret"), 5708 }), 5709 }), 5710 Schema: &configschema.Block{ 5711 Attributes: map[string]*configschema.Attribute{ 5712 "id": {Type: cty.String, Optional: true, Computed: true}, 5713 "conn_info": { 5714 NestedType: &configschema.Object{ 5715 Nesting: configschema.NestingSingle, 5716 Attributes: map[string]*configschema.Attribute{ 5717 "user": {Type: cty.String, Optional: true}, 5718 "password": {Type: cty.String, Optional: true, Sensitive: true}, 5719 }, 5720 }, 5721 }, 5722 }, 5723 }, 5724 RequiredReplace: cty.NewPathSet( 5725 cty.GetAttrPath("conn_info"), 5726 cty.GetAttrPath("password"), 5727 ), 5728 ExpectedOutput: ` # test_instance.example must be replaced 5729 -/+ resource "test_instance" "example" { 5730 ~ conn_info = { # forces replacement 5731 ~ password = (sensitive value) 5732 # (1 unchanged attribute hidden) 5733 } 5734 id = "i-02ae66f368e8518a9" 5735 } 5736 `, 5737 }, 5738 } 5739 runTestCases(t, testCases) 5740 } 5741 5742 func TestResourceChange_moved(t *testing.T) { 5743 prevRunAddr := addrs.Resource{ 5744 Mode: addrs.ManagedResourceMode, 5745 Type: "test_instance", 5746 Name: "previous", 5747 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 5748 5749 testCases := map[string]testCase{ 5750 "moved and updated": { 5751 PrevRunAddr: prevRunAddr, 5752 Action: plans.Update, 5753 Mode: addrs.ManagedResourceMode, 5754 Before: cty.ObjectVal(map[string]cty.Value{ 5755 "id": cty.StringVal("12345"), 5756 "foo": cty.StringVal("hello"), 5757 "bar": cty.StringVal("baz"), 5758 }), 5759 After: cty.ObjectVal(map[string]cty.Value{ 5760 "id": cty.StringVal("12345"), 5761 "foo": cty.StringVal("hello"), 5762 "bar": cty.StringVal("boop"), 5763 }), 5764 Schema: &configschema.Block{ 5765 Attributes: map[string]*configschema.Attribute{ 5766 "id": {Type: cty.String, Computed: true}, 5767 "foo": {Type: cty.String, Optional: true}, 5768 "bar": {Type: cty.String, Optional: true}, 5769 }, 5770 }, 5771 RequiredReplace: cty.NewPathSet(), 5772 ExpectedOutput: ` # test_instance.example will be updated in-place 5773 # (moved from test_instance.previous) 5774 ~ resource "test_instance" "example" { 5775 ~ bar = "baz" -> "boop" 5776 id = "12345" 5777 # (1 unchanged attribute hidden) 5778 } 5779 `, 5780 }, 5781 "moved without changes": { 5782 PrevRunAddr: prevRunAddr, 5783 Action: plans.NoOp, 5784 Mode: addrs.ManagedResourceMode, 5785 Before: cty.ObjectVal(map[string]cty.Value{ 5786 "id": cty.StringVal("12345"), 5787 "foo": cty.StringVal("hello"), 5788 "bar": cty.StringVal("baz"), 5789 }), 5790 After: cty.ObjectVal(map[string]cty.Value{ 5791 "id": cty.StringVal("12345"), 5792 "foo": cty.StringVal("hello"), 5793 "bar": cty.StringVal("baz"), 5794 }), 5795 Schema: &configschema.Block{ 5796 Attributes: map[string]*configschema.Attribute{ 5797 "id": {Type: cty.String, Computed: true}, 5798 "foo": {Type: cty.String, Optional: true}, 5799 "bar": {Type: cty.String, Optional: true}, 5800 }, 5801 }, 5802 RequiredReplace: cty.NewPathSet(), 5803 ExpectedOutput: ` # test_instance.previous has moved to test_instance.example 5804 resource "test_instance" "example" { 5805 id = "12345" 5806 # (2 unchanged attributes hidden) 5807 } 5808 `, 5809 }, 5810 } 5811 5812 runTestCases(t, testCases) 5813 } 5814 5815 type testCase struct { 5816 Action plans.Action 5817 ActionReason plans.ResourceInstanceChangeActionReason 5818 ModuleInst addrs.ModuleInstance 5819 Mode addrs.ResourceMode 5820 InstanceKey addrs.InstanceKey 5821 DeposedKey states.DeposedKey 5822 Before cty.Value 5823 BeforeValMarks []cty.PathValueMarks 5824 AfterValMarks []cty.PathValueMarks 5825 After cty.Value 5826 Schema *configschema.Block 5827 RequiredReplace cty.PathSet 5828 ExpectedOutput string 5829 PrevRunAddr addrs.AbsResourceInstance 5830 } 5831 5832 func runTestCases(t *testing.T, testCases map[string]testCase) { 5833 color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} 5834 5835 for name, tc := range testCases { 5836 t.Run(name, func(t *testing.T) { 5837 ty := tc.Schema.ImpliedType() 5838 5839 beforeVal := tc.Before 5840 switch { // Some fixups to make the test cases a little easier to write 5841 case beforeVal.IsNull(): 5842 beforeVal = cty.NullVal(ty) // allow mistyped nulls 5843 case !beforeVal.IsKnown(): 5844 beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns 5845 } 5846 5847 afterVal := tc.After 5848 switch { // Some fixups to make the test cases a little easier to write 5849 case afterVal.IsNull(): 5850 afterVal = cty.NullVal(ty) // allow mistyped nulls 5851 case !afterVal.IsKnown(): 5852 afterVal = cty.UnknownVal(ty) // allow mistyped unknowns 5853 } 5854 5855 addr := addrs.Resource{ 5856 Mode: tc.Mode, 5857 Type: "test_instance", 5858 Name: "example", 5859 }.Instance(tc.InstanceKey).Absolute(tc.ModuleInst) 5860 5861 prevRunAddr := tc.PrevRunAddr 5862 // If no previous run address is given, reuse the current address 5863 // to make initialization easier 5864 if prevRunAddr.Resource.Resource.Type == "" { 5865 prevRunAddr = addr 5866 } 5867 5868 change := &plans.ResourceInstanceChange{ 5869 Addr: addr, 5870 PrevRunAddr: prevRunAddr, 5871 DeposedKey: tc.DeposedKey, 5872 ProviderAddr: addrs.AbsProviderConfig{ 5873 Provider: addrs.NewDefaultProvider("test"), 5874 Module: addrs.RootModule, 5875 }, 5876 Change: plans.Change{ 5877 Action: tc.Action, 5878 Before: beforeVal.MarkWithPaths(tc.BeforeValMarks), 5879 After: afterVal.MarkWithPaths(tc.AfterValMarks), 5880 }, 5881 ActionReason: tc.ActionReason, 5882 RequiredReplace: tc.RequiredReplace, 5883 } 5884 5885 output := ResourceChange(change, tc.Schema, color, DiffLanguageProposedChange) 5886 if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" { 5887 t.Errorf("wrong output\n%s", diff) 5888 } 5889 }) 5890 } 5891 } 5892 5893 func TestOutputChanges(t *testing.T) { 5894 color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} 5895 5896 testCases := map[string]struct { 5897 changes []*plans.OutputChangeSrc 5898 output string 5899 }{ 5900 "new output value": { 5901 []*plans.OutputChangeSrc{ 5902 outputChange( 5903 "foo", 5904 cty.NullVal(cty.DynamicPseudoType), 5905 cty.StringVal("bar"), 5906 false, 5907 ), 5908 }, 5909 ` 5910 + foo = "bar"`, 5911 }, 5912 "removed output": { 5913 []*plans.OutputChangeSrc{ 5914 outputChange( 5915 "foo", 5916 cty.StringVal("bar"), 5917 cty.NullVal(cty.DynamicPseudoType), 5918 false, 5919 ), 5920 }, 5921 ` 5922 - foo = "bar" -> null`, 5923 }, 5924 "single string change": { 5925 []*plans.OutputChangeSrc{ 5926 outputChange( 5927 "foo", 5928 cty.StringVal("bar"), 5929 cty.StringVal("baz"), 5930 false, 5931 ), 5932 }, 5933 ` 5934 ~ foo = "bar" -> "baz"`, 5935 }, 5936 "element added to list": { 5937 []*plans.OutputChangeSrc{ 5938 outputChange( 5939 "foo", 5940 cty.ListVal([]cty.Value{ 5941 cty.StringVal("alpha"), 5942 cty.StringVal("beta"), 5943 cty.StringVal("delta"), 5944 cty.StringVal("epsilon"), 5945 }), 5946 cty.ListVal([]cty.Value{ 5947 cty.StringVal("alpha"), 5948 cty.StringVal("beta"), 5949 cty.StringVal("gamma"), 5950 cty.StringVal("delta"), 5951 cty.StringVal("epsilon"), 5952 }), 5953 false, 5954 ), 5955 }, 5956 ` 5957 ~ foo = [ 5958 # (1 unchanged element hidden) 5959 "beta", 5960 + "gamma", 5961 "delta", 5962 # (1 unchanged element hidden) 5963 ]`, 5964 }, 5965 "multiple outputs changed, one sensitive": { 5966 []*plans.OutputChangeSrc{ 5967 outputChange( 5968 "a", 5969 cty.NumberIntVal(1), 5970 cty.NumberIntVal(2), 5971 false, 5972 ), 5973 outputChange( 5974 "b", 5975 cty.StringVal("hunter2"), 5976 cty.StringVal("correct-horse-battery-staple"), 5977 true, 5978 ), 5979 outputChange( 5980 "c", 5981 cty.BoolVal(false), 5982 cty.BoolVal(true), 5983 false, 5984 ), 5985 }, 5986 ` 5987 ~ a = 1 -> 2 5988 ~ b = (sensitive value) 5989 ~ c = false -> true`, 5990 }, 5991 } 5992 5993 for name, tc := range testCases { 5994 t.Run(name, func(t *testing.T) { 5995 output := OutputChanges(tc.changes, color) 5996 if output != tc.output { 5997 t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output) 5998 } 5999 }) 6000 } 6001 } 6002 6003 func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc { 6004 addr := addrs.AbsOutputValue{ 6005 OutputValue: addrs.OutputValue{Name: name}, 6006 } 6007 6008 change := &plans.OutputChange{ 6009 Addr: addr, Change: plans.Change{ 6010 Before: before, 6011 After: after, 6012 }, 6013 Sensitive: sensitive, 6014 } 6015 6016 changeSrc, err := change.Encode() 6017 if err != nil { 6018 panic(fmt.Sprintf("failed to encode change for %s: %s", addr, err)) 6019 } 6020 6021 return changeSrc 6022 } 6023 6024 // A basic test schema using a configurable NestingMode for one (NestedType) attribute and one block 6025 func testSchema(nesting configschema.NestingMode) *configschema.Block { 6026 return &configschema.Block{ 6027 Attributes: map[string]*configschema.Attribute{ 6028 "id": {Type: cty.String, Optional: true, Computed: true}, 6029 "ami": {Type: cty.String, Optional: true}, 6030 "disks": { 6031 NestedType: &configschema.Object{ 6032 Attributes: map[string]*configschema.Attribute{ 6033 "mount_point": {Type: cty.String, Optional: true}, 6034 "size": {Type: cty.String, Optional: true}, 6035 }, 6036 Nesting: nesting, 6037 }, 6038 }, 6039 }, 6040 BlockTypes: map[string]*configschema.NestedBlock{ 6041 "root_block_device": { 6042 Block: configschema.Block{ 6043 Attributes: map[string]*configschema.Attribute{ 6044 "volume_type": { 6045 Type: cty.String, 6046 Optional: true, 6047 Computed: true, 6048 }, 6049 }, 6050 }, 6051 Nesting: nesting, 6052 }, 6053 }, 6054 } 6055 } 6056 6057 func testSchemaMultipleBlocks(nesting configschema.NestingMode) *configschema.Block { 6058 return &configschema.Block{ 6059 Attributes: map[string]*configschema.Attribute{ 6060 "id": {Type: cty.String, Optional: true, Computed: true}, 6061 "ami": {Type: cty.String, Optional: true}, 6062 "disks": { 6063 NestedType: &configschema.Object{ 6064 Attributes: map[string]*configschema.Attribute{ 6065 "mount_point": {Type: cty.String, Optional: true}, 6066 "size": {Type: cty.String, Optional: true}, 6067 }, 6068 Nesting: nesting, 6069 }, 6070 }, 6071 }, 6072 BlockTypes: map[string]*configschema.NestedBlock{ 6073 "root_block_device": { 6074 Block: configschema.Block{ 6075 Attributes: map[string]*configschema.Attribute{ 6076 "volume_type": { 6077 Type: cty.String, 6078 Optional: true, 6079 Computed: true, 6080 }, 6081 }, 6082 }, 6083 Nesting: nesting, 6084 }, 6085 "leaf_block_device": { 6086 Block: configschema.Block{ 6087 Attributes: map[string]*configschema.Attribute{ 6088 "volume_type": { 6089 Type: cty.String, 6090 Optional: true, 6091 Computed: true, 6092 }, 6093 }, 6094 }, 6095 Nesting: nesting, 6096 }, 6097 }, 6098 } 6099 } 6100 6101 // similar to testSchema with the addition of a "new_field" block 6102 func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block { 6103 return &configschema.Block{ 6104 Attributes: map[string]*configschema.Attribute{ 6105 "id": {Type: cty.String, Optional: true, Computed: true}, 6106 "ami": {Type: cty.String, Optional: true}, 6107 "disks": { 6108 NestedType: &configschema.Object{ 6109 Attributes: map[string]*configschema.Attribute{ 6110 "mount_point": {Type: cty.String, Optional: true}, 6111 "size": {Type: cty.String, Optional: true}, 6112 }, 6113 Nesting: nesting, 6114 }, 6115 }, 6116 }, 6117 BlockTypes: map[string]*configschema.NestedBlock{ 6118 "root_block_device": { 6119 Block: configschema.Block{ 6120 Attributes: map[string]*configschema.Attribute{ 6121 "volume_type": { 6122 Type: cty.String, 6123 Optional: true, 6124 Computed: true, 6125 }, 6126 "new_field": { 6127 Type: cty.String, 6128 Optional: true, 6129 Computed: true, 6130 }, 6131 }, 6132 }, 6133 Nesting: nesting, 6134 }, 6135 }, 6136 } 6137 }