github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/context_plan2_test.go (about) 1 package terraform 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "strings" 8 "sync" 9 "testing" 10 11 "github.com/davecgh/go-spew/spew" 12 "github.com/google/go-cmp/cmp" 13 "github.com/hashicorp/terraform/internal/addrs" 14 "github.com/hashicorp/terraform/internal/checks" 15 "github.com/hashicorp/terraform/internal/configs/configschema" 16 "github.com/hashicorp/terraform/internal/lang/marks" 17 "github.com/hashicorp/terraform/internal/plans" 18 "github.com/hashicorp/terraform/internal/providers" 19 "github.com/hashicorp/terraform/internal/states" 20 "github.com/hashicorp/terraform/internal/tfdiags" 21 "github.com/zclconf/go-cty/cty" 22 ) 23 24 func TestContext2Plan_removedDuringRefresh(t *testing.T) { 25 // This tests the situation where an object tracked in the previous run 26 // state has been deleted outside of Terraform, which we should detect 27 // during the refresh step and thus ultimately produce a plan to recreate 28 // the object, since it's still present in the configuration. 29 m := testModuleInline(t, map[string]string{ 30 "main.tf": ` 31 resource "test_object" "a" { 32 } 33 `, 34 }) 35 36 p := simpleMockProvider() 37 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 38 Provider: providers.Schema{Block: simpleTestSchema()}, 39 ResourceTypes: map[string]providers.Schema{ 40 "test_object": { 41 Block: &configschema.Block{ 42 Attributes: map[string]*configschema.Attribute{ 43 "arg": {Type: cty.String, Optional: true}, 44 }, 45 }, 46 }, 47 }, 48 } 49 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 50 resp.NewState = cty.NullVal(req.PriorState.Type()) 51 return resp 52 } 53 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 54 // We should've been given the prior state JSON as our input to upgrade. 55 if !bytes.Contains(req.RawStateJSON, []byte("previous_run")) { 56 t.Fatalf("UpgradeResourceState request doesn't contain the previous run object\n%s", req.RawStateJSON) 57 } 58 59 // We'll put something different in "arg" as part of upgrading, just 60 // so that we can verify below that PrevRunState contains the upgraded 61 // (but NOT refreshed) version of the object. 62 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 63 "arg": cty.StringVal("upgraded"), 64 }) 65 return resp 66 } 67 68 addr := mustResourceInstanceAddr("test_object.a") 69 state := states.BuildState(func(s *states.SyncState) { 70 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 71 AttrsJSON: []byte(`{"arg":"previous_run"}`), 72 Status: states.ObjectTainted, 73 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 74 }) 75 76 ctx := testContext2(t, &ContextOpts{ 77 Providers: map[addrs.Provider]providers.Factory{ 78 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 79 }, 80 }) 81 82 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 83 assertNoErrors(t, diags) 84 85 if !p.UpgradeResourceStateCalled { 86 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 87 } 88 if !p.ReadResourceCalled { 89 t.Errorf("Provider's ReadResource wasn't called; should've been") 90 } 91 92 // The object should be absent from the plan's prior state, because that 93 // records the result of refreshing. 94 if got := plan.PriorState.ResourceInstance(addr); got != nil { 95 t.Errorf( 96 "instance %s is in the prior state after planning; should've been removed\n%s", 97 addr, spew.Sdump(got), 98 ) 99 } 100 101 // However, the object should still be in the PrevRunState, because 102 // that reflects what we believed to exist before refreshing. 103 if got := plan.PrevRunState.ResourceInstance(addr); got == nil { 104 t.Errorf( 105 "instance %s is missing from the previous run state after planning; should've been preserved", 106 addr, 107 ) 108 } else { 109 if !bytes.Contains(got.Current.AttrsJSON, []byte("upgraded")) { 110 t.Fatalf("previous run state has non-upgraded object\n%s", got.Current.AttrsJSON) 111 } 112 } 113 114 // This situation should result in a drifted resource change. 115 var drifted *plans.ResourceInstanceChangeSrc 116 for _, dr := range plan.DriftedResources { 117 if dr.Addr.Equal(addr) { 118 drifted = dr 119 break 120 } 121 } 122 123 if drifted == nil { 124 t.Errorf("instance %s is missing from the drifted resource changes", addr) 125 } else { 126 if got, want := drifted.Action, plans.Delete; got != want { 127 t.Errorf("unexpected instance %s drifted resource change action. got: %s, want: %s", addr, got, want) 128 } 129 } 130 131 // Because the configuration still mentions test_object.a, we should've 132 // planned to recreate it in order to fix the drift. 133 for _, c := range plan.Changes.Resources { 134 if c.Action != plans.Create { 135 t.Fatalf("expected Create action for missing %s, got %s", c.Addr, c.Action) 136 } 137 } 138 } 139 140 func TestContext2Plan_noChangeDataSourceSensitiveNestedSet(t *testing.T) { 141 m := testModuleInline(t, map[string]string{ 142 "main.tf": ` 143 variable "bar" { 144 sensitive = true 145 default = "baz" 146 } 147 148 data "test_data_source" "foo" { 149 foo { 150 bar = var.bar 151 } 152 } 153 `, 154 }) 155 156 p := new(MockProvider) 157 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 158 DataSources: map[string]*configschema.Block{ 159 "test_data_source": { 160 Attributes: map[string]*configschema.Attribute{ 161 "id": { 162 Type: cty.String, 163 Computed: true, 164 }, 165 }, 166 BlockTypes: map[string]*configschema.NestedBlock{ 167 "foo": { 168 Block: configschema.Block{ 169 Attributes: map[string]*configschema.Attribute{ 170 "bar": {Type: cty.String, Optional: true}, 171 }, 172 }, 173 Nesting: configschema.NestingSet, 174 }, 175 }, 176 }, 177 }, 178 }) 179 180 p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ 181 State: cty.ObjectVal(map[string]cty.Value{ 182 "id": cty.StringVal("data_id"), 183 "foo": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"bar": cty.StringVal("baz")})}), 184 }), 185 } 186 187 state := states.NewState() 188 root := state.EnsureModule(addrs.RootModuleInstance) 189 root.SetResourceInstanceCurrent( 190 mustResourceInstanceAddr("data.test_data_source.foo").Resource, 191 &states.ResourceInstanceObjectSrc{ 192 Status: states.ObjectReady, 193 AttrsJSON: []byte(`{"id":"data_id", "foo":[{"bar":"baz"}]}`), 194 AttrSensitivePaths: []cty.PathValueMarks{ 195 { 196 Path: cty.GetAttrPath("foo"), 197 Marks: cty.NewValueMarks(marks.Sensitive), 198 }, 199 }, 200 }, 201 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 202 ) 203 204 ctx := testContext2(t, &ContextOpts{ 205 Providers: map[addrs.Provider]providers.Factory{ 206 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 207 }, 208 }) 209 210 plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) 211 assertNoErrors(t, diags) 212 213 for _, res := range plan.Changes.Resources { 214 if res.Action != plans.NoOp { 215 t.Fatalf("expected NoOp, got: %q %s", res.Addr, res.Action) 216 } 217 } 218 } 219 220 func TestContext2Plan_orphanDataInstance(t *testing.T) { 221 // ensure the planned replacement of the data source is evaluated properly 222 m := testModuleInline(t, map[string]string{ 223 "main.tf": ` 224 data "test_object" "a" { 225 for_each = { new = "ok" } 226 } 227 228 output "out" { 229 value = [ for k, _ in data.test_object.a: k ] 230 } 231 `, 232 }) 233 234 p := simpleMockProvider() 235 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 236 resp.State = req.Config 237 return resp 238 } 239 240 state := states.BuildState(func(s *states.SyncState) { 241 s.SetResourceInstanceCurrent(mustResourceInstanceAddr(`data.test_object.a["old"]`), &states.ResourceInstanceObjectSrc{ 242 AttrsJSON: []byte(`{"test_string":"foo"}`), 243 Status: states.ObjectReady, 244 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 245 }) 246 247 ctx := testContext2(t, &ContextOpts{ 248 Providers: map[addrs.Provider]providers.Factory{ 249 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 250 }, 251 }) 252 253 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 254 assertNoErrors(t, diags) 255 256 change, err := plan.Changes.Outputs[0].Decode() 257 if err != nil { 258 t.Fatal(err) 259 } 260 261 expected := cty.TupleVal([]cty.Value{cty.StringVal("new")}) 262 263 if change.After.Equals(expected).False() { 264 t.Fatalf("expected %#v, got %#v\n", expected, change.After) 265 } 266 } 267 268 func TestContext2Plan_basicConfigurationAliases(t *testing.T) { 269 m := testModuleInline(t, map[string]string{ 270 "main.tf": ` 271 provider "test" { 272 alias = "z" 273 test_string = "config" 274 } 275 276 module "mod" { 277 source = "./mod" 278 providers = { 279 test.x = test.z 280 } 281 } 282 `, 283 284 "mod/main.tf": ` 285 terraform { 286 required_providers { 287 test = { 288 source = "registry.terraform.io/hashicorp/test" 289 configuration_aliases = [ test.x ] 290 } 291 } 292 } 293 294 resource "test_object" "a" { 295 provider = test.x 296 } 297 298 `, 299 }) 300 301 p := simpleMockProvider() 302 303 // The resource within the module should be using the provider configured 304 // from the root module. We should never see an empty configuration. 305 p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 306 if req.Config.GetAttr("test_string").IsNull() { 307 resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing test_string value")) 308 } 309 return resp 310 } 311 312 ctx := testContext2(t, &ContextOpts{ 313 Providers: map[addrs.Provider]providers.Factory{ 314 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 315 }, 316 }) 317 318 _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 319 assertNoErrors(t, diags) 320 } 321 322 func TestContext2Plan_dataReferencesResourceInModules(t *testing.T) { 323 p := testProvider("test") 324 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 325 cfg := req.Config.AsValueMap() 326 cfg["id"] = cty.StringVal("d") 327 resp.State = cty.ObjectVal(cfg) 328 return resp 329 } 330 331 m := testModuleInline(t, map[string]string{ 332 "main.tf": ` 333 locals { 334 things = { 335 old = "first" 336 new = "second" 337 } 338 } 339 340 module "mod" { 341 source = "./mod" 342 for_each = local.things 343 } 344 `, 345 346 "./mod/main.tf": ` 347 resource "test_resource" "a" { 348 } 349 350 data "test_data_source" "d" { 351 depends_on = [test_resource.a] 352 } 353 354 resource "test_resource" "b" { 355 value = data.test_data_source.d.id 356 } 357 `}) 358 359 oldDataAddr := mustResourceInstanceAddr(`module.mod["old"].data.test_data_source.d`) 360 361 state := states.BuildState(func(s *states.SyncState) { 362 s.SetResourceInstanceCurrent( 363 mustResourceInstanceAddr(`module.mod["old"].test_resource.a`), 364 &states.ResourceInstanceObjectSrc{ 365 AttrsJSON: []byte(`{"id":"a"}`), 366 Status: states.ObjectReady, 367 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 368 ) 369 s.SetResourceInstanceCurrent( 370 mustResourceInstanceAddr(`module.mod["old"].test_resource.b`), 371 &states.ResourceInstanceObjectSrc{ 372 AttrsJSON: []byte(`{"id":"b","value":"d"}`), 373 Status: states.ObjectReady, 374 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 375 ) 376 s.SetResourceInstanceCurrent( 377 oldDataAddr, 378 &states.ResourceInstanceObjectSrc{ 379 AttrsJSON: []byte(`{"id":"d"}`), 380 Status: states.ObjectReady, 381 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 382 ) 383 }) 384 385 ctx := testContext2(t, &ContextOpts{ 386 Providers: map[addrs.Provider]providers.Factory{ 387 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 388 }, 389 }) 390 391 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 392 assertNoErrors(t, diags) 393 394 oldMod := oldDataAddr.Module 395 396 for _, c := range plan.Changes.Resources { 397 // there should be no changes from the old module instance 398 if c.Addr.Module.Equal(oldMod) && c.Action != plans.NoOp { 399 t.Errorf("unexpected change %s for %s\n", c.Action, c.Addr) 400 } 401 } 402 } 403 404 func TestContext2Plan_dataResourceChecksManagedResourceChange(t *testing.T) { 405 // This tests the situation where the remote system contains data that 406 // isn't valid per a data resource postcondition, but that the 407 // configuration is destined to make the remote system valid during apply 408 // and so we must defer reading the data resource and checking its 409 // conditions until the apply step. 410 // 411 // This is an exception to the rule tested in 412 // TestContext2Plan_dataReferencesResourceIndirectly which is relevant 413 // whenever there's at least one precondition or postcondition attached 414 // to a data resource. 415 // 416 // See TestContext2Plan_managedResourceChecksOtherManagedResourceChange for 417 // an incorrect situation where a data resource is used only indirectly 418 // to drive a precondition elsewhere, which therefore doesn't achieve this 419 // special exception. 420 421 p := testProvider("test") 422 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 423 Provider: providers.Schema{ 424 Block: &configschema.Block{}, 425 }, 426 ResourceTypes: map[string]providers.Schema{ 427 "test_resource": { 428 Block: &configschema.Block{ 429 Attributes: map[string]*configschema.Attribute{ 430 "id": { 431 Type: cty.String, 432 Computed: true, 433 }, 434 "valid": { 435 Type: cty.Bool, 436 Required: true, 437 }, 438 }, 439 }, 440 }, 441 }, 442 DataSources: map[string]providers.Schema{ 443 "test_data_source": { 444 Block: &configschema.Block{ 445 Attributes: map[string]*configschema.Attribute{ 446 "id": { 447 Type: cty.String, 448 Required: true, 449 }, 450 "valid": { 451 Type: cty.Bool, 452 Computed: true, 453 }, 454 }, 455 }, 456 }, 457 }, 458 } 459 var mu sync.Mutex 460 validVal := cty.False 461 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 462 // NOTE: This assumes that the prior state declared below will have 463 // "valid" set to false already, and thus will match validVal above. 464 resp.NewState = req.PriorState 465 return resp 466 } 467 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 468 cfg := req.Config.AsValueMap() 469 mu.Lock() 470 cfg["valid"] = validVal 471 mu.Unlock() 472 resp.State = cty.ObjectVal(cfg) 473 return resp 474 } 475 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 476 cfg := req.Config.AsValueMap() 477 prior := req.PriorState.AsValueMap() 478 resp.PlannedState = cty.ObjectVal(map[string]cty.Value{ 479 "id": prior["id"], 480 "valid": cfg["valid"], 481 }) 482 return resp 483 } 484 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 485 planned := req.PlannedState.AsValueMap() 486 487 mu.Lock() 488 validVal = planned["valid"] 489 mu.Unlock() 490 491 resp.NewState = req.PlannedState 492 return resp 493 } 494 495 m := testModuleInline(t, map[string]string{ 496 "main.tf": ` 497 498 resource "test_resource" "a" { 499 valid = true 500 } 501 502 locals { 503 # NOTE: We intentionally read through a local value here to make sure 504 # that this behavior still works even if there isn't a direct dependency 505 # between the data resource and the managed resource. 506 object_id = test_resource.a.id 507 } 508 509 data "test_data_source" "a" { 510 id = local.object_id 511 512 lifecycle { 513 postcondition { 514 condition = self.valid 515 error_message = "Not valid!" 516 } 517 } 518 } 519 `}) 520 521 managedAddr := mustResourceInstanceAddr(`test_resource.a`) 522 dataAddr := mustResourceInstanceAddr(`data.test_data_source.a`) 523 524 // This state is intended to represent the outcome of a previous apply that 525 // failed due to postcondition failure but had already updated the 526 // relevant object to be invalid. 527 // 528 // It could also potentially represent a similar situation where the 529 // previous apply succeeded but there has been a change outside of 530 // Terraform that made it invalid, although technically in that scenario 531 // the state data would become invalid only during the planning step. For 532 // our purposes here that's close enough because we don't have a real 533 // remote system in place anyway. 534 priorState := states.BuildState(func(s *states.SyncState) { 535 s.SetResourceInstanceCurrent( 536 managedAddr, 537 &states.ResourceInstanceObjectSrc{ 538 // NOTE: "valid" is false here but is true in the configuration 539 // above, which is intended to represent that applying the 540 // configuration change would make this object become valid. 541 AttrsJSON: []byte(`{"id":"boop","valid":false}`), 542 Status: states.ObjectReady, 543 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 544 ) 545 }) 546 547 ctx := testContext2(t, &ContextOpts{ 548 Providers: map[addrs.Provider]providers.Factory{ 549 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 550 }, 551 }) 552 553 plan, diags := ctx.Plan(m, priorState, DefaultPlanOpts) 554 assertNoErrors(t, diags) 555 556 if rc := plan.Changes.ResourceInstance(dataAddr); rc != nil { 557 if got, want := rc.Action, plans.Read; got != want { 558 t.Errorf("wrong action for %s\ngot: %s\nwant: %s", dataAddr, got, want) 559 } 560 if got, want := rc.ActionReason, plans.ResourceInstanceReadBecauseDependencyPending; got != want { 561 t.Errorf("wrong action reason for %s\ngot: %s\nwant: %s", dataAddr, got, want) 562 } 563 } else { 564 t.Fatalf("no planned change for %s", dataAddr) 565 } 566 567 if rc := plan.Changes.ResourceInstance(managedAddr); rc != nil { 568 if got, want := rc.Action, plans.Update; got != want { 569 t.Errorf("wrong action for %s\ngot: %s\nwant: %s", managedAddr, got, want) 570 } 571 if got, want := rc.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 572 t.Errorf("wrong action reason for %s\ngot: %s\nwant: %s", managedAddr, got, want) 573 } 574 } else { 575 t.Fatalf("no planned change for %s", managedAddr) 576 } 577 578 // This is primarily a plan-time test, since the special handling of 579 // data resources is a plan-time concern, but we'll still try applying the 580 // plan here just to make sure it's valid. 581 newState, diags := ctx.Apply(plan, m) 582 assertNoErrors(t, diags) 583 584 if rs := newState.ResourceInstance(dataAddr); rs != nil { 585 if !rs.HasCurrent() { 586 t.Errorf("no final state for %s", dataAddr) 587 } 588 } else { 589 t.Errorf("no final state for %s", dataAddr) 590 } 591 592 if rs := newState.ResourceInstance(managedAddr); rs != nil { 593 if !rs.HasCurrent() { 594 t.Errorf("no final state for %s", managedAddr) 595 } 596 } else { 597 t.Errorf("no final state for %s", managedAddr) 598 } 599 600 if got, want := validVal, cty.True; got != want { 601 t.Errorf("wrong final valid value\ngot: %#v\nwant: %#v", got, want) 602 } 603 604 } 605 606 func TestContext2Plan_managedResourceChecksOtherManagedResourceChange(t *testing.T) { 607 // This tests the incorrect situation where a managed resource checks 608 // another managed resource indirectly via a data resource. 609 // This doesn't work because Terraform can't tell that the data resource 610 // outcome will be updated by a separate managed resource change and so 611 // we expect it to fail. 612 // This would ideally have worked except that we previously included a 613 // special case in the rules for data resources where they only consider 614 // direct dependencies when deciding whether to defer (except when the 615 // data resource itself has conditions) and so they can potentially 616 // read "too early" if the user creates the explicitly-not-recommended 617 // situation of a data resource and a managed resource in the same 618 // configuration both representing the same remote object. 619 620 p := testProvider("test") 621 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 622 Provider: providers.Schema{ 623 Block: &configschema.Block{}, 624 }, 625 ResourceTypes: map[string]providers.Schema{ 626 "test_resource": { 627 Block: &configschema.Block{ 628 Attributes: map[string]*configschema.Attribute{ 629 "id": { 630 Type: cty.String, 631 Computed: true, 632 }, 633 "valid": { 634 Type: cty.Bool, 635 Required: true, 636 }, 637 }, 638 }, 639 }, 640 }, 641 DataSources: map[string]providers.Schema{ 642 "test_data_source": { 643 Block: &configschema.Block{ 644 Attributes: map[string]*configschema.Attribute{ 645 "id": { 646 Type: cty.String, 647 Required: true, 648 }, 649 "valid": { 650 Type: cty.Bool, 651 Computed: true, 652 }, 653 }, 654 }, 655 }, 656 }, 657 } 658 var mu sync.Mutex 659 validVal := cty.False 660 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 661 // NOTE: This assumes that the prior state declared below will have 662 // "valid" set to false already, and thus will match validVal above. 663 resp.NewState = req.PriorState 664 return resp 665 } 666 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 667 cfg := req.Config.AsValueMap() 668 if cfg["id"].AsString() == "main" { 669 mu.Lock() 670 cfg["valid"] = validVal 671 mu.Unlock() 672 } 673 resp.State = cty.ObjectVal(cfg) 674 return resp 675 } 676 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 677 cfg := req.Config.AsValueMap() 678 prior := req.PriorState.AsValueMap() 679 resp.PlannedState = cty.ObjectVal(map[string]cty.Value{ 680 "id": prior["id"], 681 "valid": cfg["valid"], 682 }) 683 return resp 684 } 685 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 686 planned := req.PlannedState.AsValueMap() 687 688 if planned["id"].AsString() == "main" { 689 mu.Lock() 690 validVal = planned["valid"] 691 mu.Unlock() 692 } 693 694 resp.NewState = req.PlannedState 695 return resp 696 } 697 698 m := testModuleInline(t, map[string]string{ 699 "main.tf": ` 700 701 resource "test_resource" "a" { 702 valid = true 703 } 704 705 locals { 706 # NOTE: We intentionally read through a local value here because a 707 # direct reference from data.test_data_source.a to test_resource.a would 708 # cause Terraform to defer the data resource to the apply phase due to 709 # there being a pending change for the managed resource. We're explicitly 710 # testing the failure case where the data resource read happens too 711 # eagerly, which is what results from the reference being only indirect 712 # so Terraform can't "see" that the data resource result might be affected 713 # by changes to the managed resource. 714 object_id = test_resource.a.id 715 } 716 717 data "test_data_source" "a" { 718 id = local.object_id 719 } 720 721 resource "test_resource" "b" { 722 valid = true 723 724 lifecycle { 725 precondition { 726 condition = data.test_data_source.a.valid 727 error_message = "Not valid!" 728 } 729 } 730 } 731 `}) 732 733 managedAddrA := mustResourceInstanceAddr(`test_resource.a`) 734 managedAddrB := mustResourceInstanceAddr(`test_resource.b`) 735 736 // This state is intended to represent the outcome of a previous apply that 737 // failed due to postcondition failure but had already updated the 738 // relevant object to be invalid. 739 // 740 // It could also potentially represent a similar situation where the 741 // previous apply succeeded but there has been a change outside of 742 // Terraform that made it invalid, although technically in that scenario 743 // the state data would become invalid only during the planning step. For 744 // our purposes here that's close enough because we don't have a real 745 // remote system in place anyway. 746 priorState := states.BuildState(func(s *states.SyncState) { 747 s.SetResourceInstanceCurrent( 748 managedAddrA, 749 &states.ResourceInstanceObjectSrc{ 750 // NOTE: "valid" is false here but is true in the configuration 751 // above, which is intended to represent that applying the 752 // configuration change would make this object become valid. 753 AttrsJSON: []byte(`{"id":"main","valid":false}`), 754 Status: states.ObjectReady, 755 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 756 ) 757 s.SetResourceInstanceCurrent( 758 managedAddrB, 759 &states.ResourceInstanceObjectSrc{ 760 AttrsJSON: []byte(`{"id":"checker","valid":true}`), 761 Status: states.ObjectReady, 762 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 763 ) 764 }) 765 766 ctx := testContext2(t, &ContextOpts{ 767 Providers: map[addrs.Provider]providers.Factory{ 768 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 769 }, 770 }) 771 772 _, diags := ctx.Plan(m, priorState, DefaultPlanOpts) 773 if !diags.HasErrors() { 774 t.Fatalf("unexpected successful plan; should've failed with non-passing precondition") 775 } 776 777 if got, want := diags.Err().Error(), "Resource precondition failed: Not valid!"; !strings.Contains(got, want) { 778 t.Errorf("Missing expected error message\ngot: %s\nwant substring: %s", got, want) 779 } 780 } 781 782 func TestContext2Plan_destroyWithRefresh(t *testing.T) { 783 m := testModuleInline(t, map[string]string{ 784 "main.tf": ` 785 resource "test_object" "a" { 786 } 787 `, 788 }) 789 790 p := simpleMockProvider() 791 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 792 Provider: providers.Schema{Block: simpleTestSchema()}, 793 ResourceTypes: map[string]providers.Schema{ 794 "test_object": { 795 Block: &configschema.Block{ 796 Attributes: map[string]*configschema.Attribute{ 797 "arg": {Type: cty.String, Optional: true}, 798 }, 799 }, 800 }, 801 }, 802 } 803 804 // This is called from the first instance of this provider, so we can't 805 // check p.ReadResourceCalled after plan. 806 readResourceCalled := false 807 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 808 readResourceCalled = true 809 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 810 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 811 return cty.StringVal("current"), nil 812 } 813 return v, nil 814 }) 815 if err != nil { 816 // shouldn't get here 817 t.Fatalf("ReadResourceFn transform failed") 818 return providers.ReadResourceResponse{} 819 } 820 return providers.ReadResourceResponse{ 821 NewState: newVal, 822 } 823 } 824 825 upgradeResourceStateCalled := false 826 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 827 upgradeResourceStateCalled = true 828 t.Logf("UpgradeResourceState %s", req.RawStateJSON) 829 830 // In the destroy-with-refresh codepath we end up calling 831 // UpgradeResourceState twice, because we do so once during refreshing 832 // (as part making a normal plan) and then again during the plan-destroy 833 // walk. The second call recieves the result of the earlier refresh, 834 // so we need to tolerate both "before" and "current" as possible 835 // inputs here. 836 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 837 if !bytes.Contains(req.RawStateJSON, []byte("current")) { 838 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object or the 'current' object\n%s", req.RawStateJSON) 839 } 840 } 841 842 // We'll put something different in "arg" as part of upgrading, just 843 // so that we can verify below that PrevRunState contains the upgraded 844 // (but NOT refreshed) version of the object. 845 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 846 "arg": cty.StringVal("upgraded"), 847 }) 848 return resp 849 } 850 851 addr := mustResourceInstanceAddr("test_object.a") 852 state := states.BuildState(func(s *states.SyncState) { 853 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 854 AttrsJSON: []byte(`{"arg":"before"}`), 855 Status: states.ObjectReady, 856 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 857 }) 858 859 ctx := testContext2(t, &ContextOpts{ 860 Providers: map[addrs.Provider]providers.Factory{ 861 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 862 }, 863 }) 864 865 plan, diags := ctx.Plan(m, state, &PlanOpts{ 866 Mode: plans.DestroyMode, 867 SkipRefresh: false, // the default 868 }) 869 assertNoErrors(t, diags) 870 871 if !upgradeResourceStateCalled { 872 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 873 } 874 if !readResourceCalled { 875 t.Errorf("Provider's ReadResource wasn't called; should've been") 876 } 877 878 if plan.PriorState == nil { 879 t.Fatal("missing plan state") 880 } 881 882 for _, c := range plan.Changes.Resources { 883 if c.Action != plans.Delete { 884 t.Errorf("unexpected %s change for %s", c.Action, c.Addr) 885 } 886 } 887 888 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 889 t.Errorf("%s has no previous run state at all after plan", addr) 890 } else { 891 if instState.Current == nil { 892 t.Errorf("%s has no current object in the previous run state", addr) 893 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 894 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 895 } 896 } 897 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 898 t.Errorf("%s has no prior state at all after plan", addr) 899 } else { 900 if instState.Current == nil { 901 t.Errorf("%s has no current object in the prior state", addr) 902 } else if got, want := instState.Current.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 903 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 904 } 905 } 906 } 907 908 func TestContext2Plan_destroySkipRefresh(t *testing.T) { 909 m := testModuleInline(t, map[string]string{ 910 "main.tf": ` 911 resource "test_object" "a" { 912 } 913 `, 914 }) 915 916 p := simpleMockProvider() 917 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 918 Provider: providers.Schema{Block: simpleTestSchema()}, 919 ResourceTypes: map[string]providers.Schema{ 920 "test_object": { 921 Block: &configschema.Block{ 922 Attributes: map[string]*configschema.Attribute{ 923 "arg": {Type: cty.String, Optional: true}, 924 }, 925 }, 926 }, 927 }, 928 } 929 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 930 t.Helper() 931 t.Errorf("unexpected call to ReadResource") 932 resp.NewState = req.PriorState 933 return resp 934 } 935 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 936 t.Logf("UpgradeResourceState %s", req.RawStateJSON) 937 // We should've been given the prior state JSON as our input to upgrade. 938 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 939 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 940 } 941 942 // We'll put something different in "arg" as part of upgrading, just 943 // so that we can verify below that PrevRunState contains the upgraded 944 // (but NOT refreshed) version of the object. 945 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 946 "arg": cty.StringVal("upgraded"), 947 }) 948 return resp 949 } 950 951 addr := mustResourceInstanceAddr("test_object.a") 952 state := states.BuildState(func(s *states.SyncState) { 953 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 954 AttrsJSON: []byte(`{"arg":"before"}`), 955 Status: states.ObjectReady, 956 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 957 }) 958 959 ctx := testContext2(t, &ContextOpts{ 960 Providers: map[addrs.Provider]providers.Factory{ 961 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 962 }, 963 }) 964 965 plan, diags := ctx.Plan(m, state, &PlanOpts{ 966 Mode: plans.DestroyMode, 967 SkipRefresh: true, 968 }) 969 assertNoErrors(t, diags) 970 971 if !p.UpgradeResourceStateCalled { 972 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 973 } 974 if p.ReadResourceCalled { 975 t.Errorf("Provider's ReadResource was called; shouldn't have been") 976 } 977 978 if plan.PriorState == nil { 979 t.Fatal("missing plan state") 980 } 981 982 for _, c := range plan.Changes.Resources { 983 if c.Action != plans.Delete { 984 t.Errorf("unexpected %s change for %s", c.Action, c.Addr) 985 } 986 } 987 988 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 989 t.Errorf("%s has no previous run state at all after plan", addr) 990 } else { 991 if instState.Current == nil { 992 t.Errorf("%s has no current object in the previous run state", addr) 993 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 994 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 995 } 996 } 997 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 998 t.Errorf("%s has no prior state at all after plan", addr) 999 } else { 1000 if instState.Current == nil { 1001 t.Errorf("%s has no current object in the prior state", addr) 1002 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 1003 // NOTE: The prior state should still have been _upgraded_, even 1004 // though we skipped running refresh after upgrading it. 1005 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1006 } 1007 } 1008 } 1009 1010 func TestContext2Plan_unmarkingSensitiveAttributeForOutput(t *testing.T) { 1011 m := testModuleInline(t, map[string]string{ 1012 "main.tf": ` 1013 resource "test_resource" "foo" { 1014 } 1015 1016 output "result" { 1017 value = nonsensitive(test_resource.foo.sensitive_attr) 1018 } 1019 `, 1020 }) 1021 1022 p := new(MockProvider) 1023 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 1024 ResourceTypes: map[string]*configschema.Block{ 1025 "test_resource": { 1026 Attributes: map[string]*configschema.Attribute{ 1027 "id": { 1028 Type: cty.String, 1029 Computed: true, 1030 }, 1031 "sensitive_attr": { 1032 Type: cty.String, 1033 Computed: true, 1034 Sensitive: true, 1035 }, 1036 }, 1037 }, 1038 }, 1039 }) 1040 1041 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1042 return providers.PlanResourceChangeResponse{ 1043 PlannedState: cty.UnknownVal(cty.Object(map[string]cty.Type{ 1044 "id": cty.String, 1045 "sensitive_attr": cty.String, 1046 })), 1047 } 1048 } 1049 1050 state := states.NewState() 1051 1052 ctx := testContext2(t, &ContextOpts{ 1053 Providers: map[addrs.Provider]providers.Factory{ 1054 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1055 }, 1056 }) 1057 1058 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 1059 assertNoErrors(t, diags) 1060 1061 for _, res := range plan.Changes.Resources { 1062 if res.Action != plans.Create { 1063 t.Fatalf("expected create, got: %q %s", res.Addr, res.Action) 1064 } 1065 } 1066 } 1067 1068 func TestContext2Plan_destroyNoProviderConfig(t *testing.T) { 1069 // providers do not need to be configured during a destroy plan 1070 p := simpleMockProvider() 1071 p.ValidateProviderConfigFn = func(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { 1072 v := req.Config.GetAttr("test_string") 1073 if v.IsNull() || !v.IsKnown() || v.AsString() != "ok" { 1074 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("invalid provider configuration: %#v", req.Config)) 1075 } 1076 return resp 1077 } 1078 1079 m := testModuleInline(t, map[string]string{ 1080 "main.tf": ` 1081 locals { 1082 value = "ok" 1083 } 1084 1085 provider "test" { 1086 test_string = local.value 1087 } 1088 `, 1089 }) 1090 1091 addr := mustResourceInstanceAddr("test_object.a") 1092 state := states.BuildState(func(s *states.SyncState) { 1093 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 1094 AttrsJSON: []byte(`{"test_string":"foo"}`), 1095 Status: states.ObjectReady, 1096 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1097 }) 1098 1099 ctx := testContext2(t, &ContextOpts{ 1100 Providers: map[addrs.Provider]providers.Factory{ 1101 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1102 }, 1103 }) 1104 1105 _, diags := ctx.Plan(m, state, &PlanOpts{ 1106 Mode: plans.DestroyMode, 1107 }) 1108 assertNoErrors(t, diags) 1109 } 1110 1111 func TestContext2Plan_movedResourceBasic(t *testing.T) { 1112 addrA := mustResourceInstanceAddr("test_object.a") 1113 addrB := mustResourceInstanceAddr("test_object.b") 1114 m := testModuleInline(t, map[string]string{ 1115 "main.tf": ` 1116 resource "test_object" "b" { 1117 } 1118 1119 moved { 1120 from = test_object.a 1121 to = test_object.b 1122 } 1123 `, 1124 }) 1125 1126 state := states.BuildState(func(s *states.SyncState) { 1127 // The prior state tracks test_object.a, which we should treat as 1128 // test_object.b because of the "moved" block in the config. 1129 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 1130 AttrsJSON: []byte(`{}`), 1131 Status: states.ObjectReady, 1132 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1133 }) 1134 1135 p := simpleMockProvider() 1136 ctx := testContext2(t, &ContextOpts{ 1137 Providers: map[addrs.Provider]providers.Factory{ 1138 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1139 }, 1140 }) 1141 1142 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1143 Mode: plans.NormalMode, 1144 ForceReplace: []addrs.AbsResourceInstance{ 1145 addrA, 1146 }, 1147 }) 1148 if diags.HasErrors() { 1149 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1150 } 1151 1152 t.Run(addrA.String(), func(t *testing.T) { 1153 instPlan := plan.Changes.ResourceInstance(addrA) 1154 if instPlan != nil { 1155 t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) 1156 } 1157 }) 1158 t.Run(addrB.String(), func(t *testing.T) { 1159 instPlan := plan.Changes.ResourceInstance(addrB) 1160 if instPlan == nil { 1161 t.Fatalf("no plan for %s at all", addrB) 1162 } 1163 1164 if got, want := instPlan.Addr, addrB; !got.Equal(want) { 1165 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 1166 } 1167 if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { 1168 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 1169 } 1170 if got, want := instPlan.Action, plans.NoOp; got != want { 1171 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1172 } 1173 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1174 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1175 } 1176 }) 1177 } 1178 1179 func TestContext2Plan_movedResourceCollision(t *testing.T) { 1180 addrNoKey := mustResourceInstanceAddr("test_object.a") 1181 addrZeroKey := mustResourceInstanceAddr("test_object.a[0]") 1182 m := testModuleInline(t, map[string]string{ 1183 "main.tf": ` 1184 resource "test_object" "a" { 1185 # No "count" set, so test_object.a[0] will want 1186 # to implicitly move to test_object.a, but will get 1187 # blocked by the existing object at that address. 1188 } 1189 `, 1190 }) 1191 1192 state := states.BuildState(func(s *states.SyncState) { 1193 s.SetResourceInstanceCurrent(addrNoKey, &states.ResourceInstanceObjectSrc{ 1194 AttrsJSON: []byte(`{}`), 1195 Status: states.ObjectReady, 1196 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1197 s.SetResourceInstanceCurrent(addrZeroKey, &states.ResourceInstanceObjectSrc{ 1198 AttrsJSON: []byte(`{}`), 1199 Status: states.ObjectReady, 1200 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1201 }) 1202 1203 p := simpleMockProvider() 1204 ctx := testContext2(t, &ContextOpts{ 1205 Providers: map[addrs.Provider]providers.Factory{ 1206 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1207 }, 1208 }) 1209 1210 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1211 Mode: plans.NormalMode, 1212 }) 1213 if diags.HasErrors() { 1214 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1215 } 1216 1217 // We should have a warning, though! We'll lightly abuse the "for RPC" 1218 // feature of diagnostics to get some more-readily-comparable diagnostic 1219 // values. 1220 gotDiags := diags.ForRPC() 1221 wantDiags := tfdiags.Diagnostics{ 1222 tfdiags.Sourceless( 1223 tfdiags.Warning, 1224 "Unresolved resource instance address changes", 1225 `Terraform tried to adjust resource instance addresses in the prior state based on change information recorded in the configuration, but some adjustments did not succeed due to existing objects already at the intended addresses: 1226 - test_object.a[0] could not move to test_object.a 1227 1228 Terraform has planned to destroy these objects. If Terraform's proposed changes aren't appropriate, you must first resolve the conflicts using the "terraform state" subcommands and then create a new plan.`, 1229 ), 1230 }.ForRPC() 1231 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 1232 t.Errorf("wrong diagnostics\n%s", diff) 1233 } 1234 1235 t.Run(addrNoKey.String(), func(t *testing.T) { 1236 instPlan := plan.Changes.ResourceInstance(addrNoKey) 1237 if instPlan == nil { 1238 t.Fatalf("no plan for %s at all", addrNoKey) 1239 } 1240 1241 if got, want := instPlan.Addr, addrNoKey; !got.Equal(want) { 1242 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 1243 } 1244 if got, want := instPlan.PrevRunAddr, addrNoKey; !got.Equal(want) { 1245 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 1246 } 1247 if got, want := instPlan.Action, plans.NoOp; got != want { 1248 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1249 } 1250 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1251 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1252 } 1253 }) 1254 t.Run(addrZeroKey.String(), func(t *testing.T) { 1255 instPlan := plan.Changes.ResourceInstance(addrZeroKey) 1256 if instPlan == nil { 1257 t.Fatalf("no plan for %s at all", addrZeroKey) 1258 } 1259 1260 if got, want := instPlan.Addr, addrZeroKey; !got.Equal(want) { 1261 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 1262 } 1263 if got, want := instPlan.PrevRunAddr, addrZeroKey; !got.Equal(want) { 1264 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 1265 } 1266 if got, want := instPlan.Action, plans.Delete; got != want { 1267 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1268 } 1269 if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseWrongRepetition; got != want { 1270 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1271 } 1272 }) 1273 } 1274 1275 func TestContext2Plan_movedResourceCollisionDestroy(t *testing.T) { 1276 // This is like TestContext2Plan_movedResourceCollision but intended to 1277 // ensure we still produce the expected warning (and produce it only once) 1278 // when we're creating a destroy plan, rather than a normal plan. 1279 // (This case is interesting at the time of writing because we happen to 1280 // use a normal plan as a trick to refresh before creating a destroy plan. 1281 // This test will probably become uninteresting if a future change to 1282 // the destroy-time planning behavior handles refreshing in a different 1283 // way, which avoids this pre-processing step of running a normal plan 1284 // first.) 1285 1286 addrNoKey := mustResourceInstanceAddr("test_object.a") 1287 addrZeroKey := mustResourceInstanceAddr("test_object.a[0]") 1288 m := testModuleInline(t, map[string]string{ 1289 "main.tf": ` 1290 resource "test_object" "a" { 1291 # No "count" set, so test_object.a[0] will want 1292 # to implicitly move to test_object.a, but will get 1293 # blocked by the existing object at that address. 1294 } 1295 `, 1296 }) 1297 1298 state := states.BuildState(func(s *states.SyncState) { 1299 s.SetResourceInstanceCurrent(addrNoKey, &states.ResourceInstanceObjectSrc{ 1300 AttrsJSON: []byte(`{}`), 1301 Status: states.ObjectReady, 1302 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1303 s.SetResourceInstanceCurrent(addrZeroKey, &states.ResourceInstanceObjectSrc{ 1304 AttrsJSON: []byte(`{}`), 1305 Status: states.ObjectReady, 1306 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1307 }) 1308 1309 p := simpleMockProvider() 1310 ctx := testContext2(t, &ContextOpts{ 1311 Providers: map[addrs.Provider]providers.Factory{ 1312 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1313 }, 1314 }) 1315 1316 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1317 Mode: plans.DestroyMode, 1318 }) 1319 if diags.HasErrors() { 1320 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1321 } 1322 1323 // We should have a warning, though! We'll lightly abuse the "for RPC" 1324 // feature of diagnostics to get some more-readily-comparable diagnostic 1325 // values. 1326 gotDiags := diags.ForRPC() 1327 wantDiags := tfdiags.Diagnostics{ 1328 tfdiags.Sourceless( 1329 tfdiags.Warning, 1330 "Unresolved resource instance address changes", 1331 // NOTE: This message is _lightly_ confusing in the destroy case, 1332 // because it says "Terraform has planned to destroy these objects" 1333 // but this is a plan to destroy all objects, anyway. We expect the 1334 // conflict situation to be pretty rare though, and even rarer in 1335 // a "terraform destroy", so we'll just live with that for now 1336 // unless we see evidence that lots of folks are being confused by 1337 // it in practice. 1338 `Terraform tried to adjust resource instance addresses in the prior state based on change information recorded in the configuration, but some adjustments did not succeed due to existing objects already at the intended addresses: 1339 - test_object.a[0] could not move to test_object.a 1340 1341 Terraform has planned to destroy these objects. If Terraform's proposed changes aren't appropriate, you must first resolve the conflicts using the "terraform state" subcommands and then create a new plan.`, 1342 ), 1343 }.ForRPC() 1344 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 1345 // If we get here with a diff that makes it seem like the above warning 1346 // is being reported twice, the likely cause is not correctly handling 1347 // the warnings from the hidden normal plan we run as part of preparing 1348 // for a destroy plan, unless that strategy has changed in the meantime 1349 // since we originally wrote this test. 1350 t.Errorf("wrong diagnostics\n%s", diff) 1351 } 1352 1353 t.Run(addrNoKey.String(), func(t *testing.T) { 1354 instPlan := plan.Changes.ResourceInstance(addrNoKey) 1355 if instPlan == nil { 1356 t.Fatalf("no plan for %s at all", addrNoKey) 1357 } 1358 1359 if got, want := instPlan.Addr, addrNoKey; !got.Equal(want) { 1360 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 1361 } 1362 if got, want := instPlan.PrevRunAddr, addrNoKey; !got.Equal(want) { 1363 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 1364 } 1365 if got, want := instPlan.Action, plans.Delete; got != want { 1366 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1367 } 1368 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1369 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1370 } 1371 }) 1372 t.Run(addrZeroKey.String(), func(t *testing.T) { 1373 instPlan := plan.Changes.ResourceInstance(addrZeroKey) 1374 if instPlan == nil { 1375 t.Fatalf("no plan for %s at all", addrZeroKey) 1376 } 1377 1378 if got, want := instPlan.Addr, addrZeroKey; !got.Equal(want) { 1379 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 1380 } 1381 if got, want := instPlan.PrevRunAddr, addrZeroKey; !got.Equal(want) { 1382 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 1383 } 1384 if got, want := instPlan.Action, plans.Delete; got != want { 1385 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1386 } 1387 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 1388 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 1389 } 1390 }) 1391 } 1392 1393 func TestContext2Plan_movedResourceUntargeted(t *testing.T) { 1394 addrA := mustResourceInstanceAddr("test_object.a") 1395 addrB := mustResourceInstanceAddr("test_object.b") 1396 m := testModuleInline(t, map[string]string{ 1397 "main.tf": ` 1398 resource "test_object" "b" { 1399 } 1400 1401 moved { 1402 from = test_object.a 1403 to = test_object.b 1404 } 1405 `, 1406 }) 1407 1408 state := states.BuildState(func(s *states.SyncState) { 1409 // The prior state tracks test_object.a, which we should treat as 1410 // test_object.b because of the "moved" block in the config. 1411 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 1412 AttrsJSON: []byte(`{}`), 1413 Status: states.ObjectReady, 1414 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1415 }) 1416 1417 p := simpleMockProvider() 1418 ctx := testContext2(t, &ContextOpts{ 1419 Providers: map[addrs.Provider]providers.Factory{ 1420 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1421 }, 1422 }) 1423 1424 t.Run("without targeting instance A", func(t *testing.T) { 1425 _, diags := ctx.Plan(m, state, &PlanOpts{ 1426 Mode: plans.NormalMode, 1427 Targets: []addrs.Targetable{ 1428 // NOTE: addrA isn't included here, but it's pending move to addrB 1429 // and so this plan request is invalid. 1430 addrB, 1431 }, 1432 }) 1433 diags.Sort() 1434 1435 // We're semi-abusing "ForRPC" here just to get diagnostics that are 1436 // more easily comparable than the various different diagnostics types 1437 // tfdiags uses internally. The RPC-friendly diagnostics are also 1438 // comparison-friendly, by discarding all of the dynamic type information. 1439 gotDiags := diags.ForRPC() 1440 wantDiags := tfdiags.Diagnostics{ 1441 tfdiags.Sourceless( 1442 tfdiags.Warning, 1443 "Resource targeting is in effect", 1444 `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. 1445 1446 The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, 1447 ), 1448 tfdiags.Sourceless( 1449 tfdiags.Error, 1450 "Moved resource instances excluded by targeting", 1451 `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances. 1452 1453 To create a valid plan, either remove your -target=... options altogether or add the following additional target options: 1454 -target="test_object.a" 1455 1456 Note that adding these options may include further additional resource instances in your plan, in order to respect object dependencies.`, 1457 ), 1458 }.ForRPC() 1459 1460 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 1461 t.Errorf("wrong diagnostics\n%s", diff) 1462 } 1463 }) 1464 t.Run("without targeting instance B", func(t *testing.T) { 1465 _, diags := ctx.Plan(m, state, &PlanOpts{ 1466 Mode: plans.NormalMode, 1467 Targets: []addrs.Targetable{ 1468 addrA, 1469 // NOTE: addrB isn't included here, but it's pending move from 1470 // addrA and so this plan request is invalid. 1471 }, 1472 }) 1473 diags.Sort() 1474 1475 // We're semi-abusing "ForRPC" here just to get diagnostics that are 1476 // more easily comparable than the various different diagnostics types 1477 // tfdiags uses internally. The RPC-friendly diagnostics are also 1478 // comparison-friendly, by discarding all of the dynamic type information. 1479 gotDiags := diags.ForRPC() 1480 wantDiags := tfdiags.Diagnostics{ 1481 tfdiags.Sourceless( 1482 tfdiags.Warning, 1483 "Resource targeting is in effect", 1484 `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. 1485 1486 The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, 1487 ), 1488 tfdiags.Sourceless( 1489 tfdiags.Error, 1490 "Moved resource instances excluded by targeting", 1491 `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances. 1492 1493 To create a valid plan, either remove your -target=... options altogether or add the following additional target options: 1494 -target="test_object.b" 1495 1496 Note that adding these options may include further additional resource instances in your plan, in order to respect object dependencies.`, 1497 ), 1498 }.ForRPC() 1499 1500 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 1501 t.Errorf("wrong diagnostics\n%s", diff) 1502 } 1503 }) 1504 t.Run("without targeting either instance", func(t *testing.T) { 1505 _, diags := ctx.Plan(m, state, &PlanOpts{ 1506 Mode: plans.NormalMode, 1507 Targets: []addrs.Targetable{ 1508 mustResourceInstanceAddr("test_object.unrelated"), 1509 // NOTE: neither addrA nor addrB are included here, but there's 1510 // a pending move between them and so this is invalid. 1511 }, 1512 }) 1513 diags.Sort() 1514 1515 // We're semi-abusing "ForRPC" here just to get diagnostics that are 1516 // more easily comparable than the various different diagnostics types 1517 // tfdiags uses internally. The RPC-friendly diagnostics are also 1518 // comparison-friendly, by discarding all of the dynamic type information. 1519 gotDiags := diags.ForRPC() 1520 wantDiags := tfdiags.Diagnostics{ 1521 tfdiags.Sourceless( 1522 tfdiags.Warning, 1523 "Resource targeting is in effect", 1524 `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. 1525 1526 The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, 1527 ), 1528 tfdiags.Sourceless( 1529 tfdiags.Error, 1530 "Moved resource instances excluded by targeting", 1531 `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances. 1532 1533 To create a valid plan, either remove your -target=... options altogether or add the following additional target options: 1534 -target="test_object.a" 1535 -target="test_object.b" 1536 1537 Note that adding these options may include further additional resource instances in your plan, in order to respect object dependencies.`, 1538 ), 1539 }.ForRPC() 1540 1541 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 1542 t.Errorf("wrong diagnostics\n%s", diff) 1543 } 1544 }) 1545 t.Run("with both addresses in the target set", func(t *testing.T) { 1546 // The error messages in the other subtests above suggest adding 1547 // addresses to the set of targets. This additional test makes sure that 1548 // following that advice actually leads to a valid result. 1549 1550 _, diags := ctx.Plan(m, state, &PlanOpts{ 1551 Mode: plans.NormalMode, 1552 Targets: []addrs.Targetable{ 1553 // This time we're including both addresses in the target, 1554 // to get the same effect an end-user would get if following 1555 // the advice in our error message in the other subtests. 1556 addrA, 1557 addrB, 1558 }, 1559 }) 1560 diags.Sort() 1561 1562 // We're semi-abusing "ForRPC" here just to get diagnostics that are 1563 // more easily comparable than the various different diagnostics types 1564 // tfdiags uses internally. The RPC-friendly diagnostics are also 1565 // comparison-friendly, by discarding all of the dynamic type information. 1566 gotDiags := diags.ForRPC() 1567 wantDiags := tfdiags.Diagnostics{ 1568 // Still get the warning about the -target option... 1569 tfdiags.Sourceless( 1570 tfdiags.Warning, 1571 "Resource targeting is in effect", 1572 `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. 1573 1574 The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, 1575 ), 1576 // ...but now we have no error about test_object.a 1577 }.ForRPC() 1578 1579 if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { 1580 t.Errorf("wrong diagnostics\n%s", diff) 1581 } 1582 }) 1583 } 1584 1585 func TestContext2Plan_movedResourceRefreshOnly(t *testing.T) { 1586 addrA := mustResourceInstanceAddr("test_object.a") 1587 addrB := mustResourceInstanceAddr("test_object.b") 1588 m := testModuleInline(t, map[string]string{ 1589 "main.tf": ` 1590 resource "test_object" "b" { 1591 } 1592 1593 moved { 1594 from = test_object.a 1595 to = test_object.b 1596 } 1597 `, 1598 }) 1599 1600 state := states.BuildState(func(s *states.SyncState) { 1601 // The prior state tracks test_object.a, which we should treat as 1602 // test_object.b because of the "moved" block in the config. 1603 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 1604 AttrsJSON: []byte(`{}`), 1605 Status: states.ObjectReady, 1606 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1607 }) 1608 1609 p := simpleMockProvider() 1610 ctx := testContext2(t, &ContextOpts{ 1611 Providers: map[addrs.Provider]providers.Factory{ 1612 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1613 }, 1614 }) 1615 1616 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1617 Mode: plans.RefreshOnlyMode, 1618 }) 1619 if diags.HasErrors() { 1620 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1621 } 1622 1623 t.Run(addrA.String(), func(t *testing.T) { 1624 instPlan := plan.Changes.ResourceInstance(addrA) 1625 if instPlan != nil { 1626 t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) 1627 } 1628 }) 1629 t.Run(addrB.String(), func(t *testing.T) { 1630 instPlan := plan.Changes.ResourceInstance(addrB) 1631 if instPlan != nil { 1632 t.Fatalf("unexpected plan for %s", addrB) 1633 } 1634 }) 1635 t.Run("drift", func(t *testing.T) { 1636 var drifted *plans.ResourceInstanceChangeSrc 1637 for _, dr := range plan.DriftedResources { 1638 if dr.Addr.Equal(addrB) { 1639 drifted = dr 1640 break 1641 } 1642 } 1643 1644 if drifted == nil { 1645 t.Fatalf("instance %s is missing from the drifted resource changes", addrB) 1646 } 1647 1648 if got, want := drifted.PrevRunAddr, addrA; !got.Equal(want) { 1649 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 1650 } 1651 if got, want := drifted.Action, plans.NoOp; got != want { 1652 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 1653 } 1654 }) 1655 } 1656 1657 func TestContext2Plan_refreshOnlyMode(t *testing.T) { 1658 addr := mustResourceInstanceAddr("test_object.a") 1659 1660 // The configuration, the prior state, and the refresh result intentionally 1661 // have different values for "test_string" so we can observe that the 1662 // refresh took effect but the configuration change wasn't considered. 1663 m := testModuleInline(t, map[string]string{ 1664 "main.tf": ` 1665 resource "test_object" "a" { 1666 arg = "after" 1667 } 1668 1669 output "out" { 1670 value = test_object.a.arg 1671 } 1672 `, 1673 }) 1674 state := states.BuildState(func(s *states.SyncState) { 1675 s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ 1676 AttrsJSON: []byte(`{"arg":"before"}`), 1677 Status: states.ObjectReady, 1678 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1679 }) 1680 1681 p := simpleMockProvider() 1682 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1683 Provider: providers.Schema{Block: simpleTestSchema()}, 1684 ResourceTypes: map[string]providers.Schema{ 1685 "test_object": { 1686 Block: &configschema.Block{ 1687 Attributes: map[string]*configschema.Attribute{ 1688 "arg": {Type: cty.String, Optional: true}, 1689 }, 1690 }, 1691 }, 1692 }, 1693 } 1694 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 1695 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 1696 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 1697 return cty.StringVal("current"), nil 1698 } 1699 return v, nil 1700 }) 1701 if err != nil { 1702 // shouldn't get here 1703 t.Fatalf("ReadResourceFn transform failed") 1704 return providers.ReadResourceResponse{} 1705 } 1706 return providers.ReadResourceResponse{ 1707 NewState: newVal, 1708 } 1709 } 1710 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 1711 // We should've been given the prior state JSON as our input to upgrade. 1712 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 1713 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 1714 } 1715 1716 // We'll put something different in "arg" as part of upgrading, just 1717 // so that we can verify below that PrevRunState contains the upgraded 1718 // (but NOT refreshed) version of the object. 1719 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 1720 "arg": cty.StringVal("upgraded"), 1721 }) 1722 return resp 1723 } 1724 1725 ctx := testContext2(t, &ContextOpts{ 1726 Providers: map[addrs.Provider]providers.Factory{ 1727 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1728 }, 1729 }) 1730 1731 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1732 Mode: plans.RefreshOnlyMode, 1733 }) 1734 if diags.HasErrors() { 1735 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1736 } 1737 1738 if !p.UpgradeResourceStateCalled { 1739 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 1740 } 1741 if !p.ReadResourceCalled { 1742 t.Errorf("Provider's ReadResource wasn't called; should've been") 1743 } 1744 1745 if got, want := len(plan.Changes.Resources), 0; got != want { 1746 t.Errorf("plan contains resource changes; want none\n%s", spew.Sdump(plan.Changes.Resources)) 1747 } 1748 1749 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 1750 t.Errorf("%s has no prior state at all after plan", addr) 1751 } else { 1752 if instState.Current == nil { 1753 t.Errorf("%s has no current object after plan", addr) 1754 } else if got, want := instState.Current.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 1755 // Should've saved the result of refreshing 1756 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1757 } 1758 } 1759 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 1760 t.Errorf("%s has no previous run state at all after plan", addr) 1761 } else { 1762 if instState.Current == nil { 1763 t.Errorf("%s has no current object in the previous run state", addr) 1764 } else if got, want := instState.Current.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 1765 // Should've saved the result of upgrading 1766 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1767 } 1768 } 1769 1770 // The output value should also have updated. If not, it's likely that we 1771 // skipped updating the working state to match the refreshed state when we 1772 // were evaluating the resource. 1773 if outChangeSrc := plan.Changes.OutputValue(addrs.RootModuleInstance.OutputValue("out")); outChangeSrc == nil { 1774 t.Errorf("no change planned for output value 'out'") 1775 } else { 1776 outChange, err := outChangeSrc.Decode() 1777 if err != nil { 1778 t.Fatalf("failed to decode output value 'out': %s", err) 1779 } 1780 got := outChange.After 1781 want := cty.StringVal("current") 1782 if !want.RawEquals(got) { 1783 t.Errorf("wrong value for output value 'out'\ngot: %#v\nwant: %#v", got, want) 1784 } 1785 } 1786 } 1787 1788 func TestContext2Plan_refreshOnlyMode_deposed(t *testing.T) { 1789 addr := mustResourceInstanceAddr("test_object.a") 1790 deposedKey := states.DeposedKey("byebye") 1791 1792 // The configuration, the prior state, and the refresh result intentionally 1793 // have different values for "test_string" so we can observe that the 1794 // refresh took effect but the configuration change wasn't considered. 1795 m := testModuleInline(t, map[string]string{ 1796 "main.tf": ` 1797 resource "test_object" "a" { 1798 arg = "after" 1799 } 1800 1801 output "out" { 1802 value = test_object.a.arg 1803 } 1804 `, 1805 }) 1806 state := states.BuildState(func(s *states.SyncState) { 1807 // Note that we're intentionally recording a _deposed_ object here, 1808 // and not including a current object, so a normal (non-refresh) 1809 // plan would normally plan to create a new object _and_ destroy 1810 // the deposed one, but refresh-only mode should prevent that. 1811 s.SetResourceInstanceDeposed(addr, deposedKey, &states.ResourceInstanceObjectSrc{ 1812 AttrsJSON: []byte(`{"arg":"before"}`), 1813 Status: states.ObjectReady, 1814 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1815 }) 1816 1817 p := simpleMockProvider() 1818 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1819 Provider: providers.Schema{Block: simpleTestSchema()}, 1820 ResourceTypes: map[string]providers.Schema{ 1821 "test_object": { 1822 Block: &configschema.Block{ 1823 Attributes: map[string]*configschema.Attribute{ 1824 "arg": {Type: cty.String, Optional: true}, 1825 }, 1826 }, 1827 }, 1828 }, 1829 } 1830 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 1831 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 1832 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 1833 return cty.StringVal("current"), nil 1834 } 1835 return v, nil 1836 }) 1837 if err != nil { 1838 // shouldn't get here 1839 t.Fatalf("ReadResourceFn transform failed") 1840 return providers.ReadResourceResponse{} 1841 } 1842 return providers.ReadResourceResponse{ 1843 NewState: newVal, 1844 } 1845 } 1846 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 1847 // We should've been given the prior state JSON as our input to upgrade. 1848 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 1849 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 1850 } 1851 1852 // We'll put something different in "arg" as part of upgrading, just 1853 // so that we can verify below that PrevRunState contains the upgraded 1854 // (but NOT refreshed) version of the object. 1855 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 1856 "arg": cty.StringVal("upgraded"), 1857 }) 1858 return resp 1859 } 1860 1861 ctx := testContext2(t, &ContextOpts{ 1862 Providers: map[addrs.Provider]providers.Factory{ 1863 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1864 }, 1865 }) 1866 1867 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1868 Mode: plans.RefreshOnlyMode, 1869 }) 1870 if diags.HasErrors() { 1871 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 1872 } 1873 1874 if !p.UpgradeResourceStateCalled { 1875 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 1876 } 1877 if !p.ReadResourceCalled { 1878 t.Errorf("Provider's ReadResource wasn't called; should've been") 1879 } 1880 1881 if got, want := len(plan.Changes.Resources), 0; got != want { 1882 t.Errorf("plan contains resource changes; want none\n%s", spew.Sdump(plan.Changes.Resources)) 1883 } 1884 1885 if instState := plan.PriorState.ResourceInstance(addr); instState == nil { 1886 t.Errorf("%s has no prior state at all after plan", addr) 1887 } else { 1888 if obj := instState.Deposed[deposedKey]; obj == nil { 1889 t.Errorf("%s has no deposed object after plan", addr) 1890 } else if got, want := obj.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 1891 // Should've saved the result of refreshing 1892 t.Errorf("%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1893 } 1894 } 1895 if instState := plan.PrevRunState.ResourceInstance(addr); instState == nil { 1896 t.Errorf("%s has no previous run state at all after plan", addr) 1897 } else { 1898 if obj := instState.Deposed[deposedKey]; obj == nil { 1899 t.Errorf("%s has no deposed object in the previous run state", addr) 1900 } else if got, want := obj.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 1901 // Should've saved the result of upgrading 1902 t.Errorf("%s has wrong previous run state after plan\ngot:\n%s\n\nwant substring: %s", addr, got, want) 1903 } 1904 } 1905 1906 // The output value should also have updated. If not, it's likely that we 1907 // skipped updating the working state to match the refreshed state when we 1908 // were evaluating the resource. 1909 if outChangeSrc := plan.Changes.OutputValue(addrs.RootModuleInstance.OutputValue("out")); outChangeSrc == nil { 1910 t.Errorf("no change planned for output value 'out'") 1911 } else { 1912 outChange, err := outChangeSrc.Decode() 1913 if err != nil { 1914 t.Fatalf("failed to decode output value 'out': %s", err) 1915 } 1916 got := outChange.After 1917 want := cty.UnknownVal(cty.String) 1918 if !want.RawEquals(got) { 1919 t.Errorf("wrong value for output value 'out'\ngot: %#v\nwant: %#v", got, want) 1920 } 1921 } 1922 1923 // Deposed objects should not be represented in drift. 1924 if len(plan.DriftedResources) > 0 { 1925 t.Errorf("unexpected drifted resources (%d)", len(plan.DriftedResources)) 1926 } 1927 } 1928 1929 func TestContext2Plan_refreshOnlyMode_orphan(t *testing.T) { 1930 addr := mustAbsResourceAddr("test_object.a") 1931 1932 // The configuration, the prior state, and the refresh result intentionally 1933 // have different values for "test_string" so we can observe that the 1934 // refresh took effect but the configuration change wasn't considered. 1935 m := testModuleInline(t, map[string]string{ 1936 "main.tf": ` 1937 resource "test_object" "a" { 1938 arg = "after" 1939 count = 1 1940 } 1941 1942 output "out" { 1943 value = test_object.a.*.arg 1944 } 1945 `, 1946 }) 1947 state := states.BuildState(func(s *states.SyncState) { 1948 s.SetResourceInstanceCurrent(addr.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{ 1949 AttrsJSON: []byte(`{"arg":"before"}`), 1950 Status: states.ObjectReady, 1951 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1952 s.SetResourceInstanceCurrent(addr.Instance(addrs.IntKey(1)), &states.ResourceInstanceObjectSrc{ 1953 AttrsJSON: []byte(`{"arg":"before"}`), 1954 Status: states.ObjectReady, 1955 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 1956 }) 1957 1958 p := simpleMockProvider() 1959 p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ 1960 Provider: providers.Schema{Block: simpleTestSchema()}, 1961 ResourceTypes: map[string]providers.Schema{ 1962 "test_object": { 1963 Block: &configschema.Block{ 1964 Attributes: map[string]*configschema.Attribute{ 1965 "arg": {Type: cty.String, Optional: true}, 1966 }, 1967 }, 1968 }, 1969 }, 1970 } 1971 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 1972 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 1973 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "arg"}) { 1974 return cty.StringVal("current"), nil 1975 } 1976 return v, nil 1977 }) 1978 if err != nil { 1979 // shouldn't get here 1980 t.Fatalf("ReadResourceFn transform failed") 1981 return providers.ReadResourceResponse{} 1982 } 1983 return providers.ReadResourceResponse{ 1984 NewState: newVal, 1985 } 1986 } 1987 p.UpgradeResourceStateFn = func(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { 1988 // We should've been given the prior state JSON as our input to upgrade. 1989 if !bytes.Contains(req.RawStateJSON, []byte("before")) { 1990 t.Fatalf("UpgradeResourceState request doesn't contain the 'before' object\n%s", req.RawStateJSON) 1991 } 1992 1993 // We'll put something different in "arg" as part of upgrading, just 1994 // so that we can verify below that PrevRunState contains the upgraded 1995 // (but NOT refreshed) version of the object. 1996 resp.UpgradedState = cty.ObjectVal(map[string]cty.Value{ 1997 "arg": cty.StringVal("upgraded"), 1998 }) 1999 return resp 2000 } 2001 2002 ctx := testContext2(t, &ContextOpts{ 2003 Providers: map[addrs.Provider]providers.Factory{ 2004 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2005 }, 2006 }) 2007 2008 plan, diags := ctx.Plan(m, state, &PlanOpts{ 2009 Mode: plans.RefreshOnlyMode, 2010 }) 2011 if diags.HasErrors() { 2012 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 2013 } 2014 2015 if !p.UpgradeResourceStateCalled { 2016 t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") 2017 } 2018 if !p.ReadResourceCalled { 2019 t.Errorf("Provider's ReadResource wasn't called; should've been") 2020 } 2021 2022 if got, want := len(plan.Changes.Resources), 0; got != want { 2023 t.Errorf("plan contains resource changes; want none\n%s", spew.Sdump(plan.Changes.Resources)) 2024 } 2025 2026 if rState := plan.PriorState.Resource(addr); rState == nil { 2027 t.Errorf("%s has no prior state at all after plan", addr) 2028 } else { 2029 for i := 0; i < 2; i++ { 2030 instKey := addrs.IntKey(i) 2031 if obj := rState.Instance(instKey).Current; obj == nil { 2032 t.Errorf("%s%s has no object after plan", addr, instKey) 2033 } else if got, want := obj.AttrsJSON, `"current"`; !bytes.Contains(got, []byte(want)) { 2034 // Should've saved the result of refreshing 2035 t.Errorf("%s%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, instKey, got, want) 2036 } 2037 } 2038 } 2039 if rState := plan.PrevRunState.Resource(addr); rState == nil { 2040 t.Errorf("%s has no prior state at all after plan", addr) 2041 } else { 2042 for i := 0; i < 2; i++ { 2043 instKey := addrs.IntKey(i) 2044 if obj := rState.Instance(instKey).Current; obj == nil { 2045 t.Errorf("%s%s has no object after plan", addr, instKey) 2046 } else if got, want := obj.AttrsJSON, `"upgraded"`; !bytes.Contains(got, []byte(want)) { 2047 // Should've saved the result of upgrading 2048 t.Errorf("%s%s has wrong prior state after plan\ngot:\n%s\n\nwant substring: %s", addr, instKey, got, want) 2049 } 2050 } 2051 } 2052 2053 // The output value should also have updated. If not, it's likely that we 2054 // skipped updating the working state to match the refreshed state when we 2055 // were evaluating the resource. 2056 if outChangeSrc := plan.Changes.OutputValue(addrs.RootModuleInstance.OutputValue("out")); outChangeSrc == nil { 2057 t.Errorf("no change planned for output value 'out'") 2058 } else { 2059 outChange, err := outChangeSrc.Decode() 2060 if err != nil { 2061 t.Fatalf("failed to decode output value 'out': %s", err) 2062 } 2063 got := outChange.After 2064 want := cty.TupleVal([]cty.Value{cty.StringVal("current"), cty.StringVal("current")}) 2065 if !want.RawEquals(got) { 2066 t.Errorf("wrong value for output value 'out'\ngot: %#v\nwant: %#v", got, want) 2067 } 2068 } 2069 } 2070 2071 func TestContext2Plan_invalidSensitiveModuleOutput(t *testing.T) { 2072 m := testModuleInline(t, map[string]string{ 2073 "child/main.tf": ` 2074 output "out" { 2075 value = sensitive("xyz") 2076 }`, 2077 "main.tf": ` 2078 module "child" { 2079 source = "./child" 2080 } 2081 2082 output "root" { 2083 value = module.child.out 2084 }`, 2085 }) 2086 2087 ctx := testContext2(t, &ContextOpts{}) 2088 2089 _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 2090 if !diags.HasErrors() { 2091 t.Fatal("succeeded; want errors") 2092 } 2093 if got, want := diags.Err().Error(), "Output refers to sensitive values"; !strings.Contains(got, want) { 2094 t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) 2095 } 2096 } 2097 2098 func TestContext2Plan_planDataSourceSensitiveNested(t *testing.T) { 2099 m := testModuleInline(t, map[string]string{ 2100 "main.tf": ` 2101 resource "test_instance" "bar" { 2102 } 2103 2104 data "test_data_source" "foo" { 2105 foo { 2106 bar = test_instance.bar.sensitive 2107 } 2108 } 2109 `, 2110 }) 2111 2112 p := new(MockProvider) 2113 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 2114 resp.PlannedState = cty.ObjectVal(map[string]cty.Value{ 2115 "sensitive": cty.UnknownVal(cty.String), 2116 }) 2117 return resp 2118 } 2119 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 2120 ResourceTypes: map[string]*configschema.Block{ 2121 "test_instance": { 2122 Attributes: map[string]*configschema.Attribute{ 2123 "sensitive": { 2124 Type: cty.String, 2125 Computed: true, 2126 Sensitive: true, 2127 }, 2128 }, 2129 }, 2130 }, 2131 DataSources: map[string]*configschema.Block{ 2132 "test_data_source": { 2133 Attributes: map[string]*configschema.Attribute{ 2134 "id": { 2135 Type: cty.String, 2136 Computed: true, 2137 }, 2138 }, 2139 BlockTypes: map[string]*configschema.NestedBlock{ 2140 "foo": { 2141 Block: configschema.Block{ 2142 Attributes: map[string]*configschema.Attribute{ 2143 "bar": {Type: cty.String, Optional: true}, 2144 }, 2145 }, 2146 Nesting: configschema.NestingSet, 2147 }, 2148 }, 2149 }, 2150 }, 2151 }) 2152 2153 state := states.NewState() 2154 root := state.EnsureModule(addrs.RootModuleInstance) 2155 root.SetResourceInstanceCurrent( 2156 mustResourceInstanceAddr("data.test_data_source.foo").Resource, 2157 &states.ResourceInstanceObjectSrc{ 2158 Status: states.ObjectReady, 2159 AttrsJSON: []byte(`{"string":"data_id", "foo":[{"bar":"old"}]}`), 2160 AttrSensitivePaths: []cty.PathValueMarks{ 2161 { 2162 Path: cty.GetAttrPath("foo"), 2163 Marks: cty.NewValueMarks(marks.Sensitive), 2164 }, 2165 }, 2166 }, 2167 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 2168 ) 2169 root.SetResourceInstanceCurrent( 2170 mustResourceInstanceAddr("test_instance.bar").Resource, 2171 &states.ResourceInstanceObjectSrc{ 2172 Status: states.ObjectReady, 2173 AttrsJSON: []byte(`{"sensitive":"old"}`), 2174 AttrSensitivePaths: []cty.PathValueMarks{ 2175 { 2176 Path: cty.GetAttrPath("sensitive"), 2177 Marks: cty.NewValueMarks(marks.Sensitive), 2178 }, 2179 }, 2180 }, 2181 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 2182 ) 2183 2184 ctx := testContext2(t, &ContextOpts{ 2185 Providers: map[addrs.Provider]providers.Factory{ 2186 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2187 }, 2188 }) 2189 2190 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 2191 assertNoErrors(t, diags) 2192 2193 for _, res := range plan.Changes.Resources { 2194 switch res.Addr.String() { 2195 case "test_instance.bar": 2196 if res.Action != plans.Update { 2197 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2198 } 2199 case "data.test_data_source.foo": 2200 if res.Action != plans.Read { 2201 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2202 } 2203 default: 2204 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2205 } 2206 } 2207 } 2208 2209 func TestContext2Plan_forceReplace(t *testing.T) { 2210 addrA := mustResourceInstanceAddr("test_object.a") 2211 addrB := mustResourceInstanceAddr("test_object.b") 2212 m := testModuleInline(t, map[string]string{ 2213 "main.tf": ` 2214 resource "test_object" "a" { 2215 } 2216 resource "test_object" "b" { 2217 } 2218 `, 2219 }) 2220 2221 state := states.BuildState(func(s *states.SyncState) { 2222 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 2223 AttrsJSON: []byte(`{}`), 2224 Status: states.ObjectReady, 2225 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 2226 s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{ 2227 AttrsJSON: []byte(`{}`), 2228 Status: states.ObjectReady, 2229 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 2230 }) 2231 2232 p := simpleMockProvider() 2233 ctx := testContext2(t, &ContextOpts{ 2234 Providers: map[addrs.Provider]providers.Factory{ 2235 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2236 }, 2237 }) 2238 2239 plan, diags := ctx.Plan(m, state, &PlanOpts{ 2240 Mode: plans.NormalMode, 2241 ForceReplace: []addrs.AbsResourceInstance{ 2242 addrA, 2243 }, 2244 }) 2245 if diags.HasErrors() { 2246 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 2247 } 2248 2249 t.Run(addrA.String(), func(t *testing.T) { 2250 instPlan := plan.Changes.ResourceInstance(addrA) 2251 if instPlan == nil { 2252 t.Fatalf("no plan for %s at all", addrA) 2253 } 2254 2255 if got, want := instPlan.Action, plans.DeleteThenCreate; got != want { 2256 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 2257 } 2258 if got, want := instPlan.ActionReason, plans.ResourceInstanceReplaceByRequest; got != want { 2259 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 2260 } 2261 }) 2262 t.Run(addrB.String(), func(t *testing.T) { 2263 instPlan := plan.Changes.ResourceInstance(addrB) 2264 if instPlan == nil { 2265 t.Fatalf("no plan for %s at all", addrB) 2266 } 2267 2268 if got, want := instPlan.Action, plans.NoOp; got != want { 2269 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 2270 } 2271 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 2272 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 2273 } 2274 }) 2275 } 2276 2277 func TestContext2Plan_forceReplaceIncompleteAddr(t *testing.T) { 2278 addr0 := mustResourceInstanceAddr("test_object.a[0]") 2279 addr1 := mustResourceInstanceAddr("test_object.a[1]") 2280 addrBare := mustResourceInstanceAddr("test_object.a") 2281 m := testModuleInline(t, map[string]string{ 2282 "main.tf": ` 2283 resource "test_object" "a" { 2284 count = 2 2285 } 2286 `, 2287 }) 2288 2289 state := states.BuildState(func(s *states.SyncState) { 2290 s.SetResourceInstanceCurrent(addr0, &states.ResourceInstanceObjectSrc{ 2291 AttrsJSON: []byte(`{}`), 2292 Status: states.ObjectReady, 2293 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 2294 s.SetResourceInstanceCurrent(addr1, &states.ResourceInstanceObjectSrc{ 2295 AttrsJSON: []byte(`{}`), 2296 Status: states.ObjectReady, 2297 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 2298 }) 2299 2300 p := simpleMockProvider() 2301 ctx := testContext2(t, &ContextOpts{ 2302 Providers: map[addrs.Provider]providers.Factory{ 2303 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2304 }, 2305 }) 2306 2307 plan, diags := ctx.Plan(m, state, &PlanOpts{ 2308 Mode: plans.NormalMode, 2309 ForceReplace: []addrs.AbsResourceInstance{ 2310 addrBare, 2311 }, 2312 }) 2313 if diags.HasErrors() { 2314 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 2315 } 2316 diagsErr := diags.ErrWithWarnings() 2317 if diagsErr == nil { 2318 t.Fatalf("no warnings were returned") 2319 } 2320 if got, want := diagsErr.Error(), "Incompletely-matched force-replace resource instance"; !strings.Contains(got, want) { 2321 t.Errorf("missing expected warning\ngot:\n%s\n\nwant substring: %s", got, want) 2322 } 2323 2324 t.Run(addr0.String(), func(t *testing.T) { 2325 instPlan := plan.Changes.ResourceInstance(addr0) 2326 if instPlan == nil { 2327 t.Fatalf("no plan for %s at all", addr0) 2328 } 2329 2330 if got, want := instPlan.Action, plans.NoOp; got != want { 2331 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 2332 } 2333 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 2334 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 2335 } 2336 }) 2337 t.Run(addr1.String(), func(t *testing.T) { 2338 instPlan := plan.Changes.ResourceInstance(addr1) 2339 if instPlan == nil { 2340 t.Fatalf("no plan for %s at all", addr1) 2341 } 2342 2343 if got, want := instPlan.Action, plans.NoOp; got != want { 2344 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 2345 } 2346 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 2347 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 2348 } 2349 }) 2350 } 2351 2352 // Verify that adding a module instance does force existing module data sources 2353 // to be deferred 2354 func TestContext2Plan_noChangeDataSourceAddingModuleInstance(t *testing.T) { 2355 m := testModuleInline(t, map[string]string{ 2356 "main.tf": ` 2357 locals { 2358 data = { 2359 a = "a" 2360 b = "b" 2361 } 2362 } 2363 2364 module "one" { 2365 source = "./mod" 2366 for_each = local.data 2367 input = each.value 2368 } 2369 2370 module "two" { 2371 source = "./mod" 2372 for_each = module.one 2373 input = each.value.output 2374 } 2375 `, 2376 "mod/main.tf": ` 2377 variable "input" { 2378 } 2379 2380 resource "test_resource" "x" { 2381 value = var.input 2382 } 2383 2384 data "test_data_source" "d" { 2385 foo = test_resource.x.id 2386 } 2387 2388 output "output" { 2389 value = test_resource.x.id 2390 } 2391 `, 2392 }) 2393 2394 p := testProvider("test") 2395 p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ 2396 State: cty.ObjectVal(map[string]cty.Value{ 2397 "id": cty.StringVal("data"), 2398 "foo": cty.StringVal("foo"), 2399 }), 2400 } 2401 state := states.NewState() 2402 modOne := addrs.RootModuleInstance.Child("one", addrs.StringKey("a")) 2403 modTwo := addrs.RootModuleInstance.Child("two", addrs.StringKey("a")) 2404 one := state.EnsureModule(modOne) 2405 two := state.EnsureModule(modTwo) 2406 one.SetResourceInstanceCurrent( 2407 mustResourceInstanceAddr(`test_resource.x`).Resource, 2408 &states.ResourceInstanceObjectSrc{ 2409 Status: states.ObjectReady, 2410 AttrsJSON: []byte(`{"id":"foo","value":"a"}`), 2411 }, 2412 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 2413 ) 2414 one.SetResourceInstanceCurrent( 2415 mustResourceInstanceAddr(`data.test_data_source.d`).Resource, 2416 &states.ResourceInstanceObjectSrc{ 2417 Status: states.ObjectReady, 2418 AttrsJSON: []byte(`{"id":"data"}`), 2419 }, 2420 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 2421 ) 2422 two.SetResourceInstanceCurrent( 2423 mustResourceInstanceAddr(`test_resource.x`).Resource, 2424 &states.ResourceInstanceObjectSrc{ 2425 Status: states.ObjectReady, 2426 AttrsJSON: []byte(`{"id":"foo","value":"foo"}`), 2427 }, 2428 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 2429 ) 2430 two.SetResourceInstanceCurrent( 2431 mustResourceInstanceAddr(`data.test_data_source.d`).Resource, 2432 &states.ResourceInstanceObjectSrc{ 2433 Status: states.ObjectReady, 2434 AttrsJSON: []byte(`{"id":"data"}`), 2435 }, 2436 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 2437 ) 2438 2439 ctx := testContext2(t, &ContextOpts{ 2440 Providers: map[addrs.Provider]providers.Factory{ 2441 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2442 }, 2443 }) 2444 2445 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 2446 assertNoErrors(t, diags) 2447 2448 for _, res := range plan.Changes.Resources { 2449 // both existing data sources should be read during plan 2450 if res.Addr.Module[0].InstanceKey == addrs.StringKey("b") { 2451 continue 2452 } 2453 2454 if res.Addr.Resource.Resource.Mode == addrs.DataResourceMode && res.Action != plans.NoOp { 2455 t.Errorf("unexpected %s plan for %s", res.Action, res.Addr) 2456 } 2457 } 2458 } 2459 2460 func TestContext2Plan_moduleExpandOrphansResourceInstance(t *testing.T) { 2461 // This test deals with the situation where a user has changed the 2462 // repetition/expansion mode for a module call while there are already 2463 // resource instances from the previous declaration in the state. 2464 // 2465 // This is conceptually just the same as removing the resources 2466 // from the module configuration only for that instance, but the 2467 // implementation of it ends up a little different because it's 2468 // an entry in the resource address's _module path_ that we'll find 2469 // missing, rather than the resource's own instance key, and so 2470 // our analyses need to handle that situation by indicating that all 2471 // of the resources under the missing module instance have zero 2472 // instances, regardless of which resource in that module we might 2473 // be asking about, and do so without tripping over any missing 2474 // registrations in the instance expander that might lead to panics 2475 // if we aren't careful. 2476 // 2477 // (For some history here, see https://github.com/hashicorp/terraform/issues/30110 ) 2478 2479 addrNoKey := mustResourceInstanceAddr("module.child.test_object.a[0]") 2480 addrZeroKey := mustResourceInstanceAddr("module.child[0].test_object.a[0]") 2481 m := testModuleInline(t, map[string]string{ 2482 "main.tf": ` 2483 module "child" { 2484 source = "./child" 2485 count = 1 2486 } 2487 `, 2488 "child/main.tf": ` 2489 resource "test_object" "a" { 2490 count = 1 2491 } 2492 `, 2493 }) 2494 2495 state := states.BuildState(func(s *states.SyncState) { 2496 // Notice that addrNoKey is the address which lacks any instance key 2497 // for module.child, and so that module instance doesn't match the 2498 // call declared above with count = 1, and therefore the resource 2499 // inside is "orphaned" even though the resource block actually 2500 // still exists there. 2501 s.SetResourceInstanceCurrent(addrNoKey, &states.ResourceInstanceObjectSrc{ 2502 AttrsJSON: []byte(`{}`), 2503 Status: states.ObjectReady, 2504 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 2505 }) 2506 2507 p := simpleMockProvider() 2508 ctx := testContext2(t, &ContextOpts{ 2509 Providers: map[addrs.Provider]providers.Factory{ 2510 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2511 }, 2512 }) 2513 2514 plan, diags := ctx.Plan(m, state, &PlanOpts{ 2515 Mode: plans.NormalMode, 2516 }) 2517 if diags.HasErrors() { 2518 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 2519 } 2520 2521 t.Run(addrNoKey.String(), func(t *testing.T) { 2522 instPlan := plan.Changes.ResourceInstance(addrNoKey) 2523 if instPlan == nil { 2524 t.Fatalf("no plan for %s at all", addrNoKey) 2525 } 2526 2527 if got, want := instPlan.Addr, addrNoKey; !got.Equal(want) { 2528 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 2529 } 2530 if got, want := instPlan.PrevRunAddr, addrNoKey; !got.Equal(want) { 2531 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 2532 } 2533 if got, want := instPlan.Action, plans.Delete; got != want { 2534 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 2535 } 2536 if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoModule; got != want { 2537 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 2538 } 2539 }) 2540 2541 t.Run(addrZeroKey.String(), func(t *testing.T) { 2542 instPlan := plan.Changes.ResourceInstance(addrZeroKey) 2543 if instPlan == nil { 2544 t.Fatalf("no plan for %s at all", addrZeroKey) 2545 } 2546 2547 if got, want := instPlan.Addr, addrZeroKey; !got.Equal(want) { 2548 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 2549 } 2550 if got, want := instPlan.PrevRunAddr, addrZeroKey; !got.Equal(want) { 2551 t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) 2552 } 2553 if got, want := instPlan.Action, plans.Create; got != want { 2554 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 2555 } 2556 if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { 2557 t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) 2558 } 2559 }) 2560 } 2561 2562 func TestContext2Plan_resourcePreconditionPostcondition(t *testing.T) { 2563 m := testModuleInline(t, map[string]string{ 2564 "main.tf": ` 2565 variable "boop" { 2566 type = string 2567 } 2568 2569 resource "test_resource" "a" { 2570 value = var.boop 2571 lifecycle { 2572 precondition { 2573 condition = var.boop == "boop" 2574 error_message = "Wrong boop." 2575 } 2576 postcondition { 2577 condition = self.output != "" 2578 error_message = "Output must not be blank." 2579 } 2580 } 2581 } 2582 2583 `, 2584 }) 2585 2586 p := testProvider("test") 2587 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 2588 ResourceTypes: map[string]*configschema.Block{ 2589 "test_resource": { 2590 Attributes: map[string]*configschema.Attribute{ 2591 "value": { 2592 Type: cty.String, 2593 Required: true, 2594 }, 2595 "output": { 2596 Type: cty.String, 2597 Computed: true, 2598 }, 2599 }, 2600 }, 2601 }, 2602 }) 2603 2604 ctx := testContext2(t, &ContextOpts{ 2605 Providers: map[addrs.Provider]providers.Factory{ 2606 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2607 }, 2608 }) 2609 2610 t.Run("conditions pass", func(t *testing.T) { 2611 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 2612 m := req.ProposedNewState.AsValueMap() 2613 m["output"] = cty.StringVal("bar") 2614 2615 resp.PlannedState = cty.ObjectVal(m) 2616 resp.LegacyTypeSystem = true 2617 return resp 2618 } 2619 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2620 Mode: plans.NormalMode, 2621 SetVariables: InputValues{ 2622 "boop": &InputValue{ 2623 Value: cty.StringVal("boop"), 2624 SourceType: ValueFromCLIArg, 2625 }, 2626 }, 2627 }) 2628 assertNoErrors(t, diags) 2629 for _, res := range plan.Changes.Resources { 2630 switch res.Addr.String() { 2631 case "test_resource.a": 2632 if res.Action != plans.Create { 2633 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2634 } 2635 default: 2636 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2637 } 2638 } 2639 }) 2640 2641 t.Run("precondition fail", func(t *testing.T) { 2642 _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2643 Mode: plans.NormalMode, 2644 SetVariables: InputValues{ 2645 "boop": &InputValue{ 2646 Value: cty.StringVal("nope"), 2647 SourceType: ValueFromCLIArg, 2648 }, 2649 }, 2650 }) 2651 if !diags.HasErrors() { 2652 t.Fatal("succeeded; want errors") 2653 } 2654 if got, want := diags.Err().Error(), "Resource precondition failed: Wrong boop."; got != want { 2655 t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want) 2656 } 2657 if p.PlanResourceChangeCalled { 2658 t.Errorf("Provider's PlanResourceChange was called; should'nt've been") 2659 } 2660 }) 2661 2662 t.Run("precondition fail refresh-only", func(t *testing.T) { 2663 state := states.BuildState(func(s *states.SyncState) { 2664 s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_resource.a"), &states.ResourceInstanceObjectSrc{ 2665 AttrsJSON: []byte(`{"value":"boop","output":"blorp"}`), 2666 Status: states.ObjectReady, 2667 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 2668 }) 2669 _, diags := ctx.Plan(m, state, &PlanOpts{ 2670 Mode: plans.RefreshOnlyMode, 2671 SetVariables: InputValues{ 2672 "boop": &InputValue{ 2673 Value: cty.StringVal("nope"), 2674 SourceType: ValueFromCLIArg, 2675 }, 2676 }, 2677 }) 2678 assertNoErrors(t, diags) 2679 if len(diags) == 0 { 2680 t.Fatalf("no diags, but should have warnings") 2681 } 2682 if got, want := diags.ErrWithWarnings().Error(), "Resource precondition failed: Wrong boop."; got != want { 2683 t.Fatalf("wrong warning:\ngot: %s\nwant: %q", got, want) 2684 } 2685 if !p.ReadResourceCalled { 2686 t.Errorf("Provider's ReadResource wasn't called; should've been") 2687 } 2688 }) 2689 2690 t.Run("postcondition fail", func(t *testing.T) { 2691 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 2692 m := req.ProposedNewState.AsValueMap() 2693 m["output"] = cty.StringVal("") 2694 2695 resp.PlannedState = cty.ObjectVal(m) 2696 resp.LegacyTypeSystem = true 2697 return resp 2698 } 2699 _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2700 Mode: plans.NormalMode, 2701 SetVariables: InputValues{ 2702 "boop": &InputValue{ 2703 Value: cty.StringVal("boop"), 2704 SourceType: ValueFromCLIArg, 2705 }, 2706 }, 2707 }) 2708 if !diags.HasErrors() { 2709 t.Fatal("succeeded; want errors") 2710 } 2711 if got, want := diags.Err().Error(), "Resource postcondition failed: Output must not be blank."; got != want { 2712 t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want) 2713 } 2714 if !p.PlanResourceChangeCalled { 2715 t.Errorf("Provider's PlanResourceChange wasn't called; should've been") 2716 } 2717 }) 2718 2719 t.Run("postcondition fail refresh-only", func(t *testing.T) { 2720 state := states.BuildState(func(s *states.SyncState) { 2721 s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_resource.a"), &states.ResourceInstanceObjectSrc{ 2722 AttrsJSON: []byte(`{"value":"boop","output":"blorp"}`), 2723 Status: states.ObjectReady, 2724 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 2725 }) 2726 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 2727 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 2728 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "output"}) { 2729 return cty.StringVal(""), nil 2730 } 2731 return v, nil 2732 }) 2733 if err != nil { 2734 // shouldn't get here 2735 t.Fatalf("ReadResourceFn transform failed") 2736 return providers.ReadResourceResponse{} 2737 } 2738 return providers.ReadResourceResponse{ 2739 NewState: newVal, 2740 } 2741 } 2742 _, diags := ctx.Plan(m, state, &PlanOpts{ 2743 Mode: plans.RefreshOnlyMode, 2744 SetVariables: InputValues{ 2745 "boop": &InputValue{ 2746 Value: cty.StringVal("boop"), 2747 SourceType: ValueFromCLIArg, 2748 }, 2749 }, 2750 }) 2751 assertNoErrors(t, diags) 2752 if len(diags) == 0 { 2753 t.Fatalf("no diags, but should have warnings") 2754 } 2755 if got, want := diags.ErrWithWarnings().Error(), "Resource postcondition failed: Output must not be blank."; got != want { 2756 t.Fatalf("wrong warning:\ngot: %s\nwant: %q", got, want) 2757 } 2758 if !p.ReadResourceCalled { 2759 t.Errorf("Provider's ReadResource wasn't called; should've been") 2760 } 2761 if p.PlanResourceChangeCalled { 2762 t.Errorf("Provider's PlanResourceChange was called; should'nt've been") 2763 } 2764 }) 2765 2766 t.Run("precondition and postcondition fail refresh-only", func(t *testing.T) { 2767 state := states.BuildState(func(s *states.SyncState) { 2768 s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_resource.a"), &states.ResourceInstanceObjectSrc{ 2769 AttrsJSON: []byte(`{"value":"boop","output":"blorp"}`), 2770 Status: states.ObjectReady, 2771 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 2772 }) 2773 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 2774 newVal, err := cty.Transform(req.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { 2775 if len(path) == 1 && path[0] == (cty.GetAttrStep{Name: "output"}) { 2776 return cty.StringVal(""), nil 2777 } 2778 return v, nil 2779 }) 2780 if err != nil { 2781 // shouldn't get here 2782 t.Fatalf("ReadResourceFn transform failed") 2783 return providers.ReadResourceResponse{} 2784 } 2785 return providers.ReadResourceResponse{ 2786 NewState: newVal, 2787 } 2788 } 2789 _, diags := ctx.Plan(m, state, &PlanOpts{ 2790 Mode: plans.RefreshOnlyMode, 2791 SetVariables: InputValues{ 2792 "boop": &InputValue{ 2793 Value: cty.StringVal("nope"), 2794 SourceType: ValueFromCLIArg, 2795 }, 2796 }, 2797 }) 2798 assertNoErrors(t, diags) 2799 if got, want := len(diags), 2; got != want { 2800 t.Errorf("wrong number of warnings, got %d, want %d", got, want) 2801 } 2802 warnings := diags.ErrWithWarnings().Error() 2803 wantWarnings := []string{ 2804 "Resource precondition failed: Wrong boop.", 2805 "Resource postcondition failed: Output must not be blank.", 2806 } 2807 for _, want := range wantWarnings { 2808 if !strings.Contains(warnings, want) { 2809 t.Errorf("missing warning:\ngot: %s\nwant to contain: %q", warnings, want) 2810 } 2811 } 2812 if !p.ReadResourceCalled { 2813 t.Errorf("Provider's ReadResource wasn't called; should've been") 2814 } 2815 if p.PlanResourceChangeCalled { 2816 t.Errorf("Provider's PlanResourceChange was called; should'nt've been") 2817 } 2818 }) 2819 } 2820 2821 func TestContext2Plan_dataSourcePreconditionPostcondition(t *testing.T) { 2822 m := testModuleInline(t, map[string]string{ 2823 "main.tf": ` 2824 variable "boop" { 2825 type = string 2826 } 2827 2828 data "test_data_source" "a" { 2829 foo = var.boop 2830 lifecycle { 2831 precondition { 2832 condition = var.boop == "boop" 2833 error_message = "Wrong boop." 2834 } 2835 postcondition { 2836 condition = length(self.results) > 0 2837 error_message = "Results cannot be empty." 2838 } 2839 } 2840 } 2841 2842 resource "test_resource" "a" { 2843 value = data.test_data_source.a.results[0] 2844 } 2845 `, 2846 }) 2847 2848 p := testProvider("test") 2849 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 2850 ResourceTypes: map[string]*configschema.Block{ 2851 "test_resource": { 2852 Attributes: map[string]*configschema.Attribute{ 2853 "value": { 2854 Type: cty.String, 2855 Required: true, 2856 }, 2857 }, 2858 }, 2859 }, 2860 DataSources: map[string]*configschema.Block{ 2861 "test_data_source": { 2862 Attributes: map[string]*configschema.Attribute{ 2863 "foo": { 2864 Type: cty.String, 2865 Required: true, 2866 }, 2867 "results": { 2868 Type: cty.List(cty.String), 2869 Computed: true, 2870 }, 2871 }, 2872 }, 2873 }, 2874 }) 2875 2876 ctx := testContext2(t, &ContextOpts{ 2877 Providers: map[addrs.Provider]providers.Factory{ 2878 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2879 }, 2880 }) 2881 2882 t.Run("conditions pass", func(t *testing.T) { 2883 p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ 2884 State: cty.ObjectVal(map[string]cty.Value{ 2885 "foo": cty.StringVal("boop"), 2886 "results": cty.ListVal([]cty.Value{cty.StringVal("boop")}), 2887 }), 2888 } 2889 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2890 Mode: plans.NormalMode, 2891 SetVariables: InputValues{ 2892 "boop": &InputValue{ 2893 Value: cty.StringVal("boop"), 2894 SourceType: ValueFromCLIArg, 2895 }, 2896 }, 2897 }) 2898 assertNoErrors(t, diags) 2899 for _, res := range plan.Changes.Resources { 2900 switch res.Addr.String() { 2901 case "test_resource.a": 2902 if res.Action != plans.Create { 2903 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2904 } 2905 case "data.test_data_source.a": 2906 if res.Action != plans.Read { 2907 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2908 } 2909 default: 2910 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2911 } 2912 } 2913 2914 addr := mustResourceInstanceAddr("data.test_data_source.a") 2915 if gotResult := plan.Checks.GetObjectResult(addr); gotResult == nil { 2916 t.Errorf("no check result for %s", addr) 2917 } else { 2918 wantResult := &states.CheckResultObject{ 2919 Status: checks.StatusPass, 2920 } 2921 if diff := cmp.Diff(wantResult, gotResult, valueComparer); diff != "" { 2922 t.Errorf("wrong check result for %s\n%s", addr, diff) 2923 } 2924 } 2925 }) 2926 2927 t.Run("precondition fail", func(t *testing.T) { 2928 _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2929 Mode: plans.NormalMode, 2930 SetVariables: InputValues{ 2931 "boop": &InputValue{ 2932 Value: cty.StringVal("nope"), 2933 SourceType: ValueFromCLIArg, 2934 }, 2935 }, 2936 }) 2937 if !diags.HasErrors() { 2938 t.Fatal("succeeded; want errors") 2939 } 2940 if got, want := diags.Err().Error(), "Resource precondition failed: Wrong boop."; got != want { 2941 t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want) 2942 } 2943 if p.ReadDataSourceCalled { 2944 t.Errorf("Provider's ReadResource was called; should'nt've been") 2945 } 2946 }) 2947 2948 t.Run("precondition fail refresh-only", func(t *testing.T) { 2949 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2950 Mode: plans.RefreshOnlyMode, 2951 SetVariables: InputValues{ 2952 "boop": &InputValue{ 2953 Value: cty.StringVal("nope"), 2954 SourceType: ValueFromCLIArg, 2955 }, 2956 }, 2957 }) 2958 assertNoErrors(t, diags) 2959 if len(diags) == 0 { 2960 t.Fatalf("no diags, but should have warnings") 2961 } 2962 if got, want := diags.ErrWithWarnings().Error(), "Resource precondition failed: Wrong boop."; got != want { 2963 t.Fatalf("wrong warning:\ngot: %s\nwant: %q", got, want) 2964 } 2965 for _, res := range plan.Changes.Resources { 2966 switch res.Addr.String() { 2967 case "test_resource.a": 2968 if res.Action != plans.Create { 2969 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2970 } 2971 case "data.test_data_source.a": 2972 if res.Action != plans.Read { 2973 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2974 } 2975 default: 2976 t.Fatalf("unexpected %s change for %s", res.Action, res.Addr) 2977 } 2978 } 2979 }) 2980 2981 t.Run("postcondition fail", func(t *testing.T) { 2982 p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ 2983 State: cty.ObjectVal(map[string]cty.Value{ 2984 "foo": cty.StringVal("boop"), 2985 "results": cty.ListValEmpty(cty.String), 2986 }), 2987 } 2988 _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2989 Mode: plans.NormalMode, 2990 SetVariables: InputValues{ 2991 "boop": &InputValue{ 2992 Value: cty.StringVal("boop"), 2993 SourceType: ValueFromCLIArg, 2994 }, 2995 }, 2996 }) 2997 if !diags.HasErrors() { 2998 t.Fatal("succeeded; want errors") 2999 } 3000 if got, want := diags.Err().Error(), "Resource postcondition failed: Results cannot be empty."; got != want { 3001 t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want) 3002 } 3003 if !p.ReadDataSourceCalled { 3004 t.Errorf("Provider's ReadDataSource wasn't called; should've been") 3005 } 3006 }) 3007 3008 t.Run("postcondition fail refresh-only", func(t *testing.T) { 3009 p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ 3010 State: cty.ObjectVal(map[string]cty.Value{ 3011 "foo": cty.StringVal("boop"), 3012 "results": cty.ListValEmpty(cty.String), 3013 }), 3014 } 3015 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 3016 Mode: plans.RefreshOnlyMode, 3017 SetVariables: InputValues{ 3018 "boop": &InputValue{ 3019 Value: cty.StringVal("boop"), 3020 SourceType: ValueFromCLIArg, 3021 }, 3022 }, 3023 }) 3024 assertNoErrors(t, diags) 3025 if got, want := diags.ErrWithWarnings().Error(), "Resource postcondition failed: Results cannot be empty."; got != want { 3026 t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want) 3027 } 3028 addr := mustResourceInstanceAddr("data.test_data_source.a") 3029 if gotResult := plan.Checks.GetObjectResult(addr); gotResult == nil { 3030 t.Errorf("no check result for %s", addr) 3031 } else { 3032 wantResult := &states.CheckResultObject{ 3033 Status: checks.StatusFail, 3034 FailureMessages: []string{ 3035 "Results cannot be empty.", 3036 }, 3037 } 3038 if diff := cmp.Diff(wantResult, gotResult, valueComparer); diff != "" { 3039 t.Errorf("wrong check result\n%s", diff) 3040 } 3041 } 3042 }) 3043 3044 t.Run("precondition and postcondition fail refresh-only", func(t *testing.T) { 3045 p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ 3046 State: cty.ObjectVal(map[string]cty.Value{ 3047 "foo": cty.StringVal("nope"), 3048 "results": cty.ListValEmpty(cty.String), 3049 }), 3050 } 3051 _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 3052 Mode: plans.RefreshOnlyMode, 3053 SetVariables: InputValues{ 3054 "boop": &InputValue{ 3055 Value: cty.StringVal("nope"), 3056 SourceType: ValueFromCLIArg, 3057 }, 3058 }, 3059 }) 3060 assertNoErrors(t, diags) 3061 if got, want := len(diags), 2; got != want { 3062 t.Errorf("wrong number of warnings, got %d, want %d", got, want) 3063 } 3064 warnings := diags.ErrWithWarnings().Error() 3065 wantWarnings := []string{ 3066 "Resource precondition failed: Wrong boop.", 3067 "Resource postcondition failed: Results cannot be empty.", 3068 } 3069 for _, want := range wantWarnings { 3070 if !strings.Contains(warnings, want) { 3071 t.Errorf("missing warning:\ngot: %s\nwant to contain: %q", warnings, want) 3072 } 3073 } 3074 }) 3075 } 3076 3077 func TestContext2Plan_outputPrecondition(t *testing.T) { 3078 m := testModuleInline(t, map[string]string{ 3079 "main.tf": ` 3080 variable "boop" { 3081 type = string 3082 } 3083 3084 output "a" { 3085 value = var.boop 3086 precondition { 3087 condition = var.boop == "boop" 3088 error_message = "Wrong boop." 3089 } 3090 } 3091 `, 3092 }) 3093 3094 p := testProvider("test") 3095 3096 ctx := testContext2(t, &ContextOpts{ 3097 Providers: map[addrs.Provider]providers.Factory{ 3098 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 3099 }, 3100 }) 3101 3102 t.Run("condition pass", func(t *testing.T) { 3103 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 3104 Mode: plans.NormalMode, 3105 SetVariables: InputValues{ 3106 "boop": &InputValue{ 3107 Value: cty.StringVal("boop"), 3108 SourceType: ValueFromCLIArg, 3109 }, 3110 }, 3111 }) 3112 assertNoErrors(t, diags) 3113 addr := addrs.RootModuleInstance.OutputValue("a") 3114 outputPlan := plan.Changes.OutputValue(addr) 3115 if outputPlan == nil { 3116 t.Fatalf("no plan for %s at all", addr) 3117 } 3118 if got, want := outputPlan.Addr, addr; !got.Equal(want) { 3119 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 3120 } 3121 if got, want := outputPlan.Action, plans.Create; got != want { 3122 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 3123 } 3124 if gotResult := plan.Checks.GetObjectResult(addr); gotResult == nil { 3125 t.Errorf("no check result for %s", addr) 3126 } else { 3127 wantResult := &states.CheckResultObject{ 3128 Status: checks.StatusPass, 3129 } 3130 if diff := cmp.Diff(wantResult, gotResult, valueComparer); diff != "" { 3131 t.Errorf("wrong check result\n%s", diff) 3132 } 3133 } 3134 }) 3135 3136 t.Run("condition fail", func(t *testing.T) { 3137 _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 3138 Mode: plans.NormalMode, 3139 SetVariables: InputValues{ 3140 "boop": &InputValue{ 3141 Value: cty.StringVal("nope"), 3142 SourceType: ValueFromCLIArg, 3143 }, 3144 }, 3145 }) 3146 if !diags.HasErrors() { 3147 t.Fatal("succeeded; want errors") 3148 } 3149 if got, want := diags.Err().Error(), "Module output value precondition failed: Wrong boop."; got != want { 3150 t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want) 3151 } 3152 }) 3153 3154 t.Run("condition fail refresh-only", func(t *testing.T) { 3155 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 3156 Mode: plans.RefreshOnlyMode, 3157 SetVariables: InputValues{ 3158 "boop": &InputValue{ 3159 Value: cty.StringVal("nope"), 3160 SourceType: ValueFromCLIArg, 3161 }, 3162 }, 3163 }) 3164 assertNoErrors(t, diags) 3165 if len(diags) == 0 { 3166 t.Fatalf("no diags, but should have warnings") 3167 } 3168 if got, want := diags.ErrWithWarnings().Error(), "Module output value precondition failed: Wrong boop."; got != want { 3169 t.Errorf("wrong warning:\ngot: %s\nwant: %q", got, want) 3170 } 3171 addr := addrs.RootModuleInstance.OutputValue("a") 3172 outputPlan := plan.Changes.OutputValue(addr) 3173 if outputPlan == nil { 3174 t.Fatalf("no plan for %s at all", addr) 3175 } 3176 if got, want := outputPlan.Addr, addr; !got.Equal(want) { 3177 t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) 3178 } 3179 if got, want := outputPlan.Action, plans.Create; got != want { 3180 t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) 3181 } 3182 if gotResult := plan.Checks.GetObjectResult(addr); gotResult == nil { 3183 t.Errorf("no condition result for %s", addr) 3184 } else { 3185 wantResult := &states.CheckResultObject{ 3186 Status: checks.StatusFail, 3187 FailureMessages: []string{"Wrong boop."}, 3188 } 3189 if diff := cmp.Diff(wantResult, gotResult, valueComparer); diff != "" { 3190 t.Errorf("wrong condition result\n%s", diff) 3191 } 3192 } 3193 }) 3194 } 3195 3196 func TestContext2Plan_preconditionErrors(t *testing.T) { 3197 testCases := []struct { 3198 condition string 3199 wantSummary string 3200 wantDetail string 3201 }{ 3202 { 3203 "data.test_data_source", 3204 "Invalid reference", 3205 `The "data" object must be followed by two attribute names`, 3206 }, 3207 { 3208 "self.value", 3209 `Invalid "self" reference`, 3210 "only in resource provisioner, connection, and postcondition blocks", 3211 }, 3212 { 3213 "data.foo.bar", 3214 "Reference to undeclared resource", 3215 `A data resource "foo" "bar" has not been declared in the root module`, 3216 }, 3217 { 3218 "test_resource.b.value", 3219 "Invalid condition result", 3220 "Condition expression must return either true or false", 3221 }, 3222 { 3223 "test_resource.c.value", 3224 "Invalid condition result", 3225 "Invalid condition result value: a bool is required", 3226 }, 3227 } 3228 3229 p := testProvider("test") 3230 ctx := testContext2(t, &ContextOpts{ 3231 Providers: map[addrs.Provider]providers.Factory{ 3232 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 3233 }, 3234 }) 3235 3236 for _, tc := range testCases { 3237 t.Run(tc.condition, func(t *testing.T) { 3238 main := fmt.Sprintf(` 3239 resource "test_resource" "a" { 3240 value = var.boop 3241 lifecycle { 3242 precondition { 3243 condition = %s 3244 error_message = "Not relevant." 3245 } 3246 } 3247 } 3248 3249 resource "test_resource" "b" { 3250 value = null 3251 } 3252 3253 resource "test_resource" "c" { 3254 value = "bar" 3255 } 3256 `, tc.condition) 3257 m := testModuleInline(t, map[string]string{"main.tf": main}) 3258 3259 _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 3260 if !diags.HasErrors() { 3261 t.Fatal("succeeded; want errors") 3262 } 3263 diag := diags[0] 3264 if got, want := diag.Description().Summary, tc.wantSummary; got != want { 3265 t.Errorf("unexpected summary\n got: %s\nwant: %s", got, want) 3266 } 3267 if got, want := diag.Description().Detail, tc.wantDetail; !strings.Contains(got, want) { 3268 t.Errorf("unexpected summary\ngot: %s\nwant to contain %q", got, want) 3269 } 3270 }) 3271 } 3272 } 3273 3274 func TestContext2Plan_preconditionSensitiveValues(t *testing.T) { 3275 p := testProvider("test") 3276 ctx := testContext2(t, &ContextOpts{ 3277 Providers: map[addrs.Provider]providers.Factory{ 3278 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 3279 }, 3280 }) 3281 3282 m := testModuleInline(t, map[string]string{ 3283 "main.tf": ` 3284 variable "boop" { 3285 sensitive = true 3286 type = string 3287 } 3288 3289 output "a" { 3290 sensitive = true 3291 value = var.boop 3292 3293 precondition { 3294 condition = length(var.boop) <= 4 3295 error_message = "Boop is too long, ${length(var.boop)} > 4" 3296 } 3297 } 3298 `, 3299 }) 3300 3301 _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 3302 Mode: plans.NormalMode, 3303 SetVariables: InputValues{ 3304 "boop": &InputValue{ 3305 Value: cty.StringVal("bleep"), 3306 SourceType: ValueFromCLIArg, 3307 }, 3308 }, 3309 }) 3310 if !diags.HasErrors() { 3311 t.Fatal("succeeded; want errors") 3312 } 3313 if got, want := len(diags), 2; got != want { 3314 t.Errorf("wrong number of diags, got %d, want %d", got, want) 3315 } 3316 for _, diag := range diags { 3317 desc := diag.Description() 3318 if desc.Summary == "Module output value precondition failed" { 3319 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) { 3320 t.Errorf("unexpected detail\ngot: %s\nwant to contain %q", got, want) 3321 } 3322 } else if desc.Summary == "Error message refers to sensitive values" { 3323 if got, want := desc.Detail, "The error expression used to explain this condition refers to sensitive values, so Terraform will not display the resulting message."; !strings.Contains(got, want) { 3324 t.Errorf("unexpected detail\ngot: %s\nwant to contain %q", got, want) 3325 } 3326 } else { 3327 t.Errorf("unexpected summary\ngot: %s", desc.Summary) 3328 } 3329 } 3330 } 3331 3332 func TestContext2Plan_triggeredBy(t *testing.T) { 3333 m := testModuleInline(t, map[string]string{ 3334 "main.tf": ` 3335 resource "test_object" "a" { 3336 count = 1 3337 test_string = "new" 3338 } 3339 resource "test_object" "b" { 3340 count = 1 3341 test_string = test_object.a[count.index].test_string 3342 lifecycle { 3343 # the change to test_string in the other resource should trigger replacement 3344 replace_triggered_by = [ test_object.a[count.index].test_string ] 3345 } 3346 } 3347 `, 3348 }) 3349 3350 p := simpleMockProvider() 3351 3352 ctx := testContext2(t, &ContextOpts{ 3353 Providers: map[addrs.Provider]providers.Factory{ 3354 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 3355 }, 3356 }) 3357 3358 state := states.BuildState(func(s *states.SyncState) { 3359 s.SetResourceInstanceCurrent( 3360 mustResourceInstanceAddr("test_object.a[0]"), 3361 &states.ResourceInstanceObjectSrc{ 3362 AttrsJSON: []byte(`{"test_string":"old"}`), 3363 Status: states.ObjectReady, 3364 }, 3365 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 3366 ) 3367 s.SetResourceInstanceCurrent( 3368 mustResourceInstanceAddr("test_object.b[0]"), 3369 &states.ResourceInstanceObjectSrc{ 3370 AttrsJSON: []byte(`{}`), 3371 Status: states.ObjectReady, 3372 }, 3373 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 3374 ) 3375 }) 3376 3377 plan, diags := ctx.Plan(m, state, &PlanOpts{ 3378 Mode: plans.NormalMode, 3379 }) 3380 if diags.HasErrors() { 3381 t.Fatalf("unexpected errors\n%s", diags.Err().Error()) 3382 } 3383 for _, c := range plan.Changes.Resources { 3384 switch c.Addr.String() { 3385 case "test_object.a[0]": 3386 if c.Action != plans.Update { 3387 t.Fatalf("unexpected %s change for %s\n", c.Action, c.Addr) 3388 } 3389 case "test_object.b[0]": 3390 if c.Action != plans.DeleteThenCreate { 3391 t.Fatalf("unexpected %s change for %s\n", c.Action, c.Addr) 3392 } 3393 if c.ActionReason != plans.ResourceInstanceReplaceByTriggers { 3394 t.Fatalf("incorrect reason for change: %s\n", c.ActionReason) 3395 } 3396 default: 3397 t.Fatal("unexpected change", c.Addr, c.Action) 3398 } 3399 } 3400 } 3401 3402 func TestContext2Plan_dataSchemaChange(t *testing.T) { 3403 // We can't decode the prior state when a data source upgrades the schema 3404 // in an incompatible way. Since prior state for data sources is purely 3405 // informational, decoding should be skipped altogether. 3406 m := testModuleInline(t, map[string]string{ 3407 "main.tf": ` 3408 data "test_object" "a" { 3409 obj { 3410 # args changes from a list to a map 3411 args = { 3412 val = "string" 3413 } 3414 } 3415 } 3416 `, 3417 }) 3418 3419 p := new(MockProvider) 3420 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 3421 DataSources: map[string]*configschema.Block{ 3422 "test_object": { 3423 Attributes: map[string]*configschema.Attribute{ 3424 "id": { 3425 Type: cty.String, 3426 Computed: true, 3427 }, 3428 }, 3429 BlockTypes: map[string]*configschema.NestedBlock{ 3430 "obj": { 3431 Block: configschema.Block{ 3432 Attributes: map[string]*configschema.Attribute{ 3433 "args": {Type: cty.Map(cty.String), Optional: true}, 3434 }, 3435 }, 3436 Nesting: configschema.NestingSet, 3437 }, 3438 }, 3439 }, 3440 }, 3441 }) 3442 3443 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 3444 resp.State = req.Config 3445 return resp 3446 } 3447 3448 state := states.BuildState(func(s *states.SyncState) { 3449 s.SetResourceInstanceCurrent(mustResourceInstanceAddr(`data.test_object.a`), &states.ResourceInstanceObjectSrc{ 3450 AttrsJSON: []byte(`{"id":"old","obj":[{"args":["string"]}]}`), 3451 Status: states.ObjectReady, 3452 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 3453 }) 3454 3455 ctx := testContext2(t, &ContextOpts{ 3456 Providers: map[addrs.Provider]providers.Factory{ 3457 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 3458 }, 3459 }) 3460 3461 _, diags := ctx.Plan(m, state, DefaultPlanOpts) 3462 assertNoErrors(t, diags) 3463 } 3464 3465 func TestContext2Plan_applyGraphError(t *testing.T) { 3466 m := testModuleInline(t, map[string]string{ 3467 "main.tf": ` 3468 resource "test_object" "a" { 3469 } 3470 resource "test_object" "b" { 3471 depends_on = [test_object.a] 3472 } 3473 `, 3474 }) 3475 3476 p := simpleMockProvider() 3477 3478 // Here we introduce a cycle via state which only shows up in the apply 3479 // graph where the actual destroy instances are connected in the graph. 3480 // This could happen for example when a user has an existing state with 3481 // stored dependencies, and changes the config in such a way that 3482 // contradicts the stored dependencies. 3483 state := states.NewState() 3484 root := state.EnsureModule(addrs.RootModuleInstance) 3485 root.SetResourceInstanceCurrent( 3486 mustResourceInstanceAddr("test_object.a").Resource, 3487 &states.ResourceInstanceObjectSrc{ 3488 Status: states.ObjectTainted, 3489 AttrsJSON: []byte(`{"test_string":"a"}`), 3490 Dependencies: []addrs.ConfigResource{mustResourceInstanceAddr("test_object.b").ContainingResource().Config()}, 3491 }, 3492 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 3493 ) 3494 root.SetResourceInstanceCurrent( 3495 mustResourceInstanceAddr("test_object.b").Resource, 3496 &states.ResourceInstanceObjectSrc{ 3497 Status: states.ObjectTainted, 3498 AttrsJSON: []byte(`{"test_string":"b"}`), 3499 }, 3500 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 3501 ) 3502 3503 ctx := testContext2(t, &ContextOpts{ 3504 Providers: map[addrs.Provider]providers.Factory{ 3505 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 3506 }, 3507 }) 3508 3509 _, diags := ctx.Plan(m, state, &PlanOpts{ 3510 Mode: plans.NormalMode, 3511 }) 3512 if !diags.HasErrors() { 3513 t.Fatal("cycle error not detected") 3514 } 3515 3516 msg := diags.ErrWithWarnings().Error() 3517 if !strings.Contains(msg, "Cycle") { 3518 t.Fatalf("no cycle error found:\n got: %s\n", msg) 3519 } 3520 }