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