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