github.com/opentofu/opentofu@v1.7.1/internal/tofu/context_apply2_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package tofu 7 8 import ( 9 "bytes" 10 "errors" 11 "fmt" 12 "strings" 13 "sync" 14 "testing" 15 "time" 16 17 "github.com/davecgh/go-spew/spew" 18 "github.com/google/go-cmp/cmp" 19 "github.com/zclconf/go-cty/cty" 20 21 "github.com/opentofu/opentofu/internal/addrs" 22 "github.com/opentofu/opentofu/internal/checks" 23 "github.com/opentofu/opentofu/internal/configs/configschema" 24 "github.com/opentofu/opentofu/internal/lang/marks" 25 "github.com/opentofu/opentofu/internal/plans" 26 "github.com/opentofu/opentofu/internal/providers" 27 "github.com/opentofu/opentofu/internal/states" 28 "github.com/opentofu/opentofu/internal/tfdiags" 29 ) 30 31 // Test that the PreApply hook is called with the correct deposed key 32 func TestContext2Apply_createBeforeDestroy_deposedKeyPreApply(t *testing.T) { 33 m := testModule(t, "apply-cbd-deposed-only") 34 p := testProvider("aws") 35 p.PlanResourceChangeFn = testDiffFn 36 p.ApplyResourceChangeFn = testApplyFn 37 38 deposedKey := states.NewDeposedKey() 39 40 state := states.NewState() 41 root := state.EnsureModule(addrs.RootModuleInstance) 42 root.SetResourceInstanceCurrent( 43 mustResourceInstanceAddr("aws_instance.bar").Resource, 44 &states.ResourceInstanceObjectSrc{ 45 Status: states.ObjectReady, 46 AttrsJSON: []byte(`{"id":"bar"}`), 47 }, 48 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 49 ) 50 root.SetResourceInstanceDeposed( 51 mustResourceInstanceAddr("aws_instance.bar").Resource, 52 deposedKey, 53 &states.ResourceInstanceObjectSrc{ 54 Status: states.ObjectTainted, 55 AttrsJSON: []byte(`{"id":"foo"}`), 56 }, 57 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 58 ) 59 60 hook := new(MockHook) 61 ctx := testContext2(t, &ContextOpts{ 62 Hooks: []Hook{hook}, 63 Providers: map[addrs.Provider]providers.Factory{ 64 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 65 }, 66 }) 67 68 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 69 if diags.HasErrors() { 70 t.Fatalf("diags: %s", diags.Err()) 71 } else { 72 t.Logf(legacyDiffComparisonString(plan.Changes)) 73 } 74 75 _, diags = ctx.Apply(plan, m) 76 if diags.HasErrors() { 77 t.Fatalf("diags: %s", diags.Err()) 78 } 79 80 // Verify PreApply was called correctly 81 if !hook.PreApplyCalled { 82 t.Fatalf("PreApply hook not called") 83 } 84 if addr, wantAddr := hook.PreApplyAddr, mustResourceInstanceAddr("aws_instance.bar"); !addr.Equal(wantAddr) { 85 t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) 86 } 87 if gen := hook.PreApplyGen; gen != deposedKey { 88 t.Errorf("expected gen to be %q, but was %q", deposedKey, gen) 89 } 90 } 91 92 func TestContext2Apply_destroyWithDataSourceExpansion(t *testing.T) { 93 // While managed resources store their destroy-time dependencies, data 94 // sources do not. This means that if a provider were only included in a 95 // destroy graph because of data sources, it could have dependencies which 96 // are not correctly ordered. Here we verify that the provider is not 97 // included in the destroy operation, and all dependency evaluations 98 // succeed. 99 100 m := testModuleInline(t, map[string]string{ 101 "main.tf": ` 102 module "mod" { 103 source = "./mod" 104 } 105 106 provider "other" { 107 foo = module.mod.data 108 } 109 110 # this should not require the provider be present during destroy 111 data "other_data_source" "a" { 112 } 113 `, 114 115 "mod/main.tf": ` 116 data "test_data_source" "a" { 117 count = 1 118 } 119 120 data "test_data_source" "b" { 121 count = data.test_data_source.a[0].foo == "ok" ? 1 : 0 122 } 123 124 output "data" { 125 value = data.test_data_source.a[0].foo == "ok" ? data.test_data_source.b[0].foo : "nope" 126 } 127 `, 128 }) 129 130 testP := testProvider("test") 131 otherP := testProvider("other") 132 133 readData := func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 134 return providers.ReadDataSourceResponse{ 135 State: cty.ObjectVal(map[string]cty.Value{ 136 "id": cty.StringVal("data_source"), 137 "foo": cty.StringVal("ok"), 138 }), 139 } 140 } 141 142 testP.ReadDataSourceFn = readData 143 otherP.ReadDataSourceFn = readData 144 145 ps := map[addrs.Provider]providers.Factory{ 146 addrs.NewDefaultProvider("test"): testProviderFuncFixed(testP), 147 addrs.NewDefaultProvider("other"): testProviderFuncFixed(otherP), 148 } 149 150 otherP.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 151 foo := req.Config.GetAttr("foo") 152 if foo.IsNull() || foo.AsString() != "ok" { 153 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("incorrect config val: %#v\n", foo)) 154 } 155 return resp 156 } 157 158 ctx := testContext2(t, &ContextOpts{ 159 Providers: ps, 160 }) 161 162 plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 163 if diags.HasErrors() { 164 t.Fatal(diags.Err()) 165 } 166 167 _, diags = ctx.Apply(plan, m) 168 if diags.HasErrors() { 169 t.Fatal(diags.Err()) 170 } 171 172 // now destroy the whole thing 173 ctx = testContext2(t, &ContextOpts{ 174 Providers: ps, 175 }) 176 177 plan, diags = ctx.Plan(m, states.NewState(), &PlanOpts{ 178 Mode: plans.DestroyMode, 179 }) 180 if diags.HasErrors() { 181 t.Fatal(diags.Err()) 182 } 183 184 otherP.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 185 // should not be used to destroy data sources 186 resp.Diagnostics = resp.Diagnostics.Append(errors.New("provider should not be used")) 187 return resp 188 } 189 190 _, diags = ctx.Apply(plan, m) 191 if diags.HasErrors() { 192 t.Fatal(diags.Err()) 193 } 194 } 195 196 func TestContext2Apply_destroyThenUpdate(t *testing.T) { 197 m := testModuleInline(t, map[string]string{ 198 "main.tf": ` 199 resource "test_instance" "a" { 200 value = "udpated" 201 } 202 `, 203 }) 204 205 p := testProvider("test") 206 p.PlanResourceChangeFn = testDiffFn 207 208 var orderMu sync.Mutex 209 var order []string 210 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 211 id := req.PriorState.GetAttr("id").AsString() 212 if id == "b" { 213 // slow down the b destroy, since a should wait for it 214 time.Sleep(100 * time.Millisecond) 215 } 216 217 orderMu.Lock() 218 order = append(order, id) 219 orderMu.Unlock() 220 221 resp.NewState = req.PlannedState 222 return resp 223 } 224 225 addrA := mustResourceInstanceAddr(`test_instance.a`) 226 addrB := mustResourceInstanceAddr(`test_instance.b`) 227 228 state := states.BuildState(func(s *states.SyncState) { 229 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 230 AttrsJSON: []byte(`{"id":"a","value":"old","type":"test"}`), 231 Status: states.ObjectReady, 232 }, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`)) 233 234 // test_instance.b depended on test_instance.a, and therefor should be 235 // destroyed before any changes to test_instance.a 236 s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{ 237 AttrsJSON: []byte(`{"id":"b"}`), 238 Status: states.ObjectReady, 239 Dependencies: []addrs.ConfigResource{addrA.ContainingResource().Config()}, 240 }, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`)) 241 }) 242 243 ctx := testContext2(t, &ContextOpts{ 244 Providers: map[addrs.Provider]providers.Factory{ 245 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 246 }, 247 }) 248 249 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 250 assertNoErrors(t, diags) 251 252 _, diags = ctx.Apply(plan, m) 253 if diags.HasErrors() { 254 t.Fatal(diags.Err()) 255 } 256 257 if order[0] != "b" { 258 t.Fatalf("expected apply order [b, a], got: %v\n", order) 259 } 260 } 261 262 // verify that dependencies are updated in the state during refresh and apply 263 func TestApply_updateDependencies(t *testing.T) { 264 state := states.NewState() 265 root := state.EnsureModule(addrs.RootModuleInstance) 266 267 fooAddr := mustResourceInstanceAddr("aws_instance.foo") 268 barAddr := mustResourceInstanceAddr("aws_instance.bar") 269 bazAddr := mustResourceInstanceAddr("aws_instance.baz") 270 bamAddr := mustResourceInstanceAddr("aws_instance.bam") 271 binAddr := mustResourceInstanceAddr("aws_instance.bin") 272 root.SetResourceInstanceCurrent( 273 fooAddr.Resource, 274 &states.ResourceInstanceObjectSrc{ 275 Status: states.ObjectReady, 276 AttrsJSON: []byte(`{"id":"foo"}`), 277 Dependencies: []addrs.ConfigResource{ 278 bazAddr.ContainingResource().Config(), 279 }, 280 }, 281 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 282 ) 283 root.SetResourceInstanceCurrent( 284 binAddr.Resource, 285 &states.ResourceInstanceObjectSrc{ 286 Status: states.ObjectReady, 287 AttrsJSON: []byte(`{"id":"bin","type":"aws_instance","unknown":"ok"}`), 288 Dependencies: []addrs.ConfigResource{ 289 bazAddr.ContainingResource().Config(), 290 }, 291 }, 292 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 293 ) 294 root.SetResourceInstanceCurrent( 295 bazAddr.Resource, 296 &states.ResourceInstanceObjectSrc{ 297 Status: states.ObjectReady, 298 AttrsJSON: []byte(`{"id":"baz"}`), 299 Dependencies: []addrs.ConfigResource{ 300 // Existing dependencies should not be removed from orphaned instances 301 bamAddr.ContainingResource().Config(), 302 }, 303 }, 304 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 305 ) 306 root.SetResourceInstanceCurrent( 307 barAddr.Resource, 308 &states.ResourceInstanceObjectSrc{ 309 Status: states.ObjectReady, 310 AttrsJSON: []byte(`{"id":"bar","foo":"foo"}`), 311 }, 312 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 313 ) 314 315 m := testModuleInline(t, map[string]string{ 316 "main.tf": ` 317 resource "aws_instance" "bar" { 318 foo = aws_instance.foo.id 319 } 320 321 resource "aws_instance" "foo" { 322 } 323 324 resource "aws_instance" "bin" { 325 } 326 `, 327 }) 328 329 p := testProvider("aws") 330 331 ctx := testContext2(t, &ContextOpts{ 332 Providers: map[addrs.Provider]providers.Factory{ 333 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 334 }, 335 }) 336 337 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 338 assertNoErrors(t, diags) 339 340 bar := plan.PriorState.ResourceInstance(barAddr) 341 if len(bar.Current.Dependencies) == 0 || !bar.Current.Dependencies[0].Equal(fooAddr.ContainingResource().Config()) { 342 t.Fatalf("bar should depend on foo after refresh, but got %s", bar.Current.Dependencies) 343 } 344 345 foo := plan.PriorState.ResourceInstance(fooAddr) 346 if len(foo.Current.Dependencies) == 0 || !foo.Current.Dependencies[0].Equal(bazAddr.ContainingResource().Config()) { 347 t.Fatalf("foo should depend on baz after refresh because of the update, but got %s", foo.Current.Dependencies) 348 } 349 350 bin := plan.PriorState.ResourceInstance(binAddr) 351 if len(bin.Current.Dependencies) != 0 { 352 t.Fatalf("bin should depend on nothing after refresh because there is no change, but got %s", bin.Current.Dependencies) 353 } 354 355 baz := plan.PriorState.ResourceInstance(bazAddr) 356 if len(baz.Current.Dependencies) == 0 || !baz.Current.Dependencies[0].Equal(bamAddr.ContainingResource().Config()) { 357 t.Fatalf("baz should depend on bam after refresh, but got %s", baz.Current.Dependencies) 358 } 359 360 state, diags = ctx.Apply(plan, m) 361 if diags.HasErrors() { 362 t.Fatal(diags.Err()) 363 } 364 365 bar = state.ResourceInstance(barAddr) 366 if len(bar.Current.Dependencies) == 0 || !bar.Current.Dependencies[0].Equal(fooAddr.ContainingResource().Config()) { 367 t.Fatalf("bar should still depend on foo after apply, but got %s", bar.Current.Dependencies) 368 } 369 370 foo = state.ResourceInstance(fooAddr) 371 if len(foo.Current.Dependencies) != 0 { 372 t.Fatalf("foo should have no deps after apply, but got %s", foo.Current.Dependencies) 373 } 374 375 } 376 377 func TestContext2Apply_additionalSensitiveFromState(t *testing.T) { 378 // Ensure we're not trying to double-mark values decoded from state 379 m := testModuleInline(t, map[string]string{ 380 "main.tf": ` 381 variable "secret" { 382 sensitive = true 383 default = ["secret"] 384 } 385 386 resource "test_resource" "a" { 387 sensitive_attr = var.secret 388 } 389 390 resource "test_resource" "b" { 391 value = test_resource.a.id 392 } 393 `, 394 }) 395 396 p := new(MockProvider) 397 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 398 ResourceTypes: map[string]*configschema.Block{ 399 "test_resource": { 400 Attributes: map[string]*configschema.Attribute{ 401 "id": { 402 Type: cty.String, 403 Computed: true, 404 }, 405 "value": { 406 Type: cty.String, 407 Optional: true, 408 }, 409 "sensitive_attr": { 410 Type: cty.List(cty.String), 411 Optional: true, 412 Sensitive: true, 413 }, 414 }, 415 }, 416 }, 417 }) 418 419 state := states.BuildState(func(s *states.SyncState) { 420 s.SetResourceInstanceCurrent( 421 mustResourceInstanceAddr(`test_resource.a`), 422 &states.ResourceInstanceObjectSrc{ 423 AttrsJSON: []byte(`{"id":"a","sensitive_attr":["secret"]}`), 424 AttrSensitivePaths: []cty.PathValueMarks{ 425 { 426 Path: cty.GetAttrPath("sensitive_attr"), 427 Marks: cty.NewValueMarks(marks.Sensitive), 428 }, 429 }, 430 Status: states.ObjectReady, 431 }, mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`), 432 ) 433 }) 434 435 ctx := testContext2(t, &ContextOpts{ 436 Providers: map[addrs.Provider]providers.Factory{ 437 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 438 }, 439 }) 440 441 plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) 442 assertNoErrors(t, diags) 443 444 _, diags = ctx.Apply(plan, m) 445 if diags.HasErrors() { 446 t.Fatal(diags.ErrWithWarnings()) 447 } 448 } 449 450 func TestContext2Apply_sensitiveOutputPassthrough(t *testing.T) { 451 // Ensure we're not trying to double-mark values decoded from state 452 m := testModuleInline(t, map[string]string{ 453 "main.tf": ` 454 module "mod" { 455 source = "./mod" 456 } 457 458 resource "test_object" "a" { 459 test_string = module.mod.out 460 } 461 `, 462 463 "mod/main.tf": ` 464 variable "in" { 465 sensitive = true 466 default = "foo" 467 } 468 output "out" { 469 value = var.in 470 } 471 `, 472 }) 473 474 p := simpleMockProvider() 475 476 ctx := testContext2(t, &ContextOpts{ 477 Providers: map[addrs.Provider]providers.Factory{ 478 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 479 }, 480 }) 481 482 plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 483 assertNoErrors(t, diags) 484 485 state, diags := ctx.Apply(plan, m) 486 if diags.HasErrors() { 487 t.Fatal(diags.ErrWithWarnings()) 488 } 489 490 obj := state.ResourceInstance(mustResourceInstanceAddr("test_object.a")) 491 if len(obj.Current.AttrSensitivePaths) != 1 { 492 t.Fatalf("Expected 1 sensitive mark for test_object.a, got %#v\n", obj.Current.AttrSensitivePaths) 493 } 494 495 plan, diags = ctx.Plan(m, state, DefaultPlanOpts) 496 assertNoErrors(t, diags) 497 498 // make sure the same marks are compared in the next plan as well 499 for _, c := range plan.Changes.Resources { 500 if c.Action != plans.NoOp { 501 t.Errorf("Unexpcetd %s change for %s", c.Action, c.Addr) 502 } 503 } 504 } 505 506 func TestContext2Apply_ignoreImpureFunctionChanges(t *testing.T) { 507 // The impure function call should not cause a planned change with 508 // ignore_changes 509 m := testModuleInline(t, map[string]string{ 510 "main.tf": ` 511 variable "pw" { 512 sensitive = true 513 default = "foo" 514 } 515 516 resource "test_object" "x" { 517 test_map = { 518 string = "X${bcrypt(var.pw)}" 519 } 520 lifecycle { 521 ignore_changes = [ test_map["string"] ] 522 } 523 } 524 525 resource "test_object" "y" { 526 test_map = { 527 string = "X${bcrypt(var.pw)}" 528 } 529 lifecycle { 530 ignore_changes = [ test_map ] 531 } 532 } 533 534 `, 535 }) 536 537 p := simpleMockProvider() 538 539 ctx := testContext2(t, &ContextOpts{ 540 Providers: map[addrs.Provider]providers.Factory{ 541 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 542 }, 543 }) 544 545 plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) 546 assertNoErrors(t, diags) 547 548 state, diags := ctx.Apply(plan, m) 549 assertNoErrors(t, diags) 550 551 // FINAL PLAN: 552 plan, diags = ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) 553 assertNoErrors(t, diags) 554 555 // make sure the same marks are compared in the next plan as well 556 for _, c := range plan.Changes.Resources { 557 if c.Action != plans.NoOp { 558 t.Logf("marks before: %#v", c.BeforeValMarks) 559 t.Logf("marks after: %#v", c.AfterValMarks) 560 t.Errorf("Unexpcetd %s change for %s", c.Action, c.Addr) 561 } 562 } 563 } 564 565 func TestContext2Apply_destroyWithDeposed(t *testing.T) { 566 m := testModuleInline(t, map[string]string{ 567 "main.tf": ` 568 resource "test_object" "x" { 569 test_string = "ok" 570 lifecycle { 571 create_before_destroy = true 572 } 573 }`, 574 }) 575 576 p := simpleMockProvider() 577 578 deposedKey := states.NewDeposedKey() 579 580 state := states.NewState() 581 root := state.EnsureModule(addrs.RootModuleInstance) 582 root.SetResourceInstanceDeposed( 583 mustResourceInstanceAddr("test_object.x").Resource, 584 deposedKey, 585 &states.ResourceInstanceObjectSrc{ 586 Status: states.ObjectTainted, 587 AttrsJSON: []byte(`{"test_string":"deposed"}`), 588 }, 589 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`), 590 ) 591 592 ctx := testContext2(t, &ContextOpts{ 593 Providers: map[addrs.Provider]providers.Factory{ 594 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 595 }, 596 }) 597 598 plan, diags := ctx.Plan(m, state, &PlanOpts{ 599 Mode: plans.DestroyMode, 600 }) 601 if diags.HasErrors() { 602 t.Fatalf("plan: %s", diags.Err()) 603 } 604 605 _, diags = ctx.Apply(plan, m) 606 if diags.HasErrors() { 607 t.Fatalf("apply: %s", diags.Err()) 608 } 609 610 } 611 612 func TestContext2Apply_nullableVariables(t *testing.T) { 613 m := testModule(t, "apply-nullable-variables") 614 state := states.NewState() 615 ctx := testContext2(t, &ContextOpts{}) 616 plan, diags := ctx.Plan(m, state, &PlanOpts{}) 617 if diags.HasErrors() { 618 t.Fatalf("plan: %s", diags.Err()) 619 } 620 state, diags = ctx.Apply(plan, m) 621 if diags.HasErrors() { 622 t.Fatalf("apply: %s", diags.Err()) 623 } 624 625 outputs := state.Module(addrs.RootModuleInstance).OutputValues 626 // we check for null outputs be seeing that they don't exists 627 if _, ok := outputs["nullable_null_default"]; ok { 628 t.Error("nullable_null_default: expected no output value") 629 } 630 if _, ok := outputs["nullable_non_null_default"]; ok { 631 t.Error("nullable_non_null_default: expected no output value") 632 } 633 if _, ok := outputs["nullable_no_default"]; ok { 634 t.Error("nullable_no_default: expected no output value") 635 } 636 637 if v := outputs["non_nullable_default"].Value; v.AsString() != "ok" { 638 t.Fatalf("incorrect 'non_nullable_default' output value: %#v\n", v) 639 } 640 if v := outputs["non_nullable_no_default"].Value; v.AsString() != "ok" { 641 t.Fatalf("incorrect 'non_nullable_no_default' output value: %#v\n", v) 642 } 643 } 644 645 func TestContext2Apply_targetedDestroyWithMoved(t *testing.T) { 646 m := testModuleInline(t, map[string]string{ 647 "main.tf": ` 648 module "modb" { 649 source = "./mod" 650 for_each = toset(["a", "b"]) 651 } 652 `, 653 "./mod/main.tf": ` 654 resource "test_object" "a" { 655 } 656 657 module "sub" { 658 for_each = toset(["a", "b"]) 659 source = "./sub" 660 } 661 662 moved { 663 from = module.old 664 to = module.sub 665 } 666 `, 667 "./mod/sub/main.tf": ` 668 resource "test_object" "s" { 669 } 670 `}) 671 672 p := simpleMockProvider() 673 674 ctx := testContext2(t, &ContextOpts{ 675 Providers: map[addrs.Provider]providers.Factory{ 676 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 677 }, 678 }) 679 680 plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 681 assertNoErrors(t, diags) 682 683 state, diags := ctx.Apply(plan, m) 684 assertNoErrors(t, diags) 685 686 // destroy only a single instance not included in the moved statements 687 _, diags = ctx.Plan(m, state, &PlanOpts{ 688 Mode: plans.DestroyMode, 689 Targets: []addrs.Targetable{mustResourceInstanceAddr(`module.modb["a"].test_object.a`)}, 690 }) 691 assertNoErrors(t, diags) 692 } 693 694 func TestContext2Apply_graphError(t *testing.T) { 695 m := testModuleInline(t, map[string]string{ 696 "main.tf": ` 697 resource "test_object" "a" { 698 test_string = "ok" 699 } 700 701 resource "test_object" "b" { 702 test_string = test_object.a.test_string 703 } 704 `, 705 }) 706 707 p := simpleMockProvider() 708 709 state := states.NewState() 710 root := state.EnsureModule(addrs.RootModuleInstance) 711 root.SetResourceInstanceCurrent( 712 mustResourceInstanceAddr("test_object.a").Resource, 713 &states.ResourceInstanceObjectSrc{ 714 Status: states.ObjectTainted, 715 AttrsJSON: []byte(`{"test_string":"ok"}`), 716 }, 717 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`), 718 ) 719 root.SetResourceInstanceCurrent( 720 mustResourceInstanceAddr("test_object.b").Resource, 721 &states.ResourceInstanceObjectSrc{ 722 Status: states.ObjectTainted, 723 AttrsJSON: []byte(`{"test_string":"ok"}`), 724 }, 725 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`), 726 ) 727 728 ctx := testContext2(t, &ContextOpts{ 729 Providers: map[addrs.Provider]providers.Factory{ 730 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 731 }, 732 }) 733 734 plan, diags := ctx.Plan(m, state, &PlanOpts{ 735 Mode: plans.DestroyMode, 736 }) 737 if diags.HasErrors() { 738 t.Fatalf("plan: %s", diags.Err()) 739 } 740 741 // We're going to corrupt the stored state so that the dependencies will 742 // cause a cycle when building the apply graph. 743 testObjA := plan.PriorState.Modules[""].Resources["test_object.a"].Instances[addrs.NoKey].Current 744 testObjA.Dependencies = append(testObjA.Dependencies, mustResourceInstanceAddr("test_object.b").ContainingResource().Config()) 745 746 _, diags = ctx.Apply(plan, m) 747 if !diags.HasErrors() { 748 t.Fatal("expected cycle error from apply") 749 } 750 } 751 752 func TestContext2Apply_resourcePostcondition(t *testing.T) { 753 m := testModuleInline(t, map[string]string{ 754 "main.tf": ` 755 variable "boop" { 756 type = string 757 } 758 759 resource "test_resource" "a" { 760 value = var.boop 761 } 762 763 resource "test_resource" "b" { 764 value = test_resource.a.output 765 lifecycle { 766 postcondition { 767 condition = self.output != "" 768 error_message = "Output must not be blank." 769 } 770 } 771 } 772 773 resource "test_resource" "c" { 774 value = test_resource.b.output 775 } 776 `, 777 }) 778 779 p := testProvider("test") 780 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 781 ResourceTypes: map[string]*configschema.Block{ 782 "test_resource": { 783 Attributes: map[string]*configschema.Attribute{ 784 "value": { 785 Type: cty.String, 786 Required: true, 787 }, 788 "output": { 789 Type: cty.String, 790 Computed: true, 791 }, 792 }, 793 }, 794 }, 795 }) 796 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 797 m := req.ProposedNewState.AsValueMap() 798 m["output"] = cty.UnknownVal(cty.String) 799 800 resp.PlannedState = cty.ObjectVal(m) 801 resp.LegacyTypeSystem = true 802 return resp 803 } 804 ctx := testContext2(t, &ContextOpts{ 805 Providers: map[addrs.Provider]providers.Factory{ 806 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 807 }, 808 }) 809 810 t.Run("condition pass", func(t *testing.T) { 811 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 812 Mode: plans.NormalMode, 813 SetVariables: InputValues{ 814 "boop": &InputValue{ 815 Value: cty.StringVal("boop"), 816 SourceType: ValueFromCLIArg, 817 }, 818 }, 819 }) 820 assertNoErrors(t, diags) 821 if len(plan.Changes.Resources) != 3 { 822 t.Fatalf("unexpected plan changes: %#v", plan.Changes) 823 } 824 825 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 826 m := req.PlannedState.AsValueMap() 827 m["output"] = cty.StringVal(fmt.Sprintf("new-%s", m["value"].AsString())) 828 829 resp.NewState = cty.ObjectVal(m) 830 return resp 831 } 832 state, diags := ctx.Apply(plan, m) 833 assertNoErrors(t, diags) 834 835 wantResourceAttrs := map[string]struct{ value, output string }{ 836 "a": {"boop", "new-boop"}, 837 "b": {"new-boop", "new-new-boop"}, 838 "c": {"new-new-boop", "new-new-new-boop"}, 839 } 840 for name, attrs := range wantResourceAttrs { 841 addr := mustResourceInstanceAddr(fmt.Sprintf("test_resource.%s", name)) 842 r := state.ResourceInstance(addr) 843 rd, err := r.Current.Decode(cty.Object(map[string]cty.Type{ 844 "value": cty.String, 845 "output": cty.String, 846 })) 847 if err != nil { 848 t.Fatalf("error decoding test_resource.a: %s", err) 849 } 850 want := cty.ObjectVal(map[string]cty.Value{ 851 "value": cty.StringVal(attrs.value), 852 "output": cty.StringVal(attrs.output), 853 }) 854 if !cmp.Equal(want, rd.Value, valueComparer) { 855 t.Errorf("wrong attrs for %s\n%s", addr, cmp.Diff(want, rd.Value, valueComparer)) 856 } 857 } 858 }) 859 t.Run("condition fail", func(t *testing.T) { 860 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 861 Mode: plans.NormalMode, 862 SetVariables: InputValues{ 863 "boop": &InputValue{ 864 Value: cty.StringVal("boop"), 865 SourceType: ValueFromCLIArg, 866 }, 867 }, 868 }) 869 assertNoErrors(t, diags) 870 if len(plan.Changes.Resources) != 3 { 871 t.Fatalf("unexpected plan changes: %#v", plan.Changes) 872 } 873 874 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 875 m := req.PlannedState.AsValueMap() 876 877 // For the resource with a constraint, fudge the output to make the 878 // condition fail. 879 if value := m["value"].AsString(); value == "new-boop" { 880 m["output"] = cty.StringVal("") 881 } else { 882 m["output"] = cty.StringVal(fmt.Sprintf("new-%s", value)) 883 } 884 885 resp.NewState = cty.ObjectVal(m) 886 return resp 887 } 888 state, diags := ctx.Apply(plan, m) 889 if !diags.HasErrors() { 890 t.Fatal("succeeded; want errors") 891 } 892 if got, want := diags.Err().Error(), "Resource postcondition failed: Output must not be blank."; got != want { 893 t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want) 894 } 895 896 // Resources a and b should still be recorded in state 897 wantResourceAttrs := map[string]struct{ value, output string }{ 898 "a": {"boop", "new-boop"}, 899 "b": {"new-boop", ""}, 900 } 901 for name, attrs := range wantResourceAttrs { 902 addr := mustResourceInstanceAddr(fmt.Sprintf("test_resource.%s", name)) 903 r := state.ResourceInstance(addr) 904 rd, err := r.Current.Decode(cty.Object(map[string]cty.Type{ 905 "value": cty.String, 906 "output": cty.String, 907 })) 908 if err != nil { 909 t.Fatalf("error decoding test_resource.a: %s", err) 910 } 911 want := cty.ObjectVal(map[string]cty.Value{ 912 "value": cty.StringVal(attrs.value), 913 "output": cty.StringVal(attrs.output), 914 }) 915 if !cmp.Equal(want, rd.Value, valueComparer) { 916 t.Errorf("wrong attrs for %s\n%s", addr, cmp.Diff(want, rd.Value, valueComparer)) 917 } 918 } 919 920 // Resource c should not be in state 921 if state.ResourceInstance(mustResourceInstanceAddr("test_resource.c")) != nil { 922 t.Error("test_resource.c should not exist in state, but is") 923 } 924 }) 925 } 926 927 func TestContext2Apply_outputValuePrecondition(t *testing.T) { 928 m := testModuleInline(t, map[string]string{ 929 "main.tf": ` 930 variable "input" { 931 type = string 932 } 933 934 module "child" { 935 source = "./child" 936 937 input = var.input 938 } 939 940 output "result" { 941 value = module.child.result 942 943 precondition { 944 condition = var.input != "" 945 error_message = "Input must not be empty." 946 } 947 } 948 `, 949 "child/main.tf": ` 950 variable "input" { 951 type = string 952 } 953 954 output "result" { 955 value = var.input 956 957 precondition { 958 condition = var.input != "" 959 error_message = "Input must not be empty." 960 } 961 } 962 `, 963 }) 964 965 checkableObjects := []addrs.Checkable{ 966 addrs.OutputValue{Name: "result"}.Absolute(addrs.RootModuleInstance), 967 addrs.OutputValue{Name: "result"}.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)), 968 } 969 970 t.Run("pass", func(t *testing.T) { 971 ctx := testContext2(t, &ContextOpts{}) 972 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 973 Mode: plans.NormalMode, 974 SetVariables: InputValues{ 975 "input": &InputValue{ 976 Value: cty.StringVal("beep"), 977 SourceType: ValueFromCLIArg, 978 }, 979 }, 980 }) 981 assertNoDiagnostics(t, diags) 982 983 for _, addr := range checkableObjects { 984 result := plan.Checks.GetObjectResult(addr) 985 if result == nil { 986 t.Fatalf("no check result for %s in the plan", addr) 987 } 988 if got, want := result.Status, checks.StatusPass; got != want { 989 t.Fatalf("wrong check status for %s during planning\ngot: %s\nwant: %s", addr, got, want) 990 } 991 } 992 993 state, diags := ctx.Apply(plan, m) 994 assertNoDiagnostics(t, diags) 995 for _, addr := range checkableObjects { 996 result := state.CheckResults.GetObjectResult(addr) 997 if result == nil { 998 t.Fatalf("no check result for %s in the final state", addr) 999 } 1000 if got, want := result.Status, checks.StatusPass; got != want { 1001 t.Errorf("wrong check status for %s after apply\ngot: %s\nwant: %s", addr, got, want) 1002 } 1003 } 1004 }) 1005 1006 t.Run("fail", func(t *testing.T) { 1007 // NOTE: This test actually catches a failure during planning and so 1008 // cannot proceed to apply, so it's really more of a plan test 1009 // than an apply test but better to keep all of these 1010 // thematically-related test cases together. 1011 ctx := testContext2(t, &ContextOpts{}) 1012 _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 1013 Mode: plans.NormalMode, 1014 SetVariables: InputValues{ 1015 "input": &InputValue{ 1016 Value: cty.StringVal(""), 1017 SourceType: ValueFromCLIArg, 1018 }, 1019 }, 1020 }) 1021 if !diags.HasErrors() { 1022 t.Fatalf("succeeded; want error") 1023 } 1024 1025 const wantSummary = "Module output value precondition failed" 1026 found := false 1027 for _, diag := range diags { 1028 if diag.Severity() == tfdiags.Error && diag.Description().Summary == wantSummary { 1029 found = true 1030 break 1031 } 1032 } 1033 1034 if !found { 1035 t.Fatalf("missing expected error\nwant summary: %s\ngot: %s", wantSummary, diags.Err().Error()) 1036 } 1037 }) 1038 } 1039 1040 func TestContext2Apply_resourceConditionApplyTimeFail(t *testing.T) { 1041 // This tests the less common situation where a condition fails due to 1042 // a change in a resource other than the one the condition is attached to, 1043 // and the condition result is unknown during planning. 1044 // 1045 // This edge case is a tricky one because it relies on OpenTofu still 1046 // visiting test_resource.b (in the configuration below) to evaluate 1047 // its conditions even though there aren't any changes directly planned 1048 // for it, so that we can consider whether changes to test_resource.a 1049 // have changed the outcome. 1050 1051 m := testModuleInline(t, map[string]string{ 1052 "main.tf": ` 1053 variable "input" { 1054 type = string 1055 } 1056 1057 resource "test_resource" "a" { 1058 value = var.input 1059 } 1060 1061 resource "test_resource" "b" { 1062 value = "beep" 1063 1064 lifecycle { 1065 postcondition { 1066 condition = test_resource.a.output == self.output 1067 error_message = "Outputs must match." 1068 } 1069 } 1070 } 1071 `, 1072 }) 1073 1074 p := testProvider("test") 1075 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 1076 ResourceTypes: map[string]*configschema.Block{ 1077 "test_resource": { 1078 Attributes: map[string]*configschema.Attribute{ 1079 "value": { 1080 Type: cty.String, 1081 Required: true, 1082 }, 1083 "output": { 1084 Type: cty.String, 1085 Computed: true, 1086 }, 1087 }, 1088 }, 1089 }, 1090 }) 1091 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 1092 // Whenever "value" changes, "output" follows it during the apply step, 1093 // but is initially unknown during the plan step. 1094 1095 m := req.ProposedNewState.AsValueMap() 1096 priorVal := cty.NullVal(cty.String) 1097 if !req.PriorState.IsNull() { 1098 priorVal = req.PriorState.GetAttr("value") 1099 } 1100 if m["output"].IsNull() || !priorVal.RawEquals(m["value"]) { 1101 m["output"] = cty.UnknownVal(cty.String) 1102 } 1103 1104 resp.PlannedState = cty.ObjectVal(m) 1105 resp.LegacyTypeSystem = true 1106 return resp 1107 } 1108 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 1109 m := req.PlannedState.AsValueMap() 1110 m["output"] = m["value"] 1111 resp.NewState = cty.ObjectVal(m) 1112 return resp 1113 } 1114 ctx := testContext2(t, &ContextOpts{ 1115 Providers: map[addrs.Provider]providers.Factory{ 1116 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1117 }, 1118 }) 1119 instA := mustResourceInstanceAddr("test_resource.a") 1120 instB := mustResourceInstanceAddr("test_resource.b") 1121 1122 // Preparation: an initial plan and apply with a correct input variable 1123 // should succeed and give us a valid and complete state to use for the 1124 // subsequent plan and apply that we'll expect to fail. 1125 var prevRunState *states.State 1126 { 1127 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 1128 Mode: plans.NormalMode, 1129 SetVariables: InputValues{ 1130 "input": &InputValue{ 1131 Value: cty.StringVal("beep"), 1132 SourceType: ValueFromCLIArg, 1133 }, 1134 }, 1135 }) 1136 assertNoErrors(t, diags) 1137 planA := plan.Changes.ResourceInstance(instA) 1138 if planA == nil || planA.Action != plans.Create { 1139 t.Fatalf("incorrect initial plan for instance A\nwant a 'create' change\ngot: %s", spew.Sdump(planA)) 1140 } 1141 planB := plan.Changes.ResourceInstance(instB) 1142 if planB == nil || planB.Action != plans.Create { 1143 t.Fatalf("incorrect initial plan for instance B\nwant a 'create' change\ngot: %s", spew.Sdump(planB)) 1144 } 1145 1146 state, diags := ctx.Apply(plan, m) 1147 assertNoErrors(t, diags) 1148 1149 stateA := state.ResourceInstance(instA) 1150 if stateA == nil || stateA.Current == nil || !bytes.Contains(stateA.Current.AttrsJSON, []byte(`"beep"`)) { 1151 t.Fatalf("incorrect initial state for instance A\ngot: %s", spew.Sdump(stateA)) 1152 } 1153 stateB := state.ResourceInstance(instB) 1154 if stateB == nil || stateB.Current == nil || !bytes.Contains(stateB.Current.AttrsJSON, []byte(`"beep"`)) { 1155 t.Fatalf("incorrect initial state for instance B\ngot: %s", spew.Sdump(stateB)) 1156 } 1157 prevRunState = state 1158 } 1159 1160 // Now we'll run another plan and apply with a different value for 1161 // var.input that should cause the test_resource.b condition to be unknown 1162 // during planning and then fail during apply. 1163 { 1164 plan, diags := ctx.Plan(m, prevRunState, &PlanOpts{ 1165 Mode: plans.NormalMode, 1166 SetVariables: InputValues{ 1167 "input": &InputValue{ 1168 Value: cty.StringVal("boop"), // NOTE: This has changed 1169 SourceType: ValueFromCLIArg, 1170 }, 1171 }, 1172 }) 1173 assertNoErrors(t, diags) 1174 planA := plan.Changes.ResourceInstance(instA) 1175 if planA == nil || planA.Action != plans.Update { 1176 t.Fatalf("incorrect initial plan for instance A\nwant an 'update' change\ngot: %s", spew.Sdump(planA)) 1177 } 1178 planB := plan.Changes.ResourceInstance(instB) 1179 if planB == nil || planB.Action != plans.NoOp { 1180 t.Fatalf("incorrect initial plan for instance B\nwant a 'no-op' change\ngot: %s", spew.Sdump(planB)) 1181 } 1182 1183 _, diags = ctx.Apply(plan, m) 1184 if !diags.HasErrors() { 1185 t.Fatal("final apply succeeded, but should've failed with a postcondition error") 1186 } 1187 if len(diags) != 1 { 1188 t.Fatalf("expected exactly one diagnostic, but got: %s", diags.Err().Error()) 1189 } 1190 if got, want := diags[0].Description().Summary, "Resource postcondition failed"; got != want { 1191 t.Fatalf("wrong diagnostic summary\ngot: %s\nwant: %s", got, want) 1192 } 1193 } 1194 } 1195 1196 // pass an input through some expanded values, and back to a provider to make 1197 // sure we can fully evaluate a provider configuration during a destroy plan. 1198 func TestContext2Apply_destroyWithConfiguredProvider(t *testing.T) { 1199 m := testModuleInline(t, map[string]string{ 1200 "main.tf": ` 1201 variable "in" { 1202 type = map(string) 1203 default = { 1204 "a" = "first" 1205 "b" = "second" 1206 } 1207 } 1208 1209 module "mod" { 1210 source = "./mod" 1211 for_each = var.in 1212 in = each.value 1213 } 1214 1215 locals { 1216 config = [for each in module.mod : each.out] 1217 } 1218 1219 provider "other" { 1220 output = [for each in module.mod : each.out] 1221 local = local.config 1222 var = var.in 1223 } 1224 1225 resource "other_object" "other" { 1226 } 1227 `, 1228 "./mod/main.tf": ` 1229 variable "in" { 1230 type = string 1231 } 1232 1233 data "test_object" "d" { 1234 test_string = var.in 1235 } 1236 1237 resource "test_object" "a" { 1238 test_string = var.in 1239 } 1240 1241 output "out" { 1242 value = data.test_object.d.output 1243 } 1244 `}) 1245 1246 testProvider := &MockProvider{ 1247 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 1248 Provider: providers.Schema{Block: simpleTestSchema()}, 1249 ResourceTypes: map[string]providers.Schema{ 1250 "test_object": providers.Schema{Block: simpleTestSchema()}, 1251 }, 1252 DataSources: map[string]providers.Schema{ 1253 "test_object": providers.Schema{ 1254 Block: &configschema.Block{ 1255 Attributes: map[string]*configschema.Attribute{ 1256 "test_string": { 1257 Type: cty.String, 1258 Optional: true, 1259 }, 1260 "output": { 1261 Type: cty.String, 1262 Computed: true, 1263 }, 1264 }, 1265 }, 1266 }, 1267 }, 1268 }, 1269 } 1270 1271 testProvider.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 1272 cfg := req.Config.AsValueMap() 1273 s := cfg["test_string"].AsString() 1274 if !strings.Contains("firstsecond", s) { 1275 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("expected 'first' or 'second', got %s", s)) 1276 return resp 1277 } 1278 1279 cfg["output"] = cty.StringVal(s + "-ok") 1280 resp.State = cty.ObjectVal(cfg) 1281 return resp 1282 } 1283 1284 otherProvider := &MockProvider{ 1285 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 1286 Provider: providers.Schema{ 1287 Block: &configschema.Block{ 1288 Attributes: map[string]*configschema.Attribute{ 1289 "output": { 1290 Type: cty.List(cty.String), 1291 Optional: true, 1292 }, 1293 "local": { 1294 Type: cty.List(cty.String), 1295 Optional: true, 1296 }, 1297 "var": { 1298 Type: cty.Map(cty.String), 1299 Optional: true, 1300 }, 1301 }, 1302 }, 1303 }, 1304 ResourceTypes: map[string]providers.Schema{ 1305 "other_object": providers.Schema{Block: simpleTestSchema()}, 1306 }, 1307 }, 1308 } 1309 1310 ctx := testContext2(t, &ContextOpts{ 1311 Providers: map[addrs.Provider]providers.Factory{ 1312 addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider), 1313 addrs.NewDefaultProvider("other"): testProviderFuncFixed(otherProvider), 1314 }, 1315 }) 1316 1317 opts := SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)) 1318 plan, diags := ctx.Plan(m, states.NewState(), opts) 1319 assertNoErrors(t, diags) 1320 1321 state, diags := ctx.Apply(plan, m) 1322 assertNoErrors(t, diags) 1323 1324 // Resource changes which have dependencies across providers which 1325 // themselves depend on resources can result in cycles. 1326 // Because other_object transitively depends on the module resources 1327 // through its provider, we trigger changes on both sides of this boundary 1328 // to ensure we can create a valid plan. 1329 // 1330 // Taint the object to make sure a replacement works in the plan. 1331 otherObjAddr := mustResourceInstanceAddr("other_object.other") 1332 otherObj := state.ResourceInstance(otherObjAddr) 1333 otherObj.Current.Status = states.ObjectTainted 1334 // Force a change which needs to be reverted. 1335 testObjAddr := mustResourceInstanceAddr(`module.mod["a"].test_object.a`) 1336 testObjA := state.ResourceInstance(testObjAddr) 1337 testObjA.Current.AttrsJSON = []byte(`{"test_bool":null,"test_list":null,"test_map":null,"test_number":null,"test_string":"changed"}`) 1338 1339 _, diags = ctx.Plan(m, state, opts) 1340 assertNoErrors(t, diags) 1341 return 1342 // TODO: unreachable code 1343 otherProvider.ConfigureProviderCalled = false 1344 otherProvider.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 1345 // check that our config is complete, even during a destroy plan 1346 expected := cty.ObjectVal(map[string]cty.Value{ 1347 "local": cty.ListVal([]cty.Value{cty.StringVal("first-ok"), cty.StringVal("second-ok")}), 1348 "output": cty.ListVal([]cty.Value{cty.StringVal("first-ok"), cty.StringVal("second-ok")}), 1349 "var": cty.MapVal(map[string]cty.Value{ 1350 "a": cty.StringVal("first"), 1351 "b": cty.StringVal("second"), 1352 }), 1353 }) 1354 1355 if !req.Config.RawEquals(expected) { 1356 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf( 1357 `incorrect provider config: 1358 expected: %#v 1359 got: %#v`, 1360 expected, req.Config)) 1361 } 1362 1363 return resp 1364 } 1365 1366 opts.Mode = plans.DestroyMode 1367 // skip refresh so that we don't configure the provider before the destroy plan 1368 opts.SkipRefresh = true 1369 1370 // destroy only a single instance not included in the moved statements 1371 _, diags = ctx.Plan(m, state, opts) 1372 assertNoErrors(t, diags) 1373 1374 if !otherProvider.ConfigureProviderCalled { 1375 t.Fatal("failed to configure provider during destroy plan") 1376 } 1377 } 1378 1379 // check that a provider can verify a planned destroy 1380 func TestContext2Apply_plannedDestroy(t *testing.T) { 1381 m := testModuleInline(t, map[string]string{ 1382 "main.tf": ` 1383 resource "test_object" "x" { 1384 test_string = "ok" 1385 }`, 1386 }) 1387 1388 p := simpleMockProvider() 1389 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 1390 if !req.ProposedNewState.IsNull() { 1391 // we should only be destroying in this test 1392 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unexpected plan with %#v", req.ProposedNewState)) 1393 return resp 1394 } 1395 1396 resp.PlannedState = req.ProposedNewState 1397 // we're going to verify the destroy plan by inserting private data required for destroy 1398 resp.PlannedPrivate = append(resp.PlannedPrivate, []byte("planned")...) 1399 return resp 1400 } 1401 1402 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 1403 // if the value is nil, we return that directly to correspond to a delete 1404 if !req.PlannedState.IsNull() { 1405 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unexpected apply with %#v", req.PlannedState)) 1406 return resp 1407 } 1408 1409 resp.NewState = req.PlannedState 1410 1411 // make sure we get our private data from the plan 1412 private := string(req.PlannedPrivate) 1413 if private != "planned" { 1414 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing private data from plan, got %q", private)) 1415 } 1416 return resp 1417 } 1418 1419 state := states.NewState() 1420 root := state.EnsureModule(addrs.RootModuleInstance) 1421 root.SetResourceInstanceCurrent( 1422 mustResourceInstanceAddr("test_object.x").Resource, 1423 &states.ResourceInstanceObjectSrc{ 1424 Status: states.ObjectReady, 1425 AttrsJSON: []byte(`{"test_string":"ok"}`), 1426 }, 1427 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`), 1428 ) 1429 1430 ctx := testContext2(t, &ContextOpts{ 1431 Providers: map[addrs.Provider]providers.Factory{ 1432 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1433 }, 1434 }) 1435 1436 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1437 Mode: plans.DestroyMode, 1438 // we don't want to refresh, because that actually runs a normal plan 1439 SkipRefresh: true, 1440 }) 1441 if diags.HasErrors() { 1442 t.Fatalf("plan: %s", diags.Err()) 1443 } 1444 1445 _, diags = ctx.Apply(plan, m) 1446 if diags.HasErrors() { 1447 t.Fatalf("apply: %s", diags.Err()) 1448 } 1449 } 1450 1451 func TestContext2Apply_missingOrphanedResource(t *testing.T) { 1452 m := testModuleInline(t, map[string]string{ 1453 "main.tf": ` 1454 # changed resource address to create a new object 1455 resource "test_object" "y" { 1456 test_string = "y" 1457 } 1458 `, 1459 }) 1460 1461 p := simpleMockProvider() 1462 1463 // report the prior value is missing 1464 p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { 1465 resp.NewState = cty.NullVal(req.PriorState.Type()) 1466 return resp 1467 } 1468 1469 state := states.NewState() 1470 root := state.EnsureModule(addrs.RootModuleInstance) 1471 root.SetResourceInstanceCurrent( 1472 mustResourceInstanceAddr("test_object.x").Resource, 1473 &states.ResourceInstanceObjectSrc{ 1474 Status: states.ObjectReady, 1475 AttrsJSON: []byte(`{"test_string":"x"}`), 1476 }, 1477 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`), 1478 ) 1479 1480 ctx := testContext2(t, &ContextOpts{ 1481 Providers: map[addrs.Provider]providers.Factory{ 1482 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1483 }, 1484 }) 1485 1486 opts := SimplePlanOpts(plans.NormalMode, nil) 1487 plan, diags := ctx.Plan(m, state, opts) 1488 assertNoErrors(t, diags) 1489 1490 _, diags = ctx.Apply(plan, m) 1491 assertNoErrors(t, diags) 1492 } 1493 1494 // Outputs should not cause evaluation errors during destroy 1495 // Check eval from both root level outputs and module outputs, which are 1496 // handled differently during apply. 1497 func TestContext2Apply_outputsNotToEvaluate(t *testing.T) { 1498 m := testModuleInline(t, map[string]string{ 1499 "main.tf": ` 1500 module "mod" { 1501 source = "./mod" 1502 cond = false 1503 } 1504 1505 output "from_resource" { 1506 value = module.mod.from_resource 1507 } 1508 1509 output "from_data" { 1510 value = module.mod.from_data 1511 } 1512 `, 1513 1514 "./mod/main.tf": ` 1515 variable "cond" { 1516 type = bool 1517 } 1518 1519 module "mod" { 1520 source = "../mod2/" 1521 cond = var.cond 1522 } 1523 1524 output "from_resource" { 1525 value = module.mod.resource 1526 } 1527 1528 output "from_data" { 1529 value = module.mod.data 1530 } 1531 `, 1532 1533 "./mod2/main.tf": ` 1534 variable "cond" { 1535 type = bool 1536 } 1537 1538 resource "test_object" "x" { 1539 count = var.cond ? 0:1 1540 } 1541 1542 data "test_object" "d" { 1543 count = var.cond ? 0:1 1544 } 1545 1546 output "resource" { 1547 value = var.cond ? null : test_object.x.*.test_string[0] 1548 } 1549 1550 output "data" { 1551 value = one(data.test_object.d[*].test_string) 1552 } 1553 `}) 1554 1555 p := simpleMockProvider() 1556 p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 1557 resp.State = req.Config 1558 return resp 1559 } 1560 1561 ctx := testContext2(t, &ContextOpts{ 1562 Providers: map[addrs.Provider]providers.Factory{ 1563 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1564 }, 1565 }) 1566 1567 // apply the state 1568 opts := SimplePlanOpts(plans.NormalMode, nil) 1569 plan, diags := ctx.Plan(m, states.NewState(), opts) 1570 assertNoErrors(t, diags) 1571 1572 state, diags := ctx.Apply(plan, m) 1573 assertNoErrors(t, diags) 1574 1575 // and destroy 1576 opts = SimplePlanOpts(plans.DestroyMode, nil) 1577 plan, diags = ctx.Plan(m, state, opts) 1578 assertNoErrors(t, diags) 1579 1580 state, diags = ctx.Apply(plan, m) 1581 assertNoErrors(t, diags) 1582 1583 // and destroy again with no state 1584 if !state.Empty() { 1585 t.Fatal("expected empty state, got", state) 1586 } 1587 1588 opts = SimplePlanOpts(plans.DestroyMode, nil) 1589 plan, diags = ctx.Plan(m, state, opts) 1590 assertNoErrors(t, diags) 1591 1592 _, diags = ctx.Apply(plan, m) 1593 assertNoErrors(t, diags) 1594 } 1595 1596 // don't evaluate conditions on outputs when destroying 1597 func TestContext2Apply_noOutputChecksOnDestroy(t *testing.T) { 1598 m := testModuleInline(t, map[string]string{ 1599 "main.tf": ` 1600 module "mod" { 1601 source = "./mod" 1602 } 1603 1604 output "from_resource" { 1605 value = module.mod.from_resource 1606 } 1607 `, 1608 1609 "./mod/main.tf": ` 1610 resource "test_object" "x" { 1611 test_string = "wrong val" 1612 } 1613 1614 output "from_resource" { 1615 value = test_object.x.test_string 1616 precondition { 1617 condition = test_object.x.test_string == "ok" 1618 error_message = "resource error" 1619 } 1620 } 1621 `}) 1622 1623 p := simpleMockProvider() 1624 1625 state := states.NewState() 1626 mod := state.EnsureModule(addrs.RootModuleInstance.Child("mod", addrs.NoKey)) 1627 mod.SetResourceInstanceCurrent( 1628 mustResourceInstanceAddr("test_object.x").Resource, 1629 &states.ResourceInstanceObjectSrc{ 1630 Status: states.ObjectReady, 1631 AttrsJSON: []byte(`{"test_string":"wrong_val"}`), 1632 }, 1633 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`), 1634 ) 1635 1636 ctx := testContext2(t, &ContextOpts{ 1637 Providers: map[addrs.Provider]providers.Factory{ 1638 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1639 }, 1640 }) 1641 1642 opts := SimplePlanOpts(plans.DestroyMode, nil) 1643 plan, diags := ctx.Plan(m, state, opts) 1644 assertNoErrors(t, diags) 1645 1646 _, diags = ctx.Apply(plan, m) 1647 assertNoErrors(t, diags) 1648 } 1649 1650 // -refresh-only should update checks 1651 func TestContext2Apply_refreshApplyUpdatesChecks(t *testing.T) { 1652 m := testModuleInline(t, map[string]string{ 1653 "main.tf": ` 1654 resource "test_object" "x" { 1655 test_string = "ok" 1656 lifecycle { 1657 postcondition { 1658 condition = self.test_string == "ok" 1659 error_message = "wrong val" 1660 } 1661 } 1662 } 1663 1664 output "from_resource" { 1665 value = test_object.x.test_string 1666 precondition { 1667 condition = test_object.x.test_string == "ok" 1668 error_message = "wrong val" 1669 } 1670 } 1671 `}) 1672 1673 p := simpleMockProvider() 1674 p.ReadResourceResponse = &providers.ReadResourceResponse{ 1675 NewState: cty.ObjectVal(map[string]cty.Value{ 1676 "test_string": cty.StringVal("ok"), 1677 }), 1678 } 1679 1680 state := states.NewState() 1681 mod := state.EnsureModule(addrs.RootModuleInstance) 1682 mod.SetResourceInstanceCurrent( 1683 mustResourceInstanceAddr("test_object.x").Resource, 1684 &states.ResourceInstanceObjectSrc{ 1685 Status: states.ObjectReady, 1686 AttrsJSON: []byte(`{"test_string":"wrong val"}`), 1687 }, 1688 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`), 1689 ) 1690 mod.SetOutputValue("from_resource", cty.StringVal("wrong val"), false) 1691 1692 ctx := testContext2(t, &ContextOpts{ 1693 Providers: map[addrs.Provider]providers.Factory{ 1694 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1695 }, 1696 }) 1697 1698 opts := SimplePlanOpts(plans.RefreshOnlyMode, nil) 1699 plan, diags := ctx.Plan(m, state, opts) 1700 assertNoErrors(t, diags) 1701 1702 state, diags = ctx.Apply(plan, m) 1703 assertNoErrors(t, diags) 1704 1705 resCheck := state.CheckResults.GetObjectResult(mustResourceInstanceAddr("test_object.x")) 1706 if resCheck.Status != checks.StatusPass { 1707 t.Fatalf("unexpected check %s: %s\n", resCheck.Status, resCheck.FailureMessages) 1708 } 1709 1710 outAddr := addrs.AbsOutputValue{ 1711 Module: addrs.RootModuleInstance, 1712 OutputValue: addrs.OutputValue{ 1713 Name: "from_resource", 1714 }, 1715 } 1716 outCheck := state.CheckResults.GetObjectResult(outAddr) 1717 if outCheck.Status != checks.StatusPass { 1718 t.Fatalf("unexpected check %s: %s\n", outCheck.Status, outCheck.FailureMessages) 1719 } 1720 } 1721 1722 // NoOp changes may have conditions to evaluate, but should not re-plan and 1723 // apply the entire resource. 1724 func TestContext2Apply_noRePlanNoOp(t *testing.T) { 1725 m := testModuleInline(t, map[string]string{ 1726 "main.tf": ` 1727 resource "test_object" "x" { 1728 } 1729 1730 resource "test_object" "y" { 1731 # test_object.w is being re-created, so this precondition must be evaluated 1732 # during apply, however this resource should otherwise be a NoOp. 1733 lifecycle { 1734 precondition { 1735 condition = test_object.x.test_string == null 1736 error_message = "test_object.x.test_string should be null" 1737 } 1738 } 1739 } 1740 `}) 1741 1742 p := simpleMockProvider() 1743 // make sure we can compute the attr 1744 testString := p.GetProviderSchemaResponse.ResourceTypes["test_object"].Block.Attributes["test_string"] 1745 testString.Computed = true 1746 testString.Optional = false 1747 1748 yAddr := mustResourceInstanceAddr("test_object.y") 1749 1750 state := states.NewState() 1751 mod := state.RootModule() 1752 mod.SetResourceInstanceCurrent( 1753 yAddr.Resource, 1754 &states.ResourceInstanceObjectSrc{ 1755 Status: states.ObjectReady, 1756 AttrsJSON: []byte(`{"test_string":"y"}`), 1757 }, 1758 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`), 1759 ) 1760 1761 ctx := testContext2(t, &ContextOpts{ 1762 Providers: map[addrs.Provider]providers.Factory{ 1763 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1764 }, 1765 }) 1766 1767 opts := SimplePlanOpts(plans.NormalMode, nil) 1768 plan, diags := ctx.Plan(m, state, opts) 1769 assertNoErrors(t, diags) 1770 1771 for _, c := range plan.Changes.Resources { 1772 if c.Addr.Equal(yAddr) && c.Action != plans.NoOp { 1773 t.Fatalf("unexpected %s change for test_object.y", c.Action) 1774 } 1775 } 1776 1777 // test_object.y is a NoOp change from the plan, but is included in the 1778 // graph due to the conditions which must be evaluated. This however should 1779 // not cause the resource to be re-planned. 1780 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 1781 testString := req.ProposedNewState.GetAttr("test_string") 1782 if !testString.IsNull() && testString.AsString() == "y" { 1783 resp.Diagnostics = resp.Diagnostics.Append(errors.New("Unexpected apply-time plan for test_object.y. Original plan was a NoOp")) 1784 } 1785 resp.PlannedState = req.ProposedNewState 1786 return resp 1787 } 1788 1789 _, diags = ctx.Apply(plan, m) 1790 assertNoErrors(t, diags) 1791 } 1792 1793 // ensure all references from preconditions are tracked through plan and apply 1794 func TestContext2Apply_preconditionErrorMessageRef(t *testing.T) { 1795 p := testProvider("test") 1796 ctx := testContext2(t, &ContextOpts{ 1797 Providers: map[addrs.Provider]providers.Factory{ 1798 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1799 }, 1800 }) 1801 1802 m := testModuleInline(t, map[string]string{ 1803 "main.tf": ` 1804 module "nested" { 1805 source = "./mod" 1806 } 1807 1808 output "nested_a" { 1809 value = module.nested.a 1810 } 1811 `, 1812 1813 "mod/main.tf": ` 1814 variable "boop" { 1815 default = "boop" 1816 } 1817 1818 variable "msg" { 1819 default = "Incorrect boop." 1820 } 1821 1822 output "a" { 1823 value = "x" 1824 1825 precondition { 1826 condition = var.boop == "boop" 1827 error_message = var.msg 1828 } 1829 } 1830 `, 1831 }) 1832 1833 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 1834 Mode: plans.NormalMode, 1835 }) 1836 assertNoErrors(t, diags) 1837 _, diags = ctx.Apply(plan, m) 1838 assertNoErrors(t, diags) 1839 } 1840 1841 func TestContext2Apply_destroyNullModuleOutput(t *testing.T) { 1842 p := testProvider("test") 1843 ctx := testContext2(t, &ContextOpts{ 1844 Providers: map[addrs.Provider]providers.Factory{ 1845 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1846 }, 1847 }) 1848 1849 m := testModuleInline(t, map[string]string{ 1850 "main.tf": ` 1851 module "null_module" { 1852 source = "./mod" 1853 } 1854 1855 locals { 1856 module_output = module.null_module.null_module_test 1857 } 1858 1859 output "test_root" { 1860 value = module.null_module.test_output 1861 } 1862 1863 output "root_module" { 1864 value = local.module_output #fails 1865 } 1866 `, 1867 1868 "mod/main.tf": ` 1869 output "test_output" { 1870 value = "test" 1871 } 1872 1873 output "null_module_test" { 1874 value = null 1875 } 1876 `, 1877 }) 1878 1879 // verify plan and apply 1880 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 1881 Mode: plans.NormalMode, 1882 }) 1883 assertNoErrors(t, diags) 1884 state, diags := ctx.Apply(plan, m) 1885 assertNoErrors(t, diags) 1886 1887 // now destroy 1888 plan, diags = ctx.Plan(m, state, &PlanOpts{ 1889 Mode: plans.DestroyMode, 1890 }) 1891 assertNoErrors(t, diags) 1892 _, diags = ctx.Apply(plan, m) 1893 assertNoErrors(t, diags) 1894 } 1895 1896 func TestContext2Apply_moduleOutputWithSensitiveAttrs(t *testing.T) { 1897 // Ensure that nested sensitive marks are stored when accessing non-root 1898 // module outputs, and that they do not cause the entire output value to 1899 // become sensitive. 1900 m := testModuleInline(t, map[string]string{ 1901 "main.tf": ` 1902 module "mod" { 1903 source = "./mod" 1904 } 1905 1906 resource "test_resource" "b" { 1907 // if the module output were wholly sensitive it would not be valid to use in 1908 // for_each 1909 for_each = module.mod.resources 1910 value = each.value.output 1911 } 1912 1913 output "root_output" { 1914 // The root output cannot contain any sensitive marks at all. 1915 // Applying nonsensitive would fail here if the nested sensitive mark were 1916 // not maintained through the output. 1917 value = [ for k, v in module.mod.resources : nonsensitive(v.output) ] 1918 } 1919 `, 1920 "./mod/main.tf": ` 1921 resource "test_resource" "a" { 1922 for_each = {"key": "value"} 1923 value = each.key 1924 } 1925 1926 output "resources" { 1927 value = test_resource.a 1928 } 1929 `, 1930 }) 1931 1932 p := testProvider("test") 1933 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 1934 ResourceTypes: map[string]*configschema.Block{ 1935 "test_resource": { 1936 Attributes: map[string]*configschema.Attribute{ 1937 "value": { 1938 Type: cty.String, 1939 Required: true, 1940 }, 1941 "output": { 1942 Type: cty.String, 1943 Sensitive: true, 1944 Computed: true, 1945 }, 1946 }, 1947 }, 1948 }, 1949 }) 1950 ctx := testContext2(t, &ContextOpts{ 1951 Providers: map[addrs.Provider]providers.Factory{ 1952 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1953 }, 1954 }) 1955 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 1956 Mode: plans.NormalMode, 1957 }) 1958 assertNoErrors(t, diags) 1959 _, diags = ctx.Apply(plan, m) 1960 assertNoErrors(t, diags) 1961 } 1962 1963 func TestContext2Apply_timestamps(t *testing.T) { 1964 m := testModuleInline(t, map[string]string{ 1965 "main.tf": ` 1966 resource "test_resource" "a" { 1967 id = "timestamp" 1968 value = timestamp() 1969 } 1970 1971 resource "test_resource" "b" { 1972 id = "plantimestamp" 1973 value = plantimestamp() 1974 } 1975 `, 1976 }) 1977 1978 var plantime time.Time 1979 1980 p := testProvider("test") 1981 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 1982 ResourceTypes: map[string]*configschema.Block{ 1983 "test_resource": { 1984 Attributes: map[string]*configschema.Attribute{ 1985 "id": { 1986 Type: cty.String, 1987 Required: true, 1988 }, 1989 "value": { 1990 Type: cty.String, 1991 Required: true, 1992 }, 1993 }, 1994 }, 1995 }, 1996 }) 1997 p.PlanResourceChangeFn = func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1998 values := request.ProposedNewState.AsValueMap() 1999 if id := values["id"]; id.AsString() == "plantimestamp" { 2000 var err error 2001 plantime, err = time.Parse(time.RFC3339, values["value"].AsString()) 2002 if err != nil { 2003 t.Errorf("couldn't parse plan time: %s", err) 2004 } 2005 } 2006 2007 return providers.PlanResourceChangeResponse{ 2008 PlannedState: request.ProposedNewState, 2009 } 2010 } 2011 p.ApplyResourceChangeFn = func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { 2012 values := request.PlannedState.AsValueMap() 2013 if id := values["id"]; id.AsString() == "timestamp" { 2014 applytime, err := time.Parse(time.RFC3339, values["value"].AsString()) 2015 if err != nil { 2016 t.Errorf("couldn't parse apply time: %s", err) 2017 } 2018 2019 if applytime.Before(plantime) { 2020 t.Errorf("applytime (%s) should be after plantime (%s)", applytime.Format(time.RFC3339), plantime.Format(time.RFC3339)) 2021 } 2022 } else if id.AsString() == "plantimestamp" { 2023 otherplantime, err := time.Parse(time.RFC3339, values["value"].AsString()) 2024 if err != nil { 2025 t.Errorf("couldn't parse plan time: %s", err) 2026 } 2027 2028 if !plantime.Equal(otherplantime) { 2029 t.Errorf("plantime changed from (%s) to (%s) during apply", plantime.Format(time.RFC3339), otherplantime.Format(time.RFC3339)) 2030 } 2031 } 2032 2033 return providers.ApplyResourceChangeResponse{ 2034 NewState: request.PlannedState, 2035 } 2036 } 2037 ctx := testContext2(t, &ContextOpts{ 2038 Providers: map[addrs.Provider]providers.Factory{ 2039 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2040 }, 2041 }) 2042 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2043 Mode: plans.NormalMode, 2044 }) 2045 assertNoErrors(t, diags) 2046 2047 _, diags = ctx.Apply(plan, m) 2048 assertNoErrors(t, diags) 2049 } 2050 2051 func TestContext2Apply_destroyUnusedModuleProvider(t *testing.T) { 2052 // an unsued provider within a module should not be called during destroy 2053 unusedProvider := testProvider("unused") 2054 testProvider := testProvider("test") 2055 ctx := testContext2(t, &ContextOpts{ 2056 Providers: map[addrs.Provider]providers.Factory{ 2057 addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider), 2058 addrs.NewDefaultProvider("unused"): testProviderFuncFixed(unusedProvider), 2059 }, 2060 }) 2061 2062 unusedProvider.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 2063 resp.Diagnostics = resp.Diagnostics.Append(errors.New("configuration failed")) 2064 return resp 2065 } 2066 2067 m := testModuleInline(t, map[string]string{ 2068 "main.tf": ` 2069 module "mod" { 2070 source = "./mod" 2071 } 2072 2073 resource "test_resource" "test" { 2074 } 2075 `, 2076 2077 "mod/main.tf": ` 2078 provider "unused" { 2079 } 2080 2081 resource "unused_resource" "test" { 2082 } 2083 `, 2084 }) 2085 2086 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2087 Mode: plans.DestroyMode, 2088 }) 2089 assertNoErrors(t, diags) 2090 _, diags = ctx.Apply(plan, m) 2091 assertNoErrors(t, diags) 2092 } 2093 2094 func TestContext2Apply_import(t *testing.T) { 2095 m := testModuleInline(t, map[string]string{ 2096 "main.tf": ` 2097 resource "test_resource" "a" { 2098 id = "importable" 2099 } 2100 2101 import { 2102 to = test_resource.a 2103 id = "importable" 2104 } 2105 `, 2106 }) 2107 2108 p := testProvider("test") 2109 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 2110 ResourceTypes: map[string]*configschema.Block{ 2111 "test_resource": { 2112 Attributes: map[string]*configschema.Attribute{ 2113 "id": { 2114 Type: cty.String, 2115 Required: true, 2116 }, 2117 }, 2118 }, 2119 }, 2120 }) 2121 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 2122 return providers.PlanResourceChangeResponse{ 2123 PlannedState: req.ProposedNewState, 2124 } 2125 } 2126 p.ImportResourceStateFn = func(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { 2127 return providers.ImportResourceStateResponse{ 2128 ImportedResources: []providers.ImportedResource{ 2129 { 2130 TypeName: "test_instance", 2131 State: cty.ObjectVal(map[string]cty.Value{ 2132 "id": cty.StringVal("importable"), 2133 }), 2134 }, 2135 }, 2136 } 2137 } 2138 hook := new(MockHook) 2139 ctx := testContext2(t, &ContextOpts{ 2140 Hooks: []Hook{hook}, 2141 Providers: map[addrs.Provider]providers.Factory{ 2142 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2143 }, 2144 }) 2145 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2146 Mode: plans.NormalMode, 2147 }) 2148 assertNoErrors(t, diags) 2149 2150 _, diags = ctx.Apply(plan, m) 2151 assertNoErrors(t, diags) 2152 2153 if !hook.PreApplyImportCalled { 2154 t.Fatalf("PreApplyImport hook not called") 2155 } 2156 if addr, wantAddr := hook.PreApplyImportAddr, mustResourceInstanceAddr("test_resource.a"); !addr.Equal(wantAddr) { 2157 t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) 2158 } 2159 2160 if !hook.PostApplyImportCalled { 2161 t.Fatalf("PostApplyImport hook not called") 2162 } 2163 if addr, wantAddr := hook.PostApplyImportAddr, mustResourceInstanceAddr("test_resource.a"); !addr.Equal(wantAddr) { 2164 t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) 2165 } 2166 } 2167 2168 func TestContext2Apply_noExternalReferences(t *testing.T) { 2169 m := testModuleInline(t, map[string]string{ 2170 "main.tf": ` 2171 resource "test_object" "a" { 2172 test_string = "foo" 2173 } 2174 2175 locals { 2176 local_value = test_object.a.test_string 2177 } 2178 `, 2179 }) 2180 2181 p := simpleMockProvider() 2182 ctx := testContext2(t, &ContextOpts{ 2183 Providers: map[addrs.Provider]providers.Factory{ 2184 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2185 }, 2186 }) 2187 2188 plan, diags := ctx.Plan(m, states.NewState(), nil) 2189 if diags.HasErrors() { 2190 t.Errorf("expected no errors, but got %s", diags) 2191 } 2192 2193 state, diags := ctx.Apply(plan, m) 2194 if diags.HasErrors() { 2195 t.Errorf("expected no errors, but got %s", diags) 2196 } 2197 2198 // We didn't specify any external references, so the unreferenced local 2199 // value should have been tidied up and never made it into the state. 2200 module := state.RootModule() 2201 if len(module.LocalValues) > 0 { 2202 t.Errorf("expected no local values in the state but found %d", len(module.LocalValues)) 2203 } 2204 } 2205 2206 func TestContext2Apply_withExternalReferences(t *testing.T) { 2207 m := testModuleInline(t, map[string]string{ 2208 "main.tf": ` 2209 resource "test_object" "a" { 2210 test_string = "foo" 2211 } 2212 2213 locals { 2214 local_value = test_object.a.test_string 2215 } 2216 `, 2217 }) 2218 2219 p := simpleMockProvider() 2220 ctx := testContext2(t, &ContextOpts{ 2221 Providers: map[addrs.Provider]providers.Factory{ 2222 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 2223 }, 2224 }) 2225 2226 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 2227 Mode: plans.NormalMode, 2228 ExternalReferences: []*addrs.Reference{ 2229 mustReference("local.local_value"), 2230 }, 2231 }) 2232 if diags.HasErrors() { 2233 t.Errorf("expected no errors, but got %s", diags) 2234 } 2235 2236 state, diags := ctx.Apply(plan, m) 2237 if diags.HasErrors() { 2238 t.Errorf("expected no errors, but got %s", diags) 2239 } 2240 2241 // We did specify the local value in the external references, so it should 2242 // have been preserved even though it is not referenced by anything directly 2243 // in the config. 2244 module := state.RootModule() 2245 if module.LocalValues["local_value"].AsString() != "foo" { 2246 t.Errorf("expected local value to be \"foo\" but was \"%s\"", module.LocalValues["local_value"].AsString()) 2247 } 2248 } 2249 2250 func TestContext2Apply_forgetOrphanAndDeposed(t *testing.T) { 2251 desposedKey := states.DeposedKey("deposed") 2252 addr := "aws_instance.baz" 2253 m := testModuleInline(t, map[string]string{ 2254 "main.tf": ` 2255 removed { 2256 from = aws_instance.baz 2257 } 2258 `, 2259 }) 2260 hook := new(MockHook) 2261 p := testProvider("aws") 2262 state := states.NewState() 2263 root := state.EnsureModule(addrs.RootModuleInstance) 2264 root.SetResourceInstanceCurrent( 2265 mustResourceInstanceAddr(addr).Resource, 2266 &states.ResourceInstanceObjectSrc{ 2267 Status: states.ObjectReady, 2268 AttrsJSON: []byte(`{"id":"bar"}`), 2269 }, 2270 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 2271 ) 2272 root.SetResourceInstanceDeposed( 2273 mustResourceInstanceAddr(addr).Resource, 2274 desposedKey, 2275 &states.ResourceInstanceObjectSrc{ 2276 Status: states.ObjectTainted, 2277 AttrsJSON: []byte(`{"id":"bar"}`), 2278 Dependencies: []addrs.ConfigResource{}, 2279 }, 2280 mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`), 2281 ) 2282 ctx := testContext2(t, &ContextOpts{ 2283 Providers: map[addrs.Provider]providers.Factory{ 2284 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 2285 }, 2286 }) 2287 2288 p.PlanResourceChangeFn = testDiffFn 2289 2290 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 2291 assertNoErrors(t, diags) 2292 2293 s, diags := ctx.Apply(plan, m) 2294 if diags.HasErrors() { 2295 t.Fatalf("diags: %s", diags.Err()) 2296 } 2297 2298 if !s.Empty() { 2299 t.Fatalf("State should be empty") 2300 } 2301 2302 if p.ApplyResourceChangeCalled { 2303 t.Fatalf("When we forget we don't call the provider's ApplyResourceChange unlike in destroy") 2304 } 2305 2306 if hook.PostApplyCalled { 2307 t.Fatalf("PostApply hook should not be called as part of forget") 2308 } 2309 }