github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/terraform/context_plan2_test.go (about) 1 package terraform 2 3 import ( 4 "bytes" 5 "errors" 6 "strings" 7 "testing" 8 9 "github.com/davecgh/go-spew/spew" 10 "github.com/hashicorp/terraform/internal/addrs" 11 "github.com/hashicorp/terraform/internal/configs/configschema" 12 "github.com/hashicorp/terraform/internal/lang/marks" 13 "github.com/hashicorp/terraform/internal/plans" 14 "github.com/hashicorp/terraform/internal/providers" 15 "github.com/hashicorp/terraform/internal/states" 16 "github.com/zclconf/go-cty/cty" 17 ) 18 19 func TestContext2Plan_removedDuringRefresh(t *testing.T) { 20 // This tests the situation where an object tracked in the previous run 21 // state has been deleted outside of Terraform, which we should detect 22 // during the refresh step and thus ultimately produce a plan to recreate 23 // the object, since it's still present in the configuration. 24 m := testModuleInline(t, map[string]string{ 25 "main.tf": ` 26 resource "test_object" "a" { 27 } 28 `, 29 }) 30 31 p := simpleMockProvider() 32 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 33 Provider: providers.Schema{Block: simpleTestSchema()}, 34 ResourceTypes: map[string]providers.Schema{ 35 "test_object": { 36 Block: &configschema.Block{ 37 Attributes: map[string]*configschema.Attribute{ 38 "arg": {Type: cty.String, Optional: true}, 39 }, 40 }, 41 }, 42 }, 43 } 44 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 45 resp.NewState = cty.NullVal(req.PriorState.Type()) 46 return resp 47 } 48 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 49 // We should've been given the prior state JSON as our input to upgrade. 50 if !bytes.Contains(req.RawStateJSON, []byte("previous_run")) { 51 t.Fatalf("UpgradeResourceState request doesn't contain the previous run object\n%s", req.RawStateJSON) 52 } 53 54 // We'll put something different in "arg" as part of upgrading, just 55 // so that we can verify below that PrevRunState contains the upgraded 56 // (but NOT refreshed) version of the object. 57 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 58 "arg": cty.StringVal("upgraded"), 59 }) 60 return resp 61 } 62 63 addr := mustResourceInstanceAddr("test_object.a") 64 state := states.BuildState(func(s *states.SyncState) { 65 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 66 AttrsJSON: []byte(`{"arg":"previous_run"}`), 67 Status: states.ObjectTainted, 68 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 69 }) 70 71 ctx := testContext2(t, &ContextOpts{ 72 Providers: map[addrs.Provider]providers.Factory{ 73 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 74 }, 75 }) 76 77 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 78 assertNoErrors(t, diags) 79 80 if !p.UpgradeResourceStateCalled { 81 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 82 } 83 if !p.ReadResourceCalled { 84 t.Errorf("Provider's ReadResource wasn't called; should've been") 85 } 86 87 // The object should be absent from the plan's prior state, because that 88 // records the result of refreshing. 89 if got := plan.PriorState.ResourceInstance(addr); got != nil { 90 t.Errorf( 91 "instance %s is in the prior state after planning; should've been removed\n%s", 92 addr, spew.Sdump(got), 93 ) 94 } 95 96 // However, the object should still be in the PrevRunState, because 97 // that reflects what we believed to exist before refreshing. 98 if got := plan.PrevRunState.ResourceInstance(addr); got == nil { 99 t.Errorf( 100 "instance %s is missing from the previous run state after planning; should've been preserved", 101 addr, 102 ) 103 } else { 104 if !bytes.Contains(got.Current.AttrsJSON, []byte("upgraded")) { 105 t.Fatalf("previous run state has non-upgraded object\n%s", got.Current.AttrsJSON) 106 } 107 } 108 109 // Because the configuration still mentions test_object.a, we should've 110 // planned to recreate it in order to fix the drift. 111 for _, c := range plan.Changes.Resources { 112 if c.Action != plans.Create { 113 t.Fatalf("expected Create action for missing %s, got %s", c.Addr, c.Action) 114 } 115 } 116 } 117 118 func TestContext2Plan_noChangeDataSourceSensitiveNestedSet(t *testing.T) { 119 m := testModuleInline(t, map[string]string{ 120 "main.tf": ` 121 variable "bar" { 122 sensitive = true 123 default = "baz" 124 } 125 126 data "test_data_source" "foo" { 127 foo { 128 bar = var.bar 129 } 130 } 131 `, 132 }) 133 134 p := new(MockProvider) 135 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 136 DataSources: map[string]*configschema.Block{ 137 "test_data_source": { 138 Attributes: map[string]*configschema.Attribute{ 139 "id": { 140 Type: cty.String, 141 Computed: true, 142 }, 143 }, 144 BlockTypes: map[string]*configschema.NestedBlock{ 145 "foo": { 146 Block: configschema.Block{ 147 Attributes: map[string]*configschema.Attribute{ 148 "bar": {Type: cty.String, Optional: true}, 149 }, 150 }, 151 Nesting: configschema.NestingSet, 152 }, 153 }, 154 }, 155 }, 156 }) 157 158 p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ 159 State: cty.ObjectVal(map[string]cty.Value{ 160 "id": cty.StringVal("data_id"), 161 "foo": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")})}), 162 }), 163 } 164 165 state := states.NewState() 166 root := state.EnsureModule(addrs.RootModuleInstance) 167 root.SetResourceInstanceCurrent( 168 mustResourceInstanceAddr("data.test_data_source.foo").Resource, 169 &states.ResourceInstanceObjectSrc{ 170 Status: states.ObjectReady, 171 AttrsJSON: []byte(`{"id":"data_id", "foo":[{"bar":"baz"}]}`), 172 AttrSensitivePaths: []cty.PathValueMarks{ 173 { 174 Path: cty.GetAttrPath("foo"), 175 Marks: cty.NewValueMarks(marks.Sensitive), 176 }, 177 }, 178 }, 179 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 180 ) 181 182 ctx := testContext2(t, &ContextOpts{ 183 Providers: map[addrs.Provider]providers.Factory{ 184 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 185 }, 186 }) 187 188 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 189 assertNoErrors(t, diags) 190 191 for _, res := range plan.Changes.Resources { 192 if res.Action != plans.NoOp { 193 t.Fatalf("expected NoOp, got: %q %s", res.Addr, res.Action) 194 } 195 } 196 } 197 198 func TestContext2Plan_orphanDataInstance(t *testing.T) { 199 // ensure the planned replacement of the data source is evaluated properly 200 m := testModuleInline(t, map[string]string{ 201 "main.tf": ` 202 data "test_object" "a" { 203 for_each = { new = "ok" } 204 } 205 206 output "out" { 207 value = [ for k, _ in data.test_object.a: k ] 208 } 209 `, 210 }) 211 212 p := simpleMockProvider() 213 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 214 resp.State = req.Config 215 return resp 216 } 217 218 state := states.BuildState(func(s *states.SyncState) { 219 s.SetResourceInstanceCurrent(mustResourceInstanceAddr(`data.test_object.a["old"]`), &states.ResourceInstanceObjectSrc{ 220 AttrsJSON: []byte(`{"test_string":"foo"}`), 221 Status: states.ObjectReady, 222 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 223 }) 224 225 ctx := testContext2(t, &ContextOpts{ 226 Providers: map[addrs.Provider]providers.Factory{ 227 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 228 }, 229 }) 230 231 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 232 assertNoErrors(t, diags) 233 234 change, err := plan.Changes.Outputs[0].Decode() 235 if err != nil { 236 t.Fatal(err) 237 } 238 239 expected := cty.TupleVal([]cty.Value{cty.StringVal("new")}) 240 241 if change.After.Equals(expected).False() { 242 t.Fatalf("expected %#v, got %#v\n", expected, change.After) 243 } 244 } 245 246 func TestContext2Plan_basicConfigurationAliases(t *testing.T) { 247 m := testModuleInline(t, map[string]string{ 248 "main.tf": ` 249 provider "test" { 250 alias = "z" 251 test_string = "config" 252 } 253 254 module "mod" { 255 source = "./mod" 256 providers = { 257 test.x = test.z 258 } 259 } 260 `, 261 262 "mod/main.tf": ` 263 terraform { 264 required_providers { 265 test = { 266 source = "registry.terraform.io/hashicorp/test" 267 configuration_aliases = [ test.x ] 268 } 269 } 270 } 271 272 resource "test_object" "a" { 273 provider = test.x 274 } 275 276 `, 277 }) 278 279 p := simpleMockProvider() 280 281 // The resource within the module should be using the provider configured 282 // from the root module. We should never see an empty configuration. 283 p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 284 if req.Config.GetAttr("test_string").IsNull() { 285 resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing test_string value")) 286 } 287 return resp 288 } 289 290 ctx := testContext2(t, &ContextOpts{ 291 Providers: map[addrs.Provider]providers.Factory{ 292 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 293 }, 294 }) 295 296 _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 297 assertNoErrors(t, diags) 298 } 299 300 func TestContext2Plan_dataReferencesResourceInModules(t *testing.T) { 301 p := testProvider("test") 302 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 303 cfg := req.Config.AsValueMap() 304 cfg["id"] = cty.StringVal("d") 305 resp.State = cty.ObjectVal(cfg) 306 return resp 307 } 308 309 m := testModuleInline(t, map[string]string{ 310 "main.tf": ` 311 locals { 312 things = { 313 old = "first" 314 new = "second" 315 } 316 } 317 318 module "mod" { 319 source = "./mod" 320 for_each = local.things 321 } 322 `, 323 324 "./mod/main.tf": ` 325 resource "test_resource" "a" { 326 } 327 328 data "test_data_source" "d" { 329 depends_on = [test_resource.a] 330 } 331 332 resource "test_resource" "b" { 333 value = data.test_data_source.d.id 334 } 335 `}) 336 337 oldDataAddr := mustResourceInstanceAddr(`module.mod["old"].data.test_data_source.d`) 338 339 state := states.BuildState(func(s *states.SyncState) { 340 s.SetResourceInstanceCurrent( 341 mustResourceInstanceAddr(`module.mod["old"].test_resource.a`), 342 &states.ResourceInstanceObjectSrc{ 343 AttrsJSON: []byte(`{"id":"a"}`), 344 Status: states.ObjectReady, 345 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 346 ) 347 s.SetResourceInstanceCurrent( 348 mustResourceInstanceAddr(`module.mod["old"].test_resource.b`), 349 &states.ResourceInstanceObjectSrc{ 350 AttrsJSON: []byte(`{"id":"b","value":"d"}`), 351 Status: states.ObjectReady, 352 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 353 ) 354 s.SetResourceInstanceCurrent( 355 oldDataAddr, 356 &states.ResourceInstanceObjectSrc{ 357 AttrsJSON: []byte(`{"id":"d"}`), 358 Status: states.ObjectReady, 359 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 360 ) 361 }) 362 363 ctx := testContext2(t, &ContextOpts{ 364 Providers: map[addrs.Provider]providers.Factory{ 365 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 366 }, 367 }) 368 369 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 370 assertNoErrors(t, diags) 371 372 oldMod := oldDataAddr.Module 373 374 for _, c := range plan.Changes.Resources { 375 // there should be no changes from the old module instance 376 if c.Addr.Module.Equal(oldMod) && c.Action != plans.NoOp { 377 t.Errorf("unexpected change %s for %s\n", c.Action, c.Addr) 378 } 379 } 380 } 381 382 func TestContext2Plan_destroyWithRefresh(t *testing.T) { 383 m := testModuleInline(t, map[string]string{ 384 "main.tf": ` 385 resource "test_object" "a" { 386 } 387 `, 388 }) 389 390 p := simpleMockProvider() 391 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 392 Provider: providers.Schema{Block: simpleTestSchema()}, 393 ResourceTypes: map[string]providers.Schema{ 394 "test_object": { 395 Block: &configschema.Block{ 396 Attributes: map[string]*configschema.Attribute{ 397 "arg": {Type: cty.String, Optional: true}, 398 }, 399 }, 400 }, 401 }, 402 } 403 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 404 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 405 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 406 return cty.StringVal("current"), nil 407 } 408 return v, nil 409 }) 410 if err != nil { 411 // shouldn't get here 412 t.Fatalf("ReadResourceFn transform failed") 413 return providers.ReadResourceResponse{} 414 } 415 return providers.ReadResourceResponse{ 416 NewState: newVal, 417 } 418 } 419 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 420 t.Logf("UpgradeResourceState %s", req.RawStateJSON) 421 422 // In the destroy-with-refresh codepath we end up calling 423 // UpgradeResourceState twice, because we do so once during refreshing 424 // (as part making a normal plan) and then again during the plan-destroy 425 // walk. The second call recieves the result of the earlier refresh, 426 // so we need to tolerate both "before" and "current" as possible 427 // inputs here. 428 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 429 if !bytes.Contains(req.RawStateJSON, []byte("current")) { 430 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object or the 'current' object\n%s", req.RawStateJSON) 431 } 432 } 433 434 // We'll put something different in "arg" as part of upgrading, just 435 // so that we can verify below that PrevRunState contains the upgraded 436 // (but NOT refreshed) version of the object. 437 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 438 "arg": cty.StringVal("upgraded"), 439 }) 440 return resp 441 } 442 443 addr := mustResourceInstanceAddr("test_object.a") 444 state := states.BuildState(func(s *states.SyncState) { 445 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 446 AttrsJSON: []byte(`{"arg":"before"}`), 447 Status: states.ObjectReady, 448 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 449 }) 450 451 ctx := testContext2(t, &ContextOpts{ 452 Providers: map[addrs.Provider]providers.Factory{ 453 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 454 }, 455 }) 456 457 plan, diags := ctx.Plan(m, state, &PlanOpts{ 458 Mode: plans.DestroyMode, 459 SkipRefresh: false, // the default 460 }) 461 assertNoErrors(t, diags) 462 463 if !p.UpgradeResourceStateCalled { 464 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 465 } 466 if !p.ReadResourceCalled { 467 t.Errorf("Provider's ReadResource wasn't called; should've been") 468 } 469 470 if plan.PriorState == nil { 471 t.Fatal("missing plan state") 472 } 473 474 for _, c := range plan.Changes.Resources { 475 if c.Action != plans.Delete { 476 t.Errorf("unexpected %s change for %s", c.Action, c.Addr) 477 } 478 } 479 480 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 481 t.Errorf("%s has no previous run state at all after plan", addr) 482 } else { 483 if instState.Current == nil { 484 t.Errorf("%s has no current object in the previous run state", addr) 485 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 486 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 487 } 488 } 489 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 490 t.Errorf("%s has no prior state at all after plan", addr) 491 } else { 492 if instState.Current == nil { 493 t.Errorf("%s has no current object in the prior state", addr) 494 } else if got, want := instState.Current.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 495 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 496 } 497 } 498 } 499 500 func TestContext2Plan_destroySkipRefresh(t *testing.T) { 501 m := testModuleInline(t, map[string]string{ 502 "main.tf": ` 503 resource "test_object" "a" { 504 } 505 `, 506 }) 507 508 p := simpleMockProvider() 509 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 510 Provider: providers.Schema{Block: simpleTestSchema()}, 511 ResourceTypes: map[string]providers.Schema{ 512 "test_object": { 513 Block: &configschema.Block{ 514 Attributes: map[string]*configschema.Attribute{ 515 "arg": {Type: cty.String, Optional: true}, 516 }, 517 }, 518 }, 519 }, 520 } 521 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 522 t.Helper() 523 t.Errorf("unexpected call to ReadResource") 524 resp.NewState = req.PriorState 525 return resp 526 } 527 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 528 t.Logf("UpgradeResourceState %s", req.RawStateJSON) 529 // We should've been given the prior state JSON as our input to upgrade. 530 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 531 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 532 } 533 534 // We'll put something different in "arg" as part of upgrading, just 535 // so that we can verify below that PrevRunState contains the upgraded 536 // (but NOT refreshed) version of the object. 537 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 538 "arg": cty.StringVal("upgraded"), 539 }) 540 return resp 541 } 542 543 addr := mustResourceInstanceAddr("test_object.a") 544 state := states.BuildState(func(s *states.SyncState) { 545 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 546 AttrsJSON: []byte(`{"arg":"before"}`), 547 Status: states.ObjectReady, 548 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 549 }) 550 551 ctx := testContext2(t, &ContextOpts{ 552 Providers: map[addrs.Provider]providers.Factory{ 553 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 554 }, 555 }) 556 557 plan, diags := ctx.Plan(m, state, &PlanOpts{ 558 Mode: plans.DestroyMode, 559 SkipRefresh: true, 560 }) 561 assertNoErrors(t, diags) 562 563 if !p.UpgradeResourceStateCalled { 564 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 565 } 566 if p.ReadResourceCalled { 567 t.Errorf("Provider's ReadResource was called; shouldn't have been") 568 } 569 570 if plan.PriorState == nil { 571 t.Fatal("missing plan state") 572 } 573 574 for _, c := range plan.Changes.Resources { 575 if c.Action != plans.Delete { 576 t.Errorf("unexpected %s change for %s", c.Action, c.Addr) 577 } 578 } 579 580 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 581 t.Errorf("%s has no previous run state at all after plan", addr) 582 } else { 583 if instState.Current == nil { 584 t.Errorf("%s has no current object in the previous run state", addr) 585 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 586 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 587 } 588 } 589 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 590 t.Errorf("%s has no prior state at all after plan", addr) 591 } else { 592 if instState.Current == nil { 593 t.Errorf("%s has no current object in the prior state", addr) 594 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 595 // NOTE: The prior state should still have been _upgraded_, even 596 // though we skipped running refresh after upgrading it. 597 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 598 } 599 } 600 } 601 602 func TestContext2Plan_unmarkingSensitiveAttributeForOutput(t *testing.T) { 603 m := testModuleInline(t, map[string]string{ 604 "main.tf": ` 605 resource "test_resource" "foo" { 606 } 607 608 output "result" { 609 value = nonsensitive(test_resource.foo.sensitive_attr) 610 } 611 `, 612 }) 613 614 p := new(MockProvider) 615 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 616 ResourceTypes: map[string]*configschema.Block{ 617 "test_resource": { 618 Attributes: map[string]*configschema.Attribute{ 619 "id": { 620 Type: cty.String, 621 Computed: true, 622 }, 623 "sensitive_attr": { 624 Type: cty.String, 625 Computed: true, 626 Sensitive: true, 627 }, 628 }, 629 }, 630 }, 631 }) 632 633 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 634 return providers.PlanResourceChangeResponse{ 635 PlannedState: cty.UnknownVal(cty.Object(map[string]cty.Type{ 636 "id": cty.String, 637 "sensitive_attr": cty.String, 638 })), 639 } 640 } 641 642 state := states.NewState() 643 644 ctx := testContext2(t, &ContextOpts{ 645 Providers: map[addrs.Provider]providers.Factory{ 646 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 647 }, 648 }) 649 650 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 651 assertNoErrors(t, diags) 652 653 for _, res := range plan.Changes.Resources { 654 if res.Action != plans.Create { 655 t.Fatalf("expected create, got: %q %s", res.Addr, res.Action) 656 } 657 } 658 } 659 660 func TestContext2Plan_destroyNoProviderConfig(t *testing.T) { 661 // providers do not need to be configured during a destroy plan 662 p := simpleMockProvider() 663 p.ValidateProviderConfigFn = func(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { 664 v := req.Config.GetAttr("test_string") 665 if v.IsNull() || !v.IsKnown() || v.AsString() != "ok" { 666 resp.Diagnostics = resp.Diagnostics.Append(errors.New("invalid provider configuration")) 667 } 668 return resp 669 } 670 671 m := testModuleInline(t, map[string]string{ 672 "main.tf": ` 673 locals { 674 value = "ok" 675 } 676 677 provider "test" { 678 test_string = local.value 679 } 680 `, 681 }) 682 683 addr := mustResourceInstanceAddr("test_object.a") 684 state := states.BuildState(func(s *states.SyncState) { 685 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 686 AttrsJSON: []byte(`{"test_string":"foo"}`), 687 Status: states.ObjectReady, 688 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 689 }) 690 691 ctx := testContext2(t, &ContextOpts{ 692 Providers: map[addrs.Provider]providers.Factory{ 693 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 694 }, 695 }) 696 697 _, diags := ctx.Plan(m, state, &PlanOpts{ 698 Mode: plans.DestroyMode, 699 }) 700 assertNoErrors(t, diags) 701 } 702 703 func TestContext2Plan_movedResourceBasic(t *testing.T) { 704 addrA := mustResourceInstanceAddr("test_object.a") 705 addrB := mustResourceInstanceAddr("test_object.b") 706 m := testModuleInline(t, map[string]string{ 707 "main.tf": ` 708 resource "test_object" "b" { 709 } 710 711 moved { 712 from = test_object.a 713 to = test_object.b 714 } 715 716 terraform { 717 experiments = [config_driven_move] 718 } 719 `, 720 }) 721 722 state := states.BuildState(func(s *states.SyncState) { 723 // The prior state tracks test_object.a, which we should treat as 724 // test_object.b because of the "moved" block in the config. 725 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 726 AttrsJSON: []byte(`{}`), 727 Status: states.ObjectReady, 728 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 729 }) 730 731 p := simpleMockProvider() 732 ctx := testContext2(t, &ContextOpts{ 733 Providers: map[addrs.Provider]providers.Factory{ 734 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 735 }, 736 }) 737 738 plan, diags := ctx.Plan(m, state, &PlanOpts{ 739 Mode: plans.NormalMode, 740 ForceReplace: []addrs.AbsResourceInstance{ 741 addrA, 742 }, 743 }) 744 if diags.HasErrors() { 745 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 746 } 747 748 t.Run(addrA.String(), func(t *testing.T) { 749 instPlan := plan.Changes.ResourceInstance(addrA) 750 if instPlan != nil { 751 t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) 752 } 753 }) 754 t.Run(addrB.String(), func(t *testing.T) { 755 instPlan := plan.Changes.ResourceInstance(addrB) 756 if instPlan == nil { 757 t.Fatalf("no plan for %s at all", addrB) 758 } 759 760 if got, want := instPlan.Addr, addrB; !got.Equal(want) { 761 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 762 } 763 if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { 764 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 765 } 766 if got, want := instPlan.Action, plans.NoOp; got != want { 767 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 768 } 769 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 770 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 771 } 772 }) 773 } 774 775 func TestContext2Plan_refreshOnlyMode(t *testing.T) { 776 addr := mustResourceInstanceAddr("test_object.a") 777 778 // The configuration, the prior state, and the refresh result intentionally 779 // have different values for "test_string" so we can observe that the 780 // refresh took effect but the configuration change wasn't considered. 781 m := testModuleInline(t, map[string]string{ 782 "main.tf": ` 783 resource "test_object" "a" { 784 arg = "after" 785 } 786 787 output "out" { 788 value = test_object.a.arg 789 } 790 `, 791 }) 792 state := states.BuildState(func(s *states.SyncState) { 793 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 794 AttrsJSON: []byte(`{"arg":"before"}`), 795 Status: states.ObjectReady, 796 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 797 }) 798 799 p := simpleMockProvider() 800 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 801 Provider: providers.Schema{Block: simpleTestSchema()}, 802 ResourceTypes: map[string]providers.Schema{ 803 "test_object": { 804 Block: &configschema.Block{ 805 Attributes: map[string]*configschema.Attribute{ 806 "arg": {Type: cty.String, Optional: true}, 807 }, 808 }, 809 }, 810 }, 811 } 812 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 813 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 814 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 815 return cty.StringVal("current"), nil 816 } 817 return v, nil 818 }) 819 if err != nil { 820 // shouldn't get here 821 t.Fatalf("ReadResourceFn transform failed") 822 return providers.ReadResourceResponse{} 823 } 824 return providers.ReadResourceResponse{ 825 NewState: newVal, 826 } 827 } 828 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 829 // We should've been given the prior state JSON as our input to upgrade. 830 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 831 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 832 } 833 834 // We'll put something different in "arg" as part of upgrading, just 835 // so that we can verify below that PrevRunState contains the upgraded 836 // (but NOT refreshed) version of the object. 837 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 838 "arg": cty.StringVal("upgraded"), 839 }) 840 return resp 841 } 842 843 ctx := testContext2(t, &ContextOpts{ 844 Providers: map[addrs.Provider]providers.Factory{ 845 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 846 }, 847 }) 848 849 plan, diags := ctx.Plan(m, state, &PlanOpts{ 850 Mode: plans.RefreshOnlyMode, 851 }) 852 if diags.HasErrors() { 853 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 854 } 855 856 if !p.UpgradeResourceStateCalled { 857 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 858 } 859 if !p.ReadResourceCalled { 860 t.Errorf("Provider's ReadResource wasn't called; should've been") 861 } 862 863 if got, want := len(plan.Changes.Resources), 0; got != want { 864 t.Errorf("plan contains resource changes; want none\n%s", spew.Sdump(plan.Changes.Resources)) 865 } 866 867 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 868 t.Errorf("%s has no prior state at all after plan", addr) 869 } else { 870 if instState.Current == nil { 871 t.Errorf("%s has no current object after plan", addr) 872 } else if got, want := instState.Current.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 873 // Should've saved the result of refreshing 874 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 875 } 876 } 877 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 878 t.Errorf("%s has no previous run state at all after plan", addr) 879 } else { 880 if instState.Current == nil { 881 t.Errorf("%s has no current object in the previous run state", addr) 882 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 883 // Should've saved the result of upgrading 884 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 885 } 886 } 887 888 // The output value should also have updated. If not, it's likely that we 889 // skipped updating the working state to match the refreshed state when we 890 // were evaluating the resource. 891 if outChangeSrc := plan.Changes.OutputValue(addrs.RootModuleInstance.OutputValue("out")); outChangeSrc == nil { 892 t.Errorf("no change planned for output value 'out'") 893 } else { 894 outChange, err := outChangeSrc.Decode() 895 if err != nil { 896 t.Fatalf("failed to decode output value 'out': %s", err) 897 } 898 got := outChange.After 899 want := cty.StringVal("current") 900 if !want.RawEquals(got) { 901 t.Errorf("wrong value for output value 'out'\ngot: %#v\nwant: %#v", got, want) 902 } 903 } 904 } 905 906 func TestContext2Plan_refreshOnlyMode_deposed(t *testing.T) { 907 addr := mustResourceInstanceAddr("test_object.a") 908 deposedKey := states.DeposedKey("byebye") 909 910 // The configuration, the prior state, and the refresh result intentionally 911 // have different values for "test_string" so we can observe that the 912 // refresh took effect but the configuration change wasn't considered. 913 m := testModuleInline(t, map[string]string{ 914 "main.tf": ` 915 resource "test_object" "a" { 916 arg = "after" 917 } 918 919 output "out" { 920 value = test_object.a.arg 921 } 922 `, 923 }) 924 state := states.BuildState(func(s *states.SyncState) { 925 // Note that we're intentionally recording a _deposed_ object here, 926 // and not including a current object, so a normal (non-refresh) 927 // plan would normally plan to create a new object _and_ destroy 928 // the deposed one, but refresh-only mode should prevent that. 929 s.SetResourceInstanceDeposed(addr, deposedKey, &states.ResourceInstanceObjectSrc{ 930 AttrsJSON: []byte(`{"arg":"before"}`), 931 Status: states.ObjectReady, 932 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 933 }) 934 935 p := simpleMockProvider() 936 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 937 Provider: providers.Schema{Block: simpleTestSchema()}, 938 ResourceTypes: map[string]providers.Schema{ 939 "test_object": { 940 Block: &configschema.Block{ 941 Attributes: map[string]*configschema.Attribute{ 942 "arg": {Type: cty.String, Optional: true}, 943 }, 944 }, 945 }, 946 }, 947 } 948 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 949 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 950 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 951 return cty.StringVal("current"), nil 952 } 953 return v, nil 954 }) 955 if err != nil { 956 // shouldn't get here 957 t.Fatalf("ReadResourceFn transform failed") 958 return providers.ReadResourceResponse{} 959 } 960 return providers.ReadResourceResponse{ 961 NewState: newVal, 962 } 963 } 964 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 965 // We should've been given the prior state JSON as our input to upgrade. 966 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 967 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 968 } 969 970 // We'll put something different in "arg" as part of upgrading, just 971 // so that we can verify below that PrevRunState contains the upgraded 972 // (but NOT refreshed) version of the object. 973 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 974 "arg": cty.StringVal("upgraded"), 975 }) 976 return resp 977 } 978 979 ctx := testContext2(t, &ContextOpts{ 980 Providers: map[addrs.Provider]providers.Factory{ 981 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 982 }, 983 }) 984 985 plan, diags := ctx.Plan(m, state, &PlanOpts{ 986 Mode: plans.RefreshOnlyMode, 987 }) 988 if diags.HasErrors() { 989 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 990 } 991 992 if !p.UpgradeResourceStateCalled { 993 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 994 } 995 if !p.ReadResourceCalled { 996 t.Errorf("Provider's ReadResource wasn't called; should've been") 997 } 998 999 if got, want := len(plan.Changes.Resources), 0; got != want { 1000 t.Errorf("plan contains resource changes; want none\n%s", spew.Sdump(plan.Changes.Resources)) 1001 } 1002 1003 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 1004 t.Errorf("%s has no prior state at all after plan", addr) 1005 } else { 1006 if obj := instState.Deposed[deposedKey]; obj == nil { 1007 t.Errorf("%s has no deposed object after plan", addr) 1008 } else if got, want := obj.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 1009 // Should've saved the result of refreshing 1010 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1011 } 1012 } 1013 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 1014 t.Errorf("%s has no previous run state at all after plan", addr) 1015 } else { 1016 if obj := instState.Deposed[deposedKey]; obj == nil { 1017 t.Errorf("%s has no deposed object in the previous run state", addr) 1018 } else if got, want := obj.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 1019 // Should've saved the result of upgrading 1020 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1021 } 1022 } 1023 1024 // The output value should also have updated. If not, it's likely that we 1025 // skipped updating the working state to match the refreshed state when we 1026 // were evaluating the resource. 1027 if outChangeSrc := plan.Changes.OutputValue(addrs.RootModuleInstance.OutputValue("out")); outChangeSrc == nil { 1028 t.Errorf("no change planned for output value 'out'") 1029 } else { 1030 outChange, err := outChangeSrc.Decode() 1031 if err != nil { 1032 t.Fatalf("failed to decode output value 'out': %s", err) 1033 } 1034 got := outChange.After 1035 want := cty.UnknownVal(cty.String) 1036 if !want.RawEquals(got) { 1037 t.Errorf("wrong value for output value 'out'\ngot: %#v\nwant: %#v", got, want) 1038 } 1039 } 1040 } 1041 1042 func TestContext2Plan_invalidSensitiveModuleOutput(t *testing.T) { 1043 m := testModuleInline(t, map[string]string{ 1044 "child/main.tf": ` 1045 output "out" { 1046 value = sensitive("xyz") 1047 }`, 1048 "main.tf": ` 1049 module "child" { 1050 source = "./child" 1051 } 1052 1053 output "root" { 1054 value = module.child.out 1055 }`, 1056 }) 1057 1058 ctx := testContext2(t, &ContextOpts{}) 1059 1060 _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 1061 if !diags.HasErrors() { 1062 t.Fatal("succeeded; want errors") 1063 } 1064 if got, want := diags.Err().Error(), "Output refers to sensitive values"; !strings.Contains(got, want) { 1065 t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) 1066 } 1067 } 1068 1069 func TestContext2Plan_planDataSourceSensitiveNested(t *testing.T) { 1070 m := testModuleInline(t, map[string]string{ 1071 "main.tf": ` 1072 resource "test_instance" "bar" { 1073 } 1074 1075 data "test_data_source" "foo" { 1076 foo { 1077 bar = test_instance.bar.sensitive 1078 } 1079 } 1080 `, 1081 }) 1082 1083 p := new(MockProvider) 1084 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 1085 resp.PlannedState = cty.ObjectVal(map[string]cty.Value{ 1086 "sensitive": cty.UnknownVal(cty.String), 1087 }) 1088 return resp 1089 } 1090 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 1091 ResourceTypes: map[string]*configschema.Block{ 1092 "test_instance": { 1093 Attributes: map[string]*configschema.Attribute{ 1094 "sensitive": { 1095 Type: cty.String, 1096 Computed: true, 1097 Sensitive: true, 1098 }, 1099 }, 1100 }, 1101 }, 1102 DataSources: map[string]*configschema.Block{ 1103 "test_data_source": { 1104 Attributes: map[string]*configschema.Attribute{ 1105 "id": { 1106 Type: cty.String, 1107 Computed: true, 1108 }, 1109 }, 1110 BlockTypes: map[string]*configschema.NestedBlock{ 1111 "foo": { 1112 Block: configschema.Block{ 1113 Attributes: map[string]*configschema.Attribute{ 1114 "bar": {Type: cty.String, Optional: true}, 1115 }, 1116 }, 1117 Nesting: configschema.NestingSet, 1118 }, 1119 }, 1120 }, 1121 }, 1122 }) 1123 1124 state := states.NewState() 1125 root := state.EnsureModule(addrs.RootModuleInstance) 1126 root.SetResourceInstanceCurrent( 1127 mustResourceInstanceAddr("data.test_data_source.foo").Resource, 1128 &states.ResourceInstanceObjectSrc{ 1129 Status: states.ObjectReady, 1130 AttrsJSON: []byte(`{"string":"data_id", "foo":[{"bar":"old"}]}`), 1131 AttrSensitivePaths: []cty.PathValueMarks{ 1132 { 1133 Path: cty.GetAttrPath("foo"), 1134 Marks: cty.NewValueMarks(marks.Sensitive), 1135 }, 1136 }, 1137 }, 1138 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 1139 ) 1140 root.SetResourceInstanceCurrent( 1141 mustResourceInstanceAddr("test_instance.bar").Resource, 1142 &states.ResourceInstanceObjectSrc{ 1143 Status: states.ObjectReady, 1144 AttrsJSON: []byte(`{"sensitive":"old"}`), 1145 AttrSensitivePaths: []cty.PathValueMarks{ 1146 { 1147 Path: cty.GetAttrPath("sensitive"), 1148 Marks: cty.NewValueMarks(marks.Sensitive), 1149 }, 1150 }, 1151 }, 1152 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 1153 ) 1154 1155 ctx := testContext2(t, &ContextOpts{ 1156 Providers: map[addrs.Provider]providers.Factory{ 1157 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1158 }, 1159 }) 1160 1161 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 1162 assertNoErrors(t, diags) 1163 1164 for _, res := range plan.Changes.Resources { 1165 switch res.Addr.String() { 1166 case "test_instance.bar": 1167 if res.Action != plans.Update { 1168 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 1169 } 1170 case "data.test_data_source.foo": 1171 if res.Action != plans.Read { 1172 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 1173 } 1174 default: 1175 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 1176 } 1177 } 1178 } 1179 1180 func TestContext2Plan_forceReplace(t *testing.T) { 1181 addrA := mustResourceInstanceAddr("test_object.a") 1182 addrB := mustResourceInstanceAddr("test_object.b") 1183 m := testModuleInline(t, map[string]string{ 1184 "main.tf": ` 1185 resource "test_object" "a" { 1186 } 1187 resource "test_object" "b" { 1188 } 1189 `, 1190 }) 1191 1192 state := states.BuildState(func(s *states.SyncState) { 1193 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 1194 AttrsJSON: []byte(`{}`), 1195 Status: states.ObjectReady, 1196 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1197 s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{ 1198 AttrsJSON: []byte(`{}`), 1199 Status: states.ObjectReady, 1200 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1201 }) 1202 1203 p := simpleMockProvider() 1204 ctx := testContext2(t, &ContextOpts{ 1205 Providers: map[addrs.Provider]providers.Factory{ 1206 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1207 }, 1208 }) 1209 1210 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1211 Mode: plans.NormalMode, 1212 ForceReplace: []addrs.AbsResourceInstance{ 1213 addrA, 1214 }, 1215 }) 1216 if diags.HasErrors() { 1217 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1218 } 1219 1220 t.Run(addrA.String(), func(t *testing.T) { 1221 instPlan := plan.Changes.ResourceInstance(addrA) 1222 if instPlan == nil { 1223 t.Fatalf("no plan for %s at all", addrA) 1224 } 1225 1226 if got, want := instPlan.Action, plans.DeleteThenCreate; got != want { 1227 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1228 } 1229 if got, want := instPlan.ActionReason, plans.ResourceInstanceReplaceByRequest; got != want { 1230 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1231 } 1232 }) 1233 t.Run(addrB.String(), func(t *testing.T) { 1234 instPlan := plan.Changes.ResourceInstance(addrB) 1235 if instPlan == nil { 1236 t.Fatalf("no plan for %s at all", addrB) 1237 } 1238 1239 if got, want := instPlan.Action, plans.NoOp; got != want { 1240 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1241 } 1242 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1243 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1244 } 1245 }) 1246 } 1247 1248 func TestContext2Plan_forceReplaceIncompleteAddr(t *testing.T) { 1249 addr0 := mustResourceInstanceAddr("test_object.a[0]") 1250 addr1 := mustResourceInstanceAddr("test_object.a[1]") 1251 addrBare := mustResourceInstanceAddr("test_object.a") 1252 m := testModuleInline(t, map[string]string{ 1253 "main.tf": ` 1254 resource "test_object" "a" { 1255 count = 2 1256 } 1257 `, 1258 }) 1259 1260 state := states.BuildState(func(s *states.SyncState) { 1261 s.SetResourceInstanceCurrent(addr0, &states.ResourceInstanceObjectSrc{ 1262 AttrsJSON: []byte(`{}`), 1263 Status: states.ObjectReady, 1264 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1265 s.SetResourceInstanceCurrent(addr1, &states.ResourceInstanceObjectSrc{ 1266 AttrsJSON: []byte(`{}`), 1267 Status: states.ObjectReady, 1268 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1269 }) 1270 1271 p := simpleMockProvider() 1272 ctx := testContext2(t, &ContextOpts{ 1273 Providers: map[addrs.Provider]providers.Factory{ 1274 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1275 }, 1276 }) 1277 1278 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1279 Mode: plans.NormalMode, 1280 ForceReplace: []addrs.AbsResourceInstance{ 1281 addrBare, 1282 }, 1283 }) 1284 if diags.HasErrors() { 1285 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1286 } 1287 diagsErr := diags.ErrWithWarnings() 1288 if diagsErr == nil { 1289 t.Fatalf("no warnings were returned") 1290 } 1291 if got, want := diagsErr.Error(), "Incompletely-matched force-replace resource instance"; !strings.Contains(got, want) { 1292 t.Errorf("missing expected warning\ngot:\n%s\n\nwant substring: %s", got, want) 1293 } 1294 1295 t.Run(addr0.String(), func(t *testing.T) { 1296 instPlan := plan.Changes.ResourceInstance(addr0) 1297 if instPlan == nil { 1298 t.Fatalf("no plan for %s at all", addr0) 1299 } 1300 1301 if got, want := instPlan.Action, plans.NoOp; got != want { 1302 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1303 } 1304 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1305 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1306 } 1307 }) 1308 t.Run(addr1.String(), func(t *testing.T) { 1309 instPlan := plan.Changes.ResourceInstance(addr1) 1310 if instPlan == nil { 1311 t.Fatalf("no plan for %s at all", addr1) 1312 } 1313 1314 if got, want := instPlan.Action, plans.NoOp; got != want { 1315 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1316 } 1317 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1318 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1319 } 1320 }) 1321 }