github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-internal/terraform/context_plan2_test.go (about) 1 package terraform 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "strings" 8 "testing" 9 10 "github.com/davecgh/go-spew/spew" 11 "github.com/google/go-cmp/cmp" 12 "github.com/muratcelep/terraform/not-internal/addrs" 13 "github.com/muratcelep/terraform/not-internal/configs/configschema" 14 "github.com/muratcelep/terraform/not-internal/lang/marks" 15 "github.com/muratcelep/terraform/not-internal/plans" 16 "github.com/muratcelep/terraform/not-internal/providers" 17 "github.com/muratcelep/terraform/not-internal/states" 18 "github.com/muratcelep/terraform/not-internal/tfdiags" 19 "github.com/zclconf/go-cty/cty" 20 ) 21 22 func TestContext2Plan_removedDuringRefresh(t *testing.T) { 23 // This tests the situation where an object tracked in the previous run 24 // state has been deleted outside of Terraform, which we should detect 25 // during the refresh step and thus ultimately produce a plan to recreate 26 // the object, since it's still present in the configuration. 27 m := testModuleInline(t, map[string]string{ 28 "main.tf": ` 29 resource "test_object" "a" { 30 } 31 `, 32 }) 33 34 p := simpleMockProvider() 35 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 36 Provider: providers.Schema{Block: simpleTestSchema()}, 37 ResourceTypes: map[string]providers.Schema{ 38 "test_object": { 39 Block: &configschema.Block{ 40 Attributes: map[string]*configschema.Attribute{ 41 "arg": {Type: cty.String, Optional: true}, 42 }, 43 }, 44 }, 45 }, 46 } 47 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 48 resp.NewState = cty.NullVal(req.PriorState.Type()) 49 return resp 50 } 51 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 52 // We should've been given the prior state JSON as our input to upgrade. 53 if !bytes.Contains(req.RawStateJSON, []byte("previous_run")) { 54 t.Fatalf("UpgradeResourceState request doesn't contain the previous run object\n%s", req.RawStateJSON) 55 } 56 57 // We'll put something different in "arg" as part of upgrading, just 58 // so that we can verify below that PrevRunState contains the upgraded 59 // (but NOT refreshed) version of the object. 60 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 61 "arg": cty.StringVal("upgraded"), 62 }) 63 return resp 64 } 65 66 addr := mustResourceInstanceAddr("test_object.a") 67 state := states.BuildState(func(s *states.SyncState) { 68 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 69 AttrsJSON: []byte(`{"arg":"previous_run"}`), 70 Status: states.ObjectTainted, 71 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 72 }) 73 74 ctx := testContext2(t, &ContextOpts{ 75 Providers: map[addrs.Provider]providers.Factory{ 76 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 77 }, 78 }) 79 80 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 81 assertNoErrors(t, diags) 82 83 if !p.UpgradeResourceStateCalled { 84 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 85 } 86 if !p.ReadResourceCalled { 87 t.Errorf("Provider's ReadResource wasn't called; should've been") 88 } 89 90 // The object should be absent from the plan's prior state, because that 91 // records the result of refreshing. 92 if got := plan.PriorState.ResourceInstance(addr); got != nil { 93 t.Errorf( 94 "instance %s is in the prior state after planning; should've been removed\n%s", 95 addr, spew.Sdump(got), 96 ) 97 } 98 99 // However, the object should still be in the PrevRunState, because 100 // that reflects what we believed to exist before refreshing. 101 if got := plan.PrevRunState.ResourceInstance(addr); got == nil { 102 t.Errorf( 103 "instance %s is missing from the previous run state after planning; should've been preserved", 104 addr, 105 ) 106 } else { 107 if !bytes.Contains(got.Current.AttrsJSON, []byte("upgraded")) { 108 t.Fatalf("previous run state has non-upgraded object\n%s", got.Current.AttrsJSON) 109 } 110 } 111 112 // This situation should result in a drifted resource change. 113 var drifted *plans.ResourceInstanceChangeSrc 114 for _, dr := range plan.DriftedResources { 115 if dr.Addr.Equal(addr) { 116 drifted = dr 117 break 118 } 119 } 120 121 if drifted == nil { 122 t.Errorf("instance %s is missing from the drifted resource changes", addr) 123 } else { 124 if got, want := drifted.Action, plans.Delete; got != want { 125 t.Errorf("unexpected instance %s drifted resource change action. got: %s, want: %s", addr, got, want) 126 } 127 } 128 129 // Because the configuration still mentions test_object.a, we should've 130 // planned to recreate it in order to fix the drift. 131 for _, c := range plan.Changes.Resources { 132 if c.Action != plans.Create { 133 t.Fatalf("expected Create action for missing %s, got %s", c.Addr, c.Action) 134 } 135 } 136 } 137 138 func TestContext2Plan_noChangeDataSourceSensitiveNestedSet(t *testing.T) { 139 m := testModuleInline(t, map[string]string{ 140 "main.tf": ` 141 variable "bar" { 142 sensitive = true 143 default = "baz" 144 } 145 146 data "test_data_source" "foo" { 147 foo { 148 bar = var.bar 149 } 150 } 151 `, 152 }) 153 154 p := new(MockProvider) 155 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 156 DataSources: map[string]*configschema.Block{ 157 "test_data_source": { 158 Attributes: map[string]*configschema.Attribute{ 159 "id": { 160 Type: cty.String, 161 Computed: true, 162 }, 163 }, 164 BlockTypes: map[string]*configschema.NestedBlock{ 165 "foo": { 166 Block: configschema.Block{ 167 Attributes: map[string]*configschema.Attribute{ 168 "bar": {Type: cty.String, Optional: true}, 169 }, 170 }, 171 Nesting: configschema.NestingSet, 172 }, 173 }, 174 }, 175 }, 176 }) 177 178 p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ 179 State: cty.ObjectVal(map[string]cty.Value{ 180 "id": cty.StringVal("data_id"), 181 "foo": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")})}), 182 }), 183 } 184 185 state := states.NewState() 186 root := state.EnsureModule(addrs.RootModuleInstance) 187 root.SetResourceInstanceCurrent( 188 mustResourceInstanceAddr("data.test_data_source.foo").Resource, 189 &states.ResourceInstanceObjectSrc{ 190 Status: states.ObjectReady, 191 AttrsJSON: []byte(`{"id":"data_id", "foo":[{"bar":"baz"}]}`), 192 AttrSensitivePaths: []cty.PathValueMarks{ 193 { 194 Path: cty.GetAttrPath("foo"), 195 Marks: cty.NewValueMarks(marks.Sensitive), 196 }, 197 }, 198 }, 199 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 200 ) 201 202 ctx := testContext2(t, &ContextOpts{ 203 Providers: map[addrs.Provider]providers.Factory{ 204 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 205 }, 206 }) 207 208 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 209 assertNoErrors(t, diags) 210 211 for _, res := range plan.Changes.Resources { 212 if res.Action != plans.NoOp { 213 t.Fatalf("expected NoOp, got: %q %s", res.Addr, res.Action) 214 } 215 } 216 } 217 218 func TestContext2Plan_orphanDataInstance(t *testing.T) { 219 // ensure the planned replacement of the data source is evaluated properly 220 m := testModuleInline(t, map[string]string{ 221 "main.tf": ` 222 data "test_object" "a" { 223 for_each = { new = "ok" } 224 } 225 226 output "out" { 227 value = [ for k, _ in data.test_object.a: k ] 228 } 229 `, 230 }) 231 232 p := simpleMockProvider() 233 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 234 resp.State = req.Config 235 return resp 236 } 237 238 state := states.BuildState(func(s *states.SyncState) { 239 s.SetResourceInstanceCurrent(mustResourceInstanceAddr(`data.test_object.a["old"]`), &states.ResourceInstanceObjectSrc{ 240 AttrsJSON: []byte(`{"test_string":"foo"}`), 241 Status: states.ObjectReady, 242 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 243 }) 244 245 ctx := testContext2(t, &ContextOpts{ 246 Providers: map[addrs.Provider]providers.Factory{ 247 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 248 }, 249 }) 250 251 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 252 assertNoErrors(t, diags) 253 254 change, err := plan.Changes.Outputs[0].Decode() 255 if err != nil { 256 t.Fatal(err) 257 } 258 259 expected := cty.TupleVal([]cty.Value{cty.StringVal("new")}) 260 261 if change.After.Equals(expected).False() { 262 t.Fatalf("expected %#v, got %#v\n", expected, change.After) 263 } 264 } 265 266 func TestContext2Plan_basicConfigurationAliases(t *testing.T) { 267 m := testModuleInline(t, map[string]string{ 268 "main.tf": ` 269 provider "test" { 270 alias = "z" 271 test_string = "config" 272 } 273 274 module "mod" { 275 source = "./mod" 276 providers = { 277 test.x = test.z 278 } 279 } 280 `, 281 282 "mod/main.tf": ` 283 terraform { 284 required_providers { 285 test = { 286 source = "registry.terraform.io/hashicorp/test" 287 configuration_aliases = [ test.x ] 288 } 289 } 290 } 291 292 resource "test_object" "a" { 293 provider = test.x 294 } 295 296 `, 297 }) 298 299 p := simpleMockProvider() 300 301 // The resource within the module should be using the provider configured 302 // from the root module. We should never see an empty configuration. 303 p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 304 if req.Config.GetAttr("test_string").IsNull() { 305 resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing test_string value")) 306 } 307 return resp 308 } 309 310 ctx := testContext2(t, &ContextOpts{ 311 Providers: map[addrs.Provider]providers.Factory{ 312 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 313 }, 314 }) 315 316 _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 317 assertNoErrors(t, diags) 318 } 319 320 func TestContext2Plan_dataReferencesResourceInModules(t *testing.T) { 321 p := testProvider("test") 322 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 323 cfg := req.Config.AsValueMap() 324 cfg["id"] = cty.StringVal("d") 325 resp.State = cty.ObjectVal(cfg) 326 return resp 327 } 328 329 m := testModuleInline(t, map[string]string{ 330 "main.tf": ` 331 locals { 332 things = { 333 old = "first" 334 new = "second" 335 } 336 } 337 338 module "mod" { 339 source = "./mod" 340 for_each = local.things 341 } 342 `, 343 344 "./mod/main.tf": ` 345 resource "test_resource" "a" { 346 } 347 348 data "test_data_source" "d" { 349 depends_on = [test_resource.a] 350 } 351 352 resource "test_resource" "b" { 353 value = data.test_data_source.d.id 354 } 355 `}) 356 357 oldDataAddr := mustResourceInstanceAddr(`module.mod["old"].data.test_data_source.d`) 358 359 state := states.BuildState(func(s *states.SyncState) { 360 s.SetResourceInstanceCurrent( 361 mustResourceInstanceAddr(`module.mod["old"].test_resource.a`), 362 &states.ResourceInstanceObjectSrc{ 363 AttrsJSON: []byte(`{"id":"a"}`), 364 Status: states.ObjectReady, 365 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 366 ) 367 s.SetResourceInstanceCurrent( 368 mustResourceInstanceAddr(`module.mod["old"].test_resource.b`), 369 &states.ResourceInstanceObjectSrc{ 370 AttrsJSON: []byte(`{"id":"b","value":"d"}`), 371 Status: states.ObjectReady, 372 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 373 ) 374 s.SetResourceInstanceCurrent( 375 oldDataAddr, 376 &states.ResourceInstanceObjectSrc{ 377 AttrsJSON: []byte(`{"id":"d"}`), 378 Status: states.ObjectReady, 379 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 380 ) 381 }) 382 383 ctx := testContext2(t, &ContextOpts{ 384 Providers: map[addrs.Provider]providers.Factory{ 385 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 386 }, 387 }) 388 389 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 390 assertNoErrors(t, diags) 391 392 oldMod := oldDataAddr.Module 393 394 for _, c := range plan.Changes.Resources { 395 // there should be no changes from the old module instance 396 if c.Addr.Module.Equal(oldMod) && c.Action != plans.NoOp { 397 t.Errorf("unexpected change %s for %s\n", c.Action, c.Addr) 398 } 399 } 400 } 401 402 func TestContext2Plan_destroyWithRefresh(t *testing.T) { 403 m := testModuleInline(t, map[string]string{ 404 "main.tf": ` 405 resource "test_object" "a" { 406 } 407 `, 408 }) 409 410 p := simpleMockProvider() 411 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 412 Provider: providers.Schema{Block: simpleTestSchema()}, 413 ResourceTypes: map[string]providers.Schema{ 414 "test_object": { 415 Block: &configschema.Block{ 416 Attributes: map[string]*configschema.Attribute{ 417 "arg": {Type: cty.String, Optional: true}, 418 }, 419 }, 420 }, 421 }, 422 } 423 424 // This is called from the first instance of this provider, so we can't 425 // check p.ReadResourceCalled after plan. 426 readResourceCalled := false 427 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 428 readResourceCalled = true 429 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 430 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 431 return cty.StringVal("current"), nil 432 } 433 return v, nil 434 }) 435 if err != nil { 436 // shouldn't get here 437 t.Fatalf("ReadResourceFn transform failed") 438 return providers.ReadResourceResponse{} 439 } 440 return providers.ReadResourceResponse{ 441 NewState: newVal, 442 } 443 } 444 445 upgradeResourceStateCalled := false 446 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 447 upgradeResourceStateCalled = true 448 t.Logf("UpgradeResourceState %s", req.RawStateJSON) 449 450 // In the destroy-with-refresh codepath we end up calling 451 // UpgradeResourceState twice, because we do so once during refreshing 452 // (as part making a normal plan) and then again during the plan-destroy 453 // walk. The second call recieves the result of the earlier refresh, 454 // so we need to tolerate both "before" and "current" as possible 455 // inputs here. 456 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 457 if !bytes.Contains(req.RawStateJSON, []byte("current")) { 458 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object or the 'current' object\n%s", req.RawStateJSON) 459 } 460 } 461 462 // We'll put something different in "arg" as part of upgrading, just 463 // so that we can verify below that PrevRunState contains the upgraded 464 // (but NOT refreshed) version of the object. 465 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 466 "arg": cty.StringVal("upgraded"), 467 }) 468 return resp 469 } 470 471 addr := mustResourceInstanceAddr("test_object.a") 472 state := states.BuildState(func(s *states.SyncState) { 473 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 474 AttrsJSON: []byte(`{"arg":"before"}`), 475 Status: states.ObjectReady, 476 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 477 }) 478 479 ctx := testContext2(t, &ContextOpts{ 480 Providers: map[addrs.Provider]providers.Factory{ 481 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 482 }, 483 }) 484 485 plan, diags := ctx.Plan(m, state, &PlanOpts{ 486 Mode: plans.DestroyMode, 487 SkipRefresh: false, // the default 488 }) 489 assertNoErrors(t, diags) 490 491 if !upgradeResourceStateCalled { 492 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 493 } 494 if !readResourceCalled { 495 t.Errorf("Provider's ReadResource wasn't called; should've been") 496 } 497 498 if plan.PriorState == nil { 499 t.Fatal("missing plan state") 500 } 501 502 for _, c := range plan.Changes.Resources { 503 if c.Action != plans.Delete { 504 t.Errorf("unexpected %s change for %s", c.Action, c.Addr) 505 } 506 } 507 508 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 509 t.Errorf("%s has no previous run state at all after plan", addr) 510 } else { 511 if instState.Current == nil { 512 t.Errorf("%s has no current object in the previous run state", addr) 513 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 514 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 515 } 516 } 517 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 518 t.Errorf("%s has no prior state at all after plan", addr) 519 } else { 520 if instState.Current == nil { 521 t.Errorf("%s has no current object in the prior state", addr) 522 } else if got, want := instState.Current.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 523 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 524 } 525 } 526 } 527 528 func TestContext2Plan_destroySkipRefresh(t *testing.T) { 529 m := testModuleInline(t, map[string]string{ 530 "main.tf": ` 531 resource "test_object" "a" { 532 } 533 `, 534 }) 535 536 p := simpleMockProvider() 537 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 538 Provider: providers.Schema{Block: simpleTestSchema()}, 539 ResourceTypes: map[string]providers.Schema{ 540 "test_object": { 541 Block: &configschema.Block{ 542 Attributes: map[string]*configschema.Attribute{ 543 "arg": {Type: cty.String, Optional: true}, 544 }, 545 }, 546 }, 547 }, 548 } 549 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 550 t.Helper() 551 t.Errorf("unexpected call to ReadResource") 552 resp.NewState = req.PriorState 553 return resp 554 } 555 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 556 t.Logf("UpgradeResourceState %s", req.RawStateJSON) 557 // We should've been given the prior state JSON as our input to upgrade. 558 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 559 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 560 } 561 562 // We'll put something different in "arg" as part of upgrading, just 563 // so that we can verify below that PrevRunState contains the upgraded 564 // (but NOT refreshed) version of the object. 565 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 566 "arg": cty.StringVal("upgraded"), 567 }) 568 return resp 569 } 570 571 addr := mustResourceInstanceAddr("test_object.a") 572 state := states.BuildState(func(s *states.SyncState) { 573 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 574 AttrsJSON: []byte(`{"arg":"before"}`), 575 Status: states.ObjectReady, 576 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 577 }) 578 579 ctx := testContext2(t, &ContextOpts{ 580 Providers: map[addrs.Provider]providers.Factory{ 581 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 582 }, 583 }) 584 585 plan, diags := ctx.Plan(m, state, &PlanOpts{ 586 Mode: plans.DestroyMode, 587 SkipRefresh: true, 588 }) 589 assertNoErrors(t, diags) 590 591 if !p.UpgradeResourceStateCalled { 592 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 593 } 594 if p.ReadResourceCalled { 595 t.Errorf("Provider's ReadResource was called; shouldn't have been") 596 } 597 598 if plan.PriorState == nil { 599 t.Fatal("missing plan state") 600 } 601 602 for _, c := range plan.Changes.Resources { 603 if c.Action != plans.Delete { 604 t.Errorf("unexpected %s change for %s", c.Action, c.Addr) 605 } 606 } 607 608 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 609 t.Errorf("%s has no previous run state at all after plan", addr) 610 } else { 611 if instState.Current == nil { 612 t.Errorf("%s has no current object in the previous run state", addr) 613 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 614 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 615 } 616 } 617 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 618 t.Errorf("%s has no prior state at all after plan", addr) 619 } else { 620 if instState.Current == nil { 621 t.Errorf("%s has no current object in the prior state", addr) 622 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 623 // NOTE: The prior state should still have been _upgraded_, even 624 // though we skipped running refresh after upgrading it. 625 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 626 } 627 } 628 } 629 630 func TestContext2Plan_unmarkingSensitiveAttributeForOutput(t *testing.T) { 631 m := testModuleInline(t, map[string]string{ 632 "main.tf": ` 633 resource "test_resource" "foo" { 634 } 635 636 output "result" { 637 value = nonsensitive(test_resource.foo.sensitive_attr) 638 } 639 `, 640 }) 641 642 p := new(MockProvider) 643 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 644 ResourceTypes: map[string]*configschema.Block{ 645 "test_resource": { 646 Attributes: map[string]*configschema.Attribute{ 647 "id": { 648 Type: cty.String, 649 Computed: true, 650 }, 651 "sensitive_attr": { 652 Type: cty.String, 653 Computed: true, 654 Sensitive: true, 655 }, 656 }, 657 }, 658 }, 659 }) 660 661 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 662 return providers.PlanResourceChangeResponse{ 663 PlannedState: cty.UnknownVal(cty.Object(map[string]cty.Type{ 664 "id": cty.String, 665 "sensitive_attr": cty.String, 666 })), 667 } 668 } 669 670 state := states.NewState() 671 672 ctx := testContext2(t, &ContextOpts{ 673 Providers: map[addrs.Provider]providers.Factory{ 674 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 675 }, 676 }) 677 678 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 679 assertNoErrors(t, diags) 680 681 for _, res := range plan.Changes.Resources { 682 if res.Action != plans.Create { 683 t.Fatalf("expected create, got: %q %s", res.Addr, res.Action) 684 } 685 } 686 } 687 688 func TestContext2Plan_destroyNoProviderConfig(t *testing.T) { 689 // providers do not need to be configured during a destroy plan 690 p := simpleMockProvider() 691 p.ValidateProviderConfigFn = func(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { 692 v := req.Config.GetAttr("test_string") 693 if v.IsNull() || !v.IsKnown() || v.AsString() != "ok" { 694 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("invalid provider configuration: %#v", req.Config)) 695 } 696 return resp 697 } 698 699 m := testModuleInline(t, map[string]string{ 700 "main.tf": ` 701 locals { 702 value = "ok" 703 } 704 705 provider "test" { 706 test_string = local.value 707 } 708 `, 709 }) 710 711 addr := mustResourceInstanceAddr("test_object.a") 712 state := states.BuildState(func(s *states.SyncState) { 713 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 714 AttrsJSON: []byte(`{"test_string":"foo"}`), 715 Status: states.ObjectReady, 716 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 717 }) 718 719 ctx := testContext2(t, &ContextOpts{ 720 Providers: map[addrs.Provider]providers.Factory{ 721 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 722 }, 723 }) 724 725 _, diags := ctx.Plan(m, state, &PlanOpts{ 726 Mode: plans.DestroyMode, 727 }) 728 assertNoErrors(t, diags) 729 } 730 731 func TestContext2Plan_movedResourceBasic(t *testing.T) { 732 addrA := mustResourceInstanceAddr("test_object.a") 733 addrB := mustResourceInstanceAddr("test_object.b") 734 m := testModuleInline(t, map[string]string{ 735 "main.tf": ` 736 resource "test_object" "b" { 737 } 738 739 moved { 740 from = test_object.a 741 to = test_object.b 742 } 743 `, 744 }) 745 746 state := states.BuildState(func(s *states.SyncState) { 747 // The prior state tracks test_object.a, which we should treat as 748 // test_object.b because of the "moved" block in the config. 749 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 750 AttrsJSON: []byte(`{}`), 751 Status: states.ObjectReady, 752 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 753 }) 754 755 p := simpleMockProvider() 756 ctx := testContext2(t, &ContextOpts{ 757 Providers: map[addrs.Provider]providers.Factory{ 758 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 759 }, 760 }) 761 762 plan, diags := ctx.Plan(m, state, &PlanOpts{ 763 Mode: plans.NormalMode, 764 ForceReplace: []addrs.AbsResourceInstance{ 765 addrA, 766 }, 767 }) 768 if diags.HasErrors() { 769 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 770 } 771 772 t.Run(addrA.String(), func(t *testing.T) { 773 instPlan := plan.Changes.ResourceInstance(addrA) 774 if instPlan != nil { 775 t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) 776 } 777 }) 778 t.Run(addrB.String(), func(t *testing.T) { 779 instPlan := plan.Changes.ResourceInstance(addrB) 780 if instPlan == nil { 781 t.Fatalf("no plan for %s at all", addrB) 782 } 783 784 if got, want := instPlan.Addr, addrB; !got.Equal(want) { 785 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 786 } 787 if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { 788 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 789 } 790 if got, want := instPlan.Action, plans.NoOp; got != want { 791 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 792 } 793 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 794 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 795 } 796 }) 797 } 798 799 func TestContext2Plan_movedResourceCollision(t *testing.T) { 800 addrNoKey := mustResourceInstanceAddr("test_object.a") 801 addrZeroKey := mustResourceInstanceAddr("test_object.a[0]") 802 m := testModuleInline(t, map[string]string{ 803 "main.tf": ` 804 resource "test_object" "a" { 805 # No "count" set, so test_object.a[0] will want 806 # to implicitly move to test_object.a, but will get 807 # blocked by the existing object at that address. 808 } 809 `, 810 }) 811 812 state := states.BuildState(func(s *states.SyncState) { 813 s.SetResourceInstanceCurrent(addrNoKey, &states.ResourceInstanceObjectSrc{ 814 AttrsJSON: []byte(`{}`), 815 Status: states.ObjectReady, 816 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 817 s.SetResourceInstanceCurrent(addrZeroKey, &states.ResourceInstanceObjectSrc{ 818 AttrsJSON: []byte(`{}`), 819 Status: states.ObjectReady, 820 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 821 }) 822 823 p := simpleMockProvider() 824 ctx := testContext2(t, &ContextOpts{ 825 Providers: map[addrs.Provider]providers.Factory{ 826 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 827 }, 828 }) 829 830 plan, diags := ctx.Plan(m, state, &PlanOpts{ 831 Mode: plans.NormalMode, 832 }) 833 if diags.HasErrors() { 834 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 835 } 836 837 // We should have a warning, though! We'll lightly abuse the "for RPC" 838 // feature of diagnostics to get some more-readily-comparable diagnostic 839 // values. 840 gotDiags := diags.ForRPC() 841 wantDiags := tfdiags.Diagnostics{ 842 tfdiags.Sourceless( 843 tfdiags.Warning, 844 "Unresolved resource instance address changes", 845 `Terraform tried to adjust resource instance addresses in the prior state based on change information recorded in the configuration, but some adjustments did not succeed due to existing objects already at the intended addresses: 846 - test_object.a[0] could not move to test_object.a 847 848 Terraform has planned to destroy these objects. If Terraform's proposed changes aren't appropriate, you must first resolve the conflicts using the "terraform state" subcommands and then create a new plan.`, 849 ), 850 }.ForRPC() 851 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 852 t.Errorf("wrong diagnostics\n%s", diff) 853 } 854 855 t.Run(addrNoKey.String(), func(t *testing.T) { 856 instPlan := plan.Changes.ResourceInstance(addrNoKey) 857 if instPlan == nil { 858 t.Fatalf("no plan for %s at all", addrNoKey) 859 } 860 861 if got, want := instPlan.Addr, addrNoKey; !got.Equal(want) { 862 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 863 } 864 if got, want := instPlan.PrevRunAddr, addrNoKey; !got.Equal(want) { 865 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 866 } 867 if got, want := instPlan.Action, plans.NoOp; got != want { 868 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 869 } 870 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 871 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 872 } 873 }) 874 t.Run(addrZeroKey.String(), func(t *testing.T) { 875 instPlan := plan.Changes.ResourceInstance(addrZeroKey) 876 if instPlan == nil { 877 t.Fatalf("no plan for %s at all", addrZeroKey) 878 } 879 880 if got, want := instPlan.Addr, addrZeroKey; !got.Equal(want) { 881 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 882 } 883 if got, want := instPlan.PrevRunAddr, addrZeroKey; !got.Equal(want) { 884 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 885 } 886 if got, want := instPlan.Action, plans.Delete; got != want { 887 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 888 } 889 if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseWrongRepetition; got != want { 890 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 891 } 892 }) 893 } 894 895 func TestContext2Plan_movedResourceCollisionDestroy(t *testing.T) { 896 // This is like TestContext2Plan_movedResourceCollision but intended to 897 // ensure we still produce the expected warning (and produce it only once) 898 // when we're creating a destroy plan, rather than a normal plan. 899 // (This case is interesting at the time of writing because we happen to 900 // use a normal plan as a trick to refresh before creating a destroy plan. 901 // This test will probably become uninteresting if a future change to 902 // the destroy-time planning behavior handles refreshing in a different 903 // way, which avoids this pre-processing step of running a normal plan 904 // first.) 905 906 addrNoKey := mustResourceInstanceAddr("test_object.a") 907 addrZeroKey := mustResourceInstanceAddr("test_object.a[0]") 908 m := testModuleInline(t, map[string]string{ 909 "main.tf": ` 910 resource "test_object" "a" { 911 # No "count" set, so test_object.a[0] will want 912 # to implicitly move to test_object.a, but will get 913 # blocked by the existing object at that address. 914 } 915 `, 916 }) 917 918 state := states.BuildState(func(s *states.SyncState) { 919 s.SetResourceInstanceCurrent(addrNoKey, &states.ResourceInstanceObjectSrc{ 920 AttrsJSON: []byte(`{}`), 921 Status: states.ObjectReady, 922 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 923 s.SetResourceInstanceCurrent(addrZeroKey, &states.ResourceInstanceObjectSrc{ 924 AttrsJSON: []byte(`{}`), 925 Status: states.ObjectReady, 926 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 927 }) 928 929 p := simpleMockProvider() 930 ctx := testContext2(t, &ContextOpts{ 931 Providers: map[addrs.Provider]providers.Factory{ 932 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 933 }, 934 }) 935 936 plan, diags := ctx.Plan(m, state, &PlanOpts{ 937 Mode: plans.DestroyMode, 938 }) 939 if diags.HasErrors() { 940 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 941 } 942 943 // We should have a warning, though! We'll lightly abuse the "for RPC" 944 // feature of diagnostics to get some more-readily-comparable diagnostic 945 // values. 946 gotDiags := diags.ForRPC() 947 wantDiags := tfdiags.Diagnostics{ 948 tfdiags.Sourceless( 949 tfdiags.Warning, 950 "Unresolved resource instance address changes", 951 // NOTE: This message is _lightly_ confusing in the destroy case, 952 // because it says "Terraform has planned to destroy these objects" 953 // but this is a plan to destroy all objects, anyway. We expect the 954 // conflict situation to be pretty rare though, and even rarer in 955 // a "terraform destroy", so we'll just live with that for now 956 // unless we see evidence that lots of folks are being confused by 957 // it in practice. 958 `Terraform tried to adjust resource instance addresses in the prior state based on change information recorded in the configuration, but some adjustments did not succeed due to existing objects already at the intended addresses: 959 - test_object.a[0] could not move to test_object.a 960 961 Terraform has planned to destroy these objects. If Terraform's proposed changes aren't appropriate, you must first resolve the conflicts using the "terraform state" subcommands and then create a new plan.`, 962 ), 963 }.ForRPC() 964 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 965 // If we get here with a diff that makes it seem like the above warning 966 // is being reported twice, the likely cause is not correctly handling 967 // the warnings from the hidden normal plan we run as part of preparing 968 // for a destroy plan, unless that strategy has changed in the meantime 969 // since we originally wrote this test. 970 t.Errorf("wrong diagnostics\n%s", diff) 971 } 972 973 t.Run(addrNoKey.String(), func(t *testing.T) { 974 instPlan := plan.Changes.ResourceInstance(addrNoKey) 975 if instPlan == nil { 976 t.Fatalf("no plan for %s at all", addrNoKey) 977 } 978 979 if got, want := instPlan.Addr, addrNoKey; !got.Equal(want) { 980 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 981 } 982 if got, want := instPlan.PrevRunAddr, addrNoKey; !got.Equal(want) { 983 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 984 } 985 if got, want := instPlan.Action, plans.Delete; got != want { 986 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 987 } 988 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 989 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 990 } 991 }) 992 t.Run(addrZeroKey.String(), func(t *testing.T) { 993 instPlan := plan.Changes.ResourceInstance(addrZeroKey) 994 if instPlan == nil { 995 t.Fatalf("no plan for %s at all", addrZeroKey) 996 } 997 998 if got, want := instPlan.Addr, addrZeroKey; !got.Equal(want) { 999 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 1000 } 1001 if got, want := instPlan.PrevRunAddr, addrZeroKey; !got.Equal(want) { 1002 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 1003 } 1004 if got, want := instPlan.Action, plans.Delete; got != want { 1005 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1006 } 1007 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1008 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1009 } 1010 }) 1011 } 1012 1013 func TestContext2Plan_movedResourceUntargeted(t *testing.T) { 1014 addrA := mustResourceInstanceAddr("test_object.a") 1015 addrB := mustResourceInstanceAddr("test_object.b") 1016 m := testModuleInline(t, map[string]string{ 1017 "main.tf": ` 1018 resource "test_object" "b" { 1019 } 1020 1021 moved { 1022 from = test_object.a 1023 to = test_object.b 1024 } 1025 `, 1026 }) 1027 1028 state := states.BuildState(func(s *states.SyncState) { 1029 // The prior state tracks test_object.a, which we should treat as 1030 // test_object.b because of the "moved" block in the config. 1031 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 1032 AttrsJSON: []byte(`{}`), 1033 Status: states.ObjectReady, 1034 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1035 }) 1036 1037 p := simpleMockProvider() 1038 ctx := testContext2(t, &ContextOpts{ 1039 Providers: map[addrs.Provider]providers.Factory{ 1040 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1041 }, 1042 }) 1043 1044 t.Run("without targeting instance A", func(t *testing.T) { 1045 _, diags := ctx.Plan(m, state, &PlanOpts{ 1046 Mode: plans.NormalMode, 1047 Targets: []addrs.Targetable{ 1048 // NOTE: addrA isn't included here, but it's pending move to addrB 1049 // and so this plan request is invalid. 1050 addrB, 1051 }, 1052 }) 1053 diags.Sort() 1054 1055 // We're semi-abusing "ForRPC" here just to get diagnostics that are 1056 // more easily comparable than the various different diagnostics types 1057 // tfdiags uses internally. The RPC-friendly diagnostics are also 1058 // comparison-friendly, by discarding all of the dynamic type information. 1059 gotDiags := diags.ForRPC() 1060 wantDiags := tfdiags.Diagnostics{ 1061 tfdiags.Sourceless( 1062 tfdiags.Warning, 1063 "Resource targeting is in effect", 1064 `You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration. 1065 1066 The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, 1067 ), 1068 tfdiags.Sourceless( 1069 tfdiags.Error, 1070 "Moved resource instances excluded by targeting", 1071 `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances. 1072 1073 To create a valid plan, either remove your -target=... options altogether or add the following additional target options: 1074 -target="test_object.a" 1075 1076 Note that adding these options may include further additional resource instances in your plan, in order to respect object dependencies.`, 1077 ), 1078 }.ForRPC() 1079 1080 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 1081 t.Errorf("wrong diagnostics\n%s", diff) 1082 } 1083 }) 1084 t.Run("without targeting instance B", func(t *testing.T) { 1085 _, diags := ctx.Plan(m, state, &PlanOpts{ 1086 Mode: plans.NormalMode, 1087 Targets: []addrs.Targetable{ 1088 addrA, 1089 // NOTE: addrB isn't included here, but it's pending move from 1090 // addrA and so this plan request is invalid. 1091 }, 1092 }) 1093 diags.Sort() 1094 1095 // We're semi-abusing "ForRPC" here just to get diagnostics that are 1096 // more easily comparable than the various different diagnostics types 1097 // tfdiags uses internally. The RPC-friendly diagnostics are also 1098 // comparison-friendly, by discarding all of the dynamic type information. 1099 gotDiags := diags.ForRPC() 1100 wantDiags := tfdiags.Diagnostics{ 1101 tfdiags.Sourceless( 1102 tfdiags.Warning, 1103 "Resource targeting is in effect", 1104 `You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration. 1105 1106 The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, 1107 ), 1108 tfdiags.Sourceless( 1109 tfdiags.Error, 1110 "Moved resource instances excluded by targeting", 1111 `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances. 1112 1113 To create a valid plan, either remove your -target=... options altogether or add the following additional target options: 1114 -target="test_object.b" 1115 1116 Note that adding these options may include further additional resource instances in your plan, in order to respect object dependencies.`, 1117 ), 1118 }.ForRPC() 1119 1120 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 1121 t.Errorf("wrong diagnostics\n%s", diff) 1122 } 1123 }) 1124 t.Run("without targeting either instance", func(t *testing.T) { 1125 _, diags := ctx.Plan(m, state, &PlanOpts{ 1126 Mode: plans.NormalMode, 1127 Targets: []addrs.Targetable{ 1128 mustResourceInstanceAddr("test_object.unrelated"), 1129 // NOTE: neither addrA nor addrB are included here, but there's 1130 // a pending move between them and so this is invalid. 1131 }, 1132 }) 1133 diags.Sort() 1134 1135 // We're semi-abusing "ForRPC" here just to get diagnostics that are 1136 // more easily comparable than the various different diagnostics types 1137 // tfdiags uses internally. The RPC-friendly diagnostics are also 1138 // comparison-friendly, by discarding all of the dynamic type information. 1139 gotDiags := diags.ForRPC() 1140 wantDiags := tfdiags.Diagnostics{ 1141 tfdiags.Sourceless( 1142 tfdiags.Warning, 1143 "Resource targeting is in effect", 1144 `You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration. 1145 1146 The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, 1147 ), 1148 tfdiags.Sourceless( 1149 tfdiags.Error, 1150 "Moved resource instances excluded by targeting", 1151 `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances. 1152 1153 To create a valid plan, either remove your -target=... options altogether or add the following additional target options: 1154 -target="test_object.a" 1155 -target="test_object.b" 1156 1157 Note that adding these options may include further additional resource instances in your plan, in order to respect object dependencies.`, 1158 ), 1159 }.ForRPC() 1160 1161 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 1162 t.Errorf("wrong diagnostics\n%s", diff) 1163 } 1164 }) 1165 t.Run("with both addresses in the target set", func(t *testing.T) { 1166 // The error messages in the other subtests above suggest adding 1167 // addresses to the set of targets. This additional test makes sure that 1168 // following that advice actually leads to a valid result. 1169 1170 _, diags := ctx.Plan(m, state, &PlanOpts{ 1171 Mode: plans.NormalMode, 1172 Targets: []addrs.Targetable{ 1173 // This time we're including both addresses in the target, 1174 // to get the same effect an end-user would get if following 1175 // the advice in our error message in the other subtests. 1176 addrA, 1177 addrB, 1178 }, 1179 }) 1180 diags.Sort() 1181 1182 // We're semi-abusing "ForRPC" here just to get diagnostics that are 1183 // more easily comparable than the various different diagnostics types 1184 // tfdiags uses internally. The RPC-friendly diagnostics are also 1185 // comparison-friendly, by discarding all of the dynamic type information. 1186 gotDiags := diags.ForRPC() 1187 wantDiags := tfdiags.Diagnostics{ 1188 // Still get the warning about the -target option... 1189 tfdiags.Sourceless( 1190 tfdiags.Warning, 1191 "Resource targeting is in effect", 1192 `You are creating a plan with the -target option, which means that the result of this plan may not represent all of the changes requested by the current configuration. 1193 1194 The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, 1195 ), 1196 // ...but now we have no error about test_object.a 1197 }.ForRPC() 1198 1199 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 1200 t.Errorf("wrong diagnostics\n%s", diff) 1201 } 1202 }) 1203 } 1204 1205 func TestContext2Plan_movedResourceRefreshOnly(t *testing.T) { 1206 addrA := mustResourceInstanceAddr("test_object.a") 1207 addrB := mustResourceInstanceAddr("test_object.b") 1208 m := testModuleInline(t, map[string]string{ 1209 "main.tf": ` 1210 resource "test_object" "b" { 1211 } 1212 1213 moved { 1214 from = test_object.a 1215 to = test_object.b 1216 } 1217 `, 1218 }) 1219 1220 state := states.BuildState(func(s *states.SyncState) { 1221 // The prior state tracks test_object.a, which we should treat as 1222 // test_object.b because of the "moved" block in the config. 1223 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 1224 AttrsJSON: []byte(`{}`), 1225 Status: states.ObjectReady, 1226 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1227 }) 1228 1229 p := simpleMockProvider() 1230 ctx := testContext2(t, &ContextOpts{ 1231 Providers: map[addrs.Provider]providers.Factory{ 1232 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1233 }, 1234 }) 1235 1236 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1237 Mode: plans.RefreshOnlyMode, 1238 }) 1239 if diags.HasErrors() { 1240 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1241 } 1242 1243 t.Run(addrA.String(), func(t *testing.T) { 1244 instPlan := plan.Changes.ResourceInstance(addrA) 1245 if instPlan != nil { 1246 t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) 1247 } 1248 }) 1249 t.Run(addrB.String(), func(t *testing.T) { 1250 instPlan := plan.Changes.ResourceInstance(addrB) 1251 if instPlan != nil { 1252 t.Fatalf("unexpected plan for %s", addrB) 1253 } 1254 }) 1255 t.Run("drift", func(t *testing.T) { 1256 var drifted *plans.ResourceInstanceChangeSrc 1257 for _, dr := range plan.DriftedResources { 1258 if dr.Addr.Equal(addrB) { 1259 drifted = dr 1260 break 1261 } 1262 } 1263 1264 if drifted == nil { 1265 t.Fatalf("instance %s is missing from the drifted resource changes", addrB) 1266 } 1267 1268 if got, want := drifted.PrevRunAddr, addrA; !got.Equal(want) { 1269 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 1270 } 1271 if got, want := drifted.Action, plans.NoOp; got != want { 1272 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1273 } 1274 }) 1275 } 1276 1277 func TestContext2Plan_refreshOnlyMode(t *testing.T) { 1278 addr := mustResourceInstanceAddr("test_object.a") 1279 1280 // The configuration, the prior state, and the refresh result intentionally 1281 // have different values for "test_string" so we can observe that the 1282 // refresh took effect but the configuration change wasn't considered. 1283 m := testModuleInline(t, map[string]string{ 1284 "main.tf": ` 1285 resource "test_object" "a" { 1286 arg = "after" 1287 } 1288 1289 output "out" { 1290 value = test_object.a.arg 1291 } 1292 `, 1293 }) 1294 state := states.BuildState(func(s *states.SyncState) { 1295 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 1296 AttrsJSON: []byte(`{"arg":"before"}`), 1297 Status: states.ObjectReady, 1298 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1299 }) 1300 1301 p := simpleMockProvider() 1302 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1303 Provider: providers.Schema{Block: simpleTestSchema()}, 1304 ResourceTypes: map[string]providers.Schema{ 1305 "test_object": { 1306 Block: &configschema.Block{ 1307 Attributes: map[string]*configschema.Attribute{ 1308 "arg": {Type: cty.String, Optional: true}, 1309 }, 1310 }, 1311 }, 1312 }, 1313 } 1314 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 1315 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 1316 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 1317 return cty.StringVal("current"), nil 1318 } 1319 return v, nil 1320 }) 1321 if err != nil { 1322 // shouldn't get here 1323 t.Fatalf("ReadResourceFn transform failed") 1324 return providers.ReadResourceResponse{} 1325 } 1326 return providers.ReadResourceResponse{ 1327 NewState: newVal, 1328 } 1329 } 1330 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 1331 // We should've been given the prior state JSON as our input to upgrade. 1332 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 1333 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 1334 } 1335 1336 // We'll put something different in "arg" as part of upgrading, just 1337 // so that we can verify below that PrevRunState contains the upgraded 1338 // (but NOT refreshed) version of the object. 1339 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 1340 "arg": cty.StringVal("upgraded"), 1341 }) 1342 return resp 1343 } 1344 1345 ctx := testContext2(t, &ContextOpts{ 1346 Providers: map[addrs.Provider]providers.Factory{ 1347 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1348 }, 1349 }) 1350 1351 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1352 Mode: plans.RefreshOnlyMode, 1353 }) 1354 if diags.HasErrors() { 1355 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1356 } 1357 1358 if !p.UpgradeResourceStateCalled { 1359 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 1360 } 1361 if !p.ReadResourceCalled { 1362 t.Errorf("Provider's ReadResource wasn't called; should've been") 1363 } 1364 1365 if got, want := len(plan.Changes.Resources), 0; got != want { 1366 t.Errorf("plan contains resource changes; want none\n%s", spew.Sdump(plan.Changes.Resources)) 1367 } 1368 1369 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 1370 t.Errorf("%s has no prior state at all after plan", addr) 1371 } else { 1372 if instState.Current == nil { 1373 t.Errorf("%s has no current object after plan", addr) 1374 } else if got, want := instState.Current.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 1375 // Should've saved the result of refreshing 1376 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1377 } 1378 } 1379 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 1380 t.Errorf("%s has no previous run state at all after plan", addr) 1381 } else { 1382 if instState.Current == nil { 1383 t.Errorf("%s has no current object in the previous run state", addr) 1384 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 1385 // Should've saved the result of upgrading 1386 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1387 } 1388 } 1389 1390 // The output value should also have updated. If not, it's likely that we 1391 // skipped updating the working state to match the refreshed state when we 1392 // were evaluating the resource. 1393 if outChangeSrc := plan.Changes.OutputValue(addrs.RootModuleInstance.OutputValue("out")); outChangeSrc == nil { 1394 t.Errorf("no change planned for output value 'out'") 1395 } else { 1396 outChange, err := outChangeSrc.Decode() 1397 if err != nil { 1398 t.Fatalf("failed to decode output value 'out': %s", err) 1399 } 1400 got := outChange.After 1401 want := cty.StringVal("current") 1402 if !want.RawEquals(got) { 1403 t.Errorf("wrong value for output value 'out'\ngot: %#v\nwant: %#v", got, want) 1404 } 1405 } 1406 } 1407 1408 func TestContext2Plan_refreshOnlyMode_deposed(t *testing.T) { 1409 addr := mustResourceInstanceAddr("test_object.a") 1410 deposedKey := states.DeposedKey("byebye") 1411 1412 // The configuration, the prior state, and the refresh result intentionally 1413 // have different values for "test_string" so we can observe that the 1414 // refresh took effect but the configuration change wasn't considered. 1415 m := testModuleInline(t, map[string]string{ 1416 "main.tf": ` 1417 resource "test_object" "a" { 1418 arg = "after" 1419 } 1420 1421 output "out" { 1422 value = test_object.a.arg 1423 } 1424 `, 1425 }) 1426 state := states.BuildState(func(s *states.SyncState) { 1427 // Note that we're intentionally recording a _deposed_ object here, 1428 // and not including a current object, so a normal (non-refresh) 1429 // plan would normally plan to create a new object _and_ destroy 1430 // the deposed one, but refresh-only mode should prevent that. 1431 s.SetResourceInstanceDeposed(addr, deposedKey, &states.ResourceInstanceObjectSrc{ 1432 AttrsJSON: []byte(`{"arg":"before"}`), 1433 Status: states.ObjectReady, 1434 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1435 }) 1436 1437 p := simpleMockProvider() 1438 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1439 Provider: providers.Schema{Block: simpleTestSchema()}, 1440 ResourceTypes: map[string]providers.Schema{ 1441 "test_object": { 1442 Block: &configschema.Block{ 1443 Attributes: map[string]*configschema.Attribute{ 1444 "arg": {Type: cty.String, Optional: true}, 1445 }, 1446 }, 1447 }, 1448 }, 1449 } 1450 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 1451 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 1452 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 1453 return cty.StringVal("current"), nil 1454 } 1455 return v, nil 1456 }) 1457 if err != nil { 1458 // shouldn't get here 1459 t.Fatalf("ReadResourceFn transform failed") 1460 return providers.ReadResourceResponse{} 1461 } 1462 return providers.ReadResourceResponse{ 1463 NewState: newVal, 1464 } 1465 } 1466 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 1467 // We should've been given the prior state JSON as our input to upgrade. 1468 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 1469 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 1470 } 1471 1472 // We'll put something different in "arg" as part of upgrading, just 1473 // so that we can verify below that PrevRunState contains the upgraded 1474 // (but NOT refreshed) version of the object. 1475 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 1476 "arg": cty.StringVal("upgraded"), 1477 }) 1478 return resp 1479 } 1480 1481 ctx := testContext2(t, &ContextOpts{ 1482 Providers: map[addrs.Provider]providers.Factory{ 1483 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1484 }, 1485 }) 1486 1487 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1488 Mode: plans.RefreshOnlyMode, 1489 }) 1490 if diags.HasErrors() { 1491 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1492 } 1493 1494 if !p.UpgradeResourceStateCalled { 1495 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 1496 } 1497 if !p.ReadResourceCalled { 1498 t.Errorf("Provider's ReadResource wasn't called; should've been") 1499 } 1500 1501 if got, want := len(plan.Changes.Resources), 0; got != want { 1502 t.Errorf("plan contains resource changes; want none\n%s", spew.Sdump(plan.Changes.Resources)) 1503 } 1504 1505 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 1506 t.Errorf("%s has no prior state at all after plan", addr) 1507 } else { 1508 if obj := instState.Deposed[deposedKey]; obj == nil { 1509 t.Errorf("%s has no deposed object after plan", addr) 1510 } else if got, want := obj.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 1511 // Should've saved the result of refreshing 1512 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1513 } 1514 } 1515 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 1516 t.Errorf("%s has no previous run state at all after plan", addr) 1517 } else { 1518 if obj := instState.Deposed[deposedKey]; obj == nil { 1519 t.Errorf("%s has no deposed object in the previous run state", addr) 1520 } else if got, want := obj.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 1521 // Should've saved the result of upgrading 1522 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1523 } 1524 } 1525 1526 // The output value should also have updated. If not, it's likely that we 1527 // skipped updating the working state to match the refreshed state when we 1528 // were evaluating the resource. 1529 if outChangeSrc := plan.Changes.OutputValue(addrs.RootModuleInstance.OutputValue("out")); outChangeSrc == nil { 1530 t.Errorf("no change planned for output value 'out'") 1531 } else { 1532 outChange, err := outChangeSrc.Decode() 1533 if err != nil { 1534 t.Fatalf("failed to decode output value 'out': %s", err) 1535 } 1536 got := outChange.After 1537 want := cty.UnknownVal(cty.String) 1538 if !want.RawEquals(got) { 1539 t.Errorf("wrong value for output value 'out'\ngot: %#v\nwant: %#v", got, want) 1540 } 1541 } 1542 1543 // Deposed objects should not be represented in drift. 1544 if len(plan.DriftedResources) > 0 { 1545 t.Errorf("unexpected drifted resources (%d)", len(plan.DriftedResources)) 1546 } 1547 } 1548 1549 func TestContext2Plan_refreshOnlyMode_orphan(t *testing.T) { 1550 addr := mustAbsResourceAddr("test_object.a") 1551 1552 // The configuration, the prior state, and the refresh result intentionally 1553 // have different values for "test_string" so we can observe that the 1554 // refresh took effect but the configuration change wasn't considered. 1555 m := testModuleInline(t, map[string]string{ 1556 "main.tf": ` 1557 resource "test_object" "a" { 1558 arg = "after" 1559 count = 1 1560 } 1561 1562 output "out" { 1563 value = test_object.a.*.arg 1564 } 1565 `, 1566 }) 1567 state := states.BuildState(func(s *states.SyncState) { 1568 s.SetResourceInstanceCurrent(addr.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{ 1569 AttrsJSON: []byte(`{"arg":"before"}`), 1570 Status: states.ObjectReady, 1571 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1572 s.SetResourceInstanceCurrent(addr.Instance(addrs.IntKey(1)), &states.ResourceInstanceObjectSrc{ 1573 AttrsJSON: []byte(`{"arg":"before"}`), 1574 Status: states.ObjectReady, 1575 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1576 }) 1577 1578 p := simpleMockProvider() 1579 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1580 Provider: providers.Schema{Block: simpleTestSchema()}, 1581 ResourceTypes: map[string]providers.Schema{ 1582 "test_object": { 1583 Block: &configschema.Block{ 1584 Attributes: map[string]*configschema.Attribute{ 1585 "arg": {Type: cty.String, Optional: true}, 1586 }, 1587 }, 1588 }, 1589 }, 1590 } 1591 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 1592 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 1593 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 1594 return cty.StringVal("current"), nil 1595 } 1596 return v, nil 1597 }) 1598 if err != nil { 1599 // shouldn't get here 1600 t.Fatalf("ReadResourceFn transform failed") 1601 return providers.ReadResourceResponse{} 1602 } 1603 return providers.ReadResourceResponse{ 1604 NewState: newVal, 1605 } 1606 } 1607 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 1608 // We should've been given the prior state JSON as our input to upgrade. 1609 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 1610 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 1611 } 1612 1613 // We'll put something different in "arg" as part of upgrading, just 1614 // so that we can verify below that PrevRunState contains the upgraded 1615 // (but NOT refreshed) version of the object. 1616 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 1617 "arg": cty.StringVal("upgraded"), 1618 }) 1619 return resp 1620 } 1621 1622 ctx := testContext2(t, &ContextOpts{ 1623 Providers: map[addrs.Provider]providers.Factory{ 1624 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1625 }, 1626 }) 1627 1628 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1629 Mode: plans.RefreshOnlyMode, 1630 }) 1631 if diags.HasErrors() { 1632 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1633 } 1634 1635 if !p.UpgradeResourceStateCalled { 1636 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 1637 } 1638 if !p.ReadResourceCalled { 1639 t.Errorf("Provider's ReadResource wasn't called; should've been") 1640 } 1641 1642 if got, want := len(plan.Changes.Resources), 0; got != want { 1643 t.Errorf("plan contains resource changes; want none\n%s", spew.Sdump(plan.Changes.Resources)) 1644 } 1645 1646 if rState := plan.PriorState.Resource(addr); rState == nil { 1647 t.Errorf("%s has no prior state at all after plan", addr) 1648 } else { 1649 for i := 0; i < 2; i++ { 1650 instKey := addrs.IntKey(i) 1651 if obj := rState.Instance(instKey).Current; obj == nil { 1652 t.Errorf("%s%s has no object after plan", addr, instKey) 1653 } else if got, want := obj.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 1654 // Should've saved the result of refreshing 1655 t.Errorf("%s%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, instKey, got, want) 1656 } 1657 } 1658 } 1659 if rState := plan.PrevRunState.Resource(addr); rState == nil { 1660 t.Errorf("%s has no prior state at all after plan", addr) 1661 } else { 1662 for i := 0; i < 2; i++ { 1663 instKey := addrs.IntKey(i) 1664 if obj := rState.Instance(instKey).Current; obj == nil { 1665 t.Errorf("%s%s has no object after plan", addr, instKey) 1666 } else if got, want := obj.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 1667 // Should've saved the result of upgrading 1668 t.Errorf("%s%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, instKey, got, want) 1669 } 1670 } 1671 } 1672 1673 // The output value should also have updated. If not, it's likely that we 1674 // skipped updating the working state to match the refreshed state when we 1675 // were evaluating the resource. 1676 if outChangeSrc := plan.Changes.OutputValue(addrs.RootModuleInstance.OutputValue("out")); outChangeSrc == nil { 1677 t.Errorf("no change planned for output value 'out'") 1678 } else { 1679 outChange, err := outChangeSrc.Decode() 1680 if err != nil { 1681 t.Fatalf("failed to decode output value 'out': %s", err) 1682 } 1683 got := outChange.After 1684 want := cty.TupleVal([]cty.Value{cty.StringVal("current"), cty.StringVal("current")}) 1685 if !want.RawEquals(got) { 1686 t.Errorf("wrong value for output value 'out'\ngot: %#v\nwant: %#v", got, want) 1687 } 1688 } 1689 } 1690 1691 func TestContext2Plan_invalidSensitiveModuleOutput(t *testing.T) { 1692 m := testModuleInline(t, map[string]string{ 1693 "child/main.tf": ` 1694 output "out" { 1695 value = sensitive("xyz") 1696 }`, 1697 "main.tf": ` 1698 module "child" { 1699 source = "./child" 1700 } 1701 1702 output "root" { 1703 value = module.child.out 1704 }`, 1705 }) 1706 1707 ctx := testContext2(t, &ContextOpts{}) 1708 1709 _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 1710 if !diags.HasErrors() { 1711 t.Fatal("succeeded; want errors") 1712 } 1713 if got, want := diags.Err().Error(), "Output refers to sensitive values"; !strings.Contains(got, want) { 1714 t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) 1715 } 1716 } 1717 1718 func TestContext2Plan_planDataSourceSensitiveNested(t *testing.T) { 1719 m := testModuleInline(t, map[string]string{ 1720 "main.tf": ` 1721 resource "test_instance" "bar" { 1722 } 1723 1724 data "test_data_source" "foo" { 1725 foo { 1726 bar = test_instance.bar.sensitive 1727 } 1728 } 1729 `, 1730 }) 1731 1732 p := new(MockProvider) 1733 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 1734 resp.PlannedState = cty.ObjectVal(map[string]cty.Value{ 1735 "sensitive": cty.UnknownVal(cty.String), 1736 }) 1737 return resp 1738 } 1739 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 1740 ResourceTypes: map[string]*configschema.Block{ 1741 "test_instance": { 1742 Attributes: map[string]*configschema.Attribute{ 1743 "sensitive": { 1744 Type: cty.String, 1745 Computed: true, 1746 Sensitive: true, 1747 }, 1748 }, 1749 }, 1750 }, 1751 DataSources: map[string]*configschema.Block{ 1752 "test_data_source": { 1753 Attributes: map[string]*configschema.Attribute{ 1754 "id": { 1755 Type: cty.String, 1756 Computed: true, 1757 }, 1758 }, 1759 BlockTypes: map[string]*configschema.NestedBlock{ 1760 "foo": { 1761 Block: configschema.Block{ 1762 Attributes: map[string]*configschema.Attribute{ 1763 "bar": {Type: cty.String, Optional: true}, 1764 }, 1765 }, 1766 Nesting: configschema.NestingSet, 1767 }, 1768 }, 1769 }, 1770 }, 1771 }) 1772 1773 state := states.NewState() 1774 root := state.EnsureModule(addrs.RootModuleInstance) 1775 root.SetResourceInstanceCurrent( 1776 mustResourceInstanceAddr("data.test_data_source.foo").Resource, 1777 &states.ResourceInstanceObjectSrc{ 1778 Status: states.ObjectReady, 1779 AttrsJSON: []byte(`{"string":"data_id", "foo":[{"bar":"old"}]}`), 1780 AttrSensitivePaths: []cty.PathValueMarks{ 1781 { 1782 Path: cty.GetAttrPath("foo"), 1783 Marks: cty.NewValueMarks(marks.Sensitive), 1784 }, 1785 }, 1786 }, 1787 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 1788 ) 1789 root.SetResourceInstanceCurrent( 1790 mustResourceInstanceAddr("test_instance.bar").Resource, 1791 &states.ResourceInstanceObjectSrc{ 1792 Status: states.ObjectReady, 1793 AttrsJSON: []byte(`{"sensitive":"old"}`), 1794 AttrSensitivePaths: []cty.PathValueMarks{ 1795 { 1796 Path: cty.GetAttrPath("sensitive"), 1797 Marks: cty.NewValueMarks(marks.Sensitive), 1798 }, 1799 }, 1800 }, 1801 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 1802 ) 1803 1804 ctx := testContext2(t, &ContextOpts{ 1805 Providers: map[addrs.Provider]providers.Factory{ 1806 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1807 }, 1808 }) 1809 1810 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 1811 assertNoErrors(t, diags) 1812 1813 for _, res := range plan.Changes.Resources { 1814 switch res.Addr.String() { 1815 case "test_instance.bar": 1816 if res.Action != plans.Update { 1817 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 1818 } 1819 case "data.test_data_source.foo": 1820 if res.Action != plans.Read { 1821 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 1822 } 1823 default: 1824 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 1825 } 1826 } 1827 } 1828 1829 func TestContext2Plan_forceReplace(t *testing.T) { 1830 addrA := mustResourceInstanceAddr("test_object.a") 1831 addrB := mustResourceInstanceAddr("test_object.b") 1832 m := testModuleInline(t, map[string]string{ 1833 "main.tf": ` 1834 resource "test_object" "a" { 1835 } 1836 resource "test_object" "b" { 1837 } 1838 `, 1839 }) 1840 1841 state := states.BuildState(func(s *states.SyncState) { 1842 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 1843 AttrsJSON: []byte(`{}`), 1844 Status: states.ObjectReady, 1845 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1846 s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{ 1847 AttrsJSON: []byte(`{}`), 1848 Status: states.ObjectReady, 1849 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1850 }) 1851 1852 p := simpleMockProvider() 1853 ctx := testContext2(t, &ContextOpts{ 1854 Providers: map[addrs.Provider]providers.Factory{ 1855 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1856 }, 1857 }) 1858 1859 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1860 Mode: plans.NormalMode, 1861 ForceReplace: []addrs.AbsResourceInstance{ 1862 addrA, 1863 }, 1864 }) 1865 if diags.HasErrors() { 1866 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1867 } 1868 1869 t.Run(addrA.String(), func(t *testing.T) { 1870 instPlan := plan.Changes.ResourceInstance(addrA) 1871 if instPlan == nil { 1872 t.Fatalf("no plan for %s at all", addrA) 1873 } 1874 1875 if got, want := instPlan.Action, plans.DeleteThenCreate; got != want { 1876 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1877 } 1878 if got, want := instPlan.ActionReason, plans.ResourceInstanceReplaceByRequest; got != want { 1879 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1880 } 1881 }) 1882 t.Run(addrB.String(), func(t *testing.T) { 1883 instPlan := plan.Changes.ResourceInstance(addrB) 1884 if instPlan == nil { 1885 t.Fatalf("no plan for %s at all", addrB) 1886 } 1887 1888 if got, want := instPlan.Action, plans.NoOp; got != want { 1889 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1890 } 1891 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1892 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1893 } 1894 }) 1895 } 1896 1897 func TestContext2Plan_forceReplaceIncompleteAddr(t *testing.T) { 1898 addr0 := mustResourceInstanceAddr("test_object.a[0]") 1899 addr1 := mustResourceInstanceAddr("test_object.a[1]") 1900 addrBare := mustResourceInstanceAddr("test_object.a") 1901 m := testModuleInline(t, map[string]string{ 1902 "main.tf": ` 1903 resource "test_object" "a" { 1904 count = 2 1905 } 1906 `, 1907 }) 1908 1909 state := states.BuildState(func(s *states.SyncState) { 1910 s.SetResourceInstanceCurrent(addr0, &states.ResourceInstanceObjectSrc{ 1911 AttrsJSON: []byte(`{}`), 1912 Status: states.ObjectReady, 1913 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1914 s.SetResourceInstanceCurrent(addr1, &states.ResourceInstanceObjectSrc{ 1915 AttrsJSON: []byte(`{}`), 1916 Status: states.ObjectReady, 1917 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1918 }) 1919 1920 p := simpleMockProvider() 1921 ctx := testContext2(t, &ContextOpts{ 1922 Providers: map[addrs.Provider]providers.Factory{ 1923 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1924 }, 1925 }) 1926 1927 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1928 Mode: plans.NormalMode, 1929 ForceReplace: []addrs.AbsResourceInstance{ 1930 addrBare, 1931 }, 1932 }) 1933 if diags.HasErrors() { 1934 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1935 } 1936 diagsErr := diags.ErrWithWarnings() 1937 if diagsErr == nil { 1938 t.Fatalf("no warnings were returned") 1939 } 1940 if got, want := diagsErr.Error(), "Incompletely-matched force-replace resource instance"; !strings.Contains(got, want) { 1941 t.Errorf("missing expected warning\ngot:\n%s\n\nwant substring: %s", got, want) 1942 } 1943 1944 t.Run(addr0.String(), func(t *testing.T) { 1945 instPlan := plan.Changes.ResourceInstance(addr0) 1946 if instPlan == nil { 1947 t.Fatalf("no plan for %s at all", addr0) 1948 } 1949 1950 if got, want := instPlan.Action, plans.NoOp; got != want { 1951 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1952 } 1953 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1954 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1955 } 1956 }) 1957 t.Run(addr1.String(), func(t *testing.T) { 1958 instPlan := plan.Changes.ResourceInstance(addr1) 1959 if instPlan == nil { 1960 t.Fatalf("no plan for %s at all", addr1) 1961 } 1962 1963 if got, want := instPlan.Action, plans.NoOp; got != want { 1964 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1965 } 1966 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1967 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1968 } 1969 }) 1970 } 1971 1972 // Verify that adding a module instance does force existing module data sources 1973 // to be deferred 1974 func TestContext2Plan_noChangeDataSourceAddingModuleInstance(t *testing.T) { 1975 m := testModuleInline(t, map[string]string{ 1976 "main.tf": ` 1977 locals { 1978 data = { 1979 a = "a" 1980 b = "b" 1981 } 1982 } 1983 1984 module "one" { 1985 source = "./mod" 1986 for_each = local.data 1987 input = each.value 1988 } 1989 1990 module "two" { 1991 source = "./mod" 1992 for_each = module.one 1993 input = each.value.output 1994 } 1995 `, 1996 "mod/main.tf": ` 1997 variable "input" { 1998 } 1999 2000 resource "test_resource" "x" { 2001 value = var.input 2002 } 2003 2004 data "test_data_source" "d" { 2005 foo = test_resource.x.id 2006 } 2007 2008 output "output" { 2009 value = test_resource.x.id 2010 } 2011 `, 2012 }) 2013 2014 p := testProvider("test") 2015 p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ 2016 State: cty.ObjectVal(map[string]cty.Value{ 2017 "id": cty.StringVal("data"), 2018 "foo": cty.StringVal("foo"), 2019 }), 2020 } 2021 state := states.NewState() 2022 modOne := addrs.RootModuleInstance.Child("one", addrs.StringKey("a")) 2023 modTwo := addrs.RootModuleInstance.Child("two", addrs.StringKey("a")) 2024 one := state.EnsureModule(modOne) 2025 two := state.EnsureModule(modTwo) 2026 one.SetResourceInstanceCurrent( 2027 mustResourceInstanceAddr(`test_resource.x`).Resource, 2028 &states.ResourceInstanceObjectSrc{ 2029 Status: states.ObjectReady, 2030 AttrsJSON: []byte(`{"id":"foo","value":"a"}`), 2031 }, 2032 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 2033 ) 2034 one.SetResourceInstanceCurrent( 2035 mustResourceInstanceAddr(`data.test_data_source.d`).Resource, 2036 &states.ResourceInstanceObjectSrc{ 2037 Status: states.ObjectReady, 2038 AttrsJSON: []byte(`{"id":"data"}`), 2039 }, 2040 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 2041 ) 2042 two.SetResourceInstanceCurrent( 2043 mustResourceInstanceAddr(`test_resource.x`).Resource, 2044 &states.ResourceInstanceObjectSrc{ 2045 Status: states.ObjectReady, 2046 AttrsJSON: []byte(`{"id":"foo","value":"foo"}`), 2047 }, 2048 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 2049 ) 2050 two.SetResourceInstanceCurrent( 2051 mustResourceInstanceAddr(`data.test_data_source.d`).Resource, 2052 &states.ResourceInstanceObjectSrc{ 2053 Status: states.ObjectReady, 2054 AttrsJSON: []byte(`{"id":"data"}`), 2055 }, 2056 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 2057 ) 2058 2059 ctx := testContext2(t, &ContextOpts{ 2060 Providers: map[addrs.Provider]providers.Factory{ 2061 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2062 }, 2063 }) 2064 2065 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 2066 assertNoErrors(t, diags) 2067 2068 for _, res := range plan.Changes.Resources { 2069 // both existing data sources should be read during plan 2070 if res.Addr.Module[0].InstanceKey == addrs.StringKey("b") { 2071 continue 2072 } 2073 2074 if res.Addr.Resource.Resource.Mode == addrs.DataResourceMode && res.Action != plans.NoOp { 2075 t.Errorf("unexpected %s plan for %s", res.Action, res.Addr) 2076 } 2077 } 2078 }