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