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