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