github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/context_apply2_test.go (about) 1 package terraform 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "strings" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/davecgh/go-spew/spew" 13 "github.com/google/go-cmp/cmp" 14 "github.com/zclconf/go-cty/cty" 15 16 "github.com/hashicorp/terraform/internal/addrs" 17 "github.com/hashicorp/terraform/internal/configs/configschema" 18 "github.com/hashicorp/terraform/internal/lang/marks" 19 "github.com/hashicorp/terraform/internal/plans" 20 "github.com/hashicorp/terraform/internal/providers" 21 "github.com/hashicorp/terraform/internal/states" 22 ) 23 24 // Test that the PreApply hook is called with the correct deposed key 25 func TestContext2Apply_createBeforeDestroy_deposedKeyPreApply(t *testing.T) { 26 m := testModule(t, "apply-cbd-deposed-only") 27 p := testProvider("aws") 28 p.PlanResourceChangeFn = testDiffFn 29 p.ApplyResourceChangeFn = testApplyFn 30 31 deposedKey := states.NewDeposedKey() 32 33 state := states.NewState() 34 root := state.EnsureModule(addrs.RootModuleInstance) 35 root.SetResourceInstanceCurrent( 36 mustResourceInstanceAddr("aws_instance.bar").Resource, 37 &states.ResourceInstanceObjectSrc{ 38 Status: states.ObjectReady, 39 AttrsJSON: []byte(`{"id":"bar"}`), 40 }, 41 mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), 42 ) 43 root.SetResourceInstanceDeposed( 44 mustResourceInstanceAddr("aws_instance.bar").Resource, 45 deposedKey, 46 &states.ResourceInstanceObjectSrc{ 47 Status: states.ObjectTainted, 48 AttrsJSON: []byte(`{"id":"foo"}`), 49 }, 50 mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), 51 ) 52 53 hook := new(MockHook) 54 ctx := testContext2(t, &ContextOpts{ 55 Hooks: []Hook{hook}, 56 Providers: map[addrs.Provider]providers.Factory{ 57 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 58 }, 59 }) 60 61 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 62 if diags.HasErrors() { 63 t.Fatalf("diags: %s", diags.Err()) 64 } else { 65 t.Logf(legacyDiffComparisonString(plan.Changes)) 66 } 67 68 _, diags = ctx.Apply(plan, m) 69 if diags.HasErrors() { 70 t.Fatalf("diags: %s", diags.Err()) 71 } 72 73 // Verify PreApply was called correctly 74 if !hook.PreApplyCalled { 75 t.Fatalf("PreApply hook not called") 76 } 77 if addr, wantAddr := hook.PreApplyAddr, mustResourceInstanceAddr("aws_instance.bar"); !addr.Equal(wantAddr) { 78 t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) 79 } 80 if gen := hook.PreApplyGen; gen != deposedKey { 81 t.Errorf("expected gen to be %q, but was %q", deposedKey, gen) 82 } 83 } 84 85 func TestContext2Apply_destroyWithDataSourceExpansion(t *testing.T) { 86 // While managed resources store their destroy-time dependencies, data 87 // sources do not. This means that if a provider were only included in a 88 // destroy graph because of data sources, it could have dependencies which 89 // are not correctly ordered. Here we verify that the provider is not 90 // included in the destroy operation, and all dependency evaluations 91 // succeed. 92 93 m := testModuleInline(t, map[string]string{ 94 "main.tf": ` 95 module "mod" { 96 source = "./mod" 97 } 98 99 provider "other" { 100 foo = module.mod.data 101 } 102 103 # this should not require the provider be present during destroy 104 data "other_data_source" "a" { 105 } 106 `, 107 108 "mod/main.tf": ` 109 data "test_data_source" "a" { 110 count = 1 111 } 112 113 data "test_data_source" "b" { 114 count = data.test_data_source.a[0].foo == "ok" ? 1 : 0 115 } 116 117 output "data" { 118 value = data.test_data_source.a[0].foo == "ok" ? data.test_data_source.b[0].foo : "nope" 119 } 120 `, 121 }) 122 123 testP := testProvider("test") 124 otherP := testProvider("other") 125 126 readData := func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { 127 return providers.ReadDataSourceResponse{ 128 State: cty.ObjectVal(map[string]cty.Value{ 129 "id": cty.StringVal("data_source"), 130 "foo": cty.StringVal("ok"), 131 }), 132 } 133 } 134 135 testP.ReadDataSourceFn = readData 136 otherP.ReadDataSourceFn = readData 137 138 ps := map[addrs.Provider]providers.Factory{ 139 addrs.NewDefaultProvider("test"): testProviderFuncFixed(testP), 140 addrs.NewDefaultProvider("other"): testProviderFuncFixed(otherP), 141 } 142 143 otherP.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 144 foo := req.Config.GetAttr("foo") 145 if foo.IsNull() || foo.AsString() != "ok" { 146 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("incorrect config val: %#v\n", foo)) 147 } 148 return resp 149 } 150 151 ctx := testContext2(t, &ContextOpts{ 152 Providers: ps, 153 }) 154 155 plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 156 if diags.HasErrors() { 157 t.Fatal(diags.Err()) 158 } 159 160 _, diags = ctx.Apply(plan, m) 161 if diags.HasErrors() { 162 t.Fatal(diags.Err()) 163 } 164 165 // now destroy the whole thing 166 ctx = testContext2(t, &ContextOpts{ 167 Providers: ps, 168 }) 169 170 plan, diags = ctx.Plan(m, states.NewState(), &PlanOpts{ 171 Mode: plans.DestroyMode, 172 }) 173 if diags.HasErrors() { 174 t.Fatal(diags.Err()) 175 } 176 177 otherP.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 178 // should not be used to destroy data sources 179 resp.Diagnostics = resp.Diagnostics.Append(errors.New("provider should not be used")) 180 return resp 181 } 182 183 _, diags = ctx.Apply(plan, m) 184 if diags.HasErrors() { 185 t.Fatal(diags.Err()) 186 } 187 } 188 189 func TestContext2Apply_destroyThenUpdate(t *testing.T) { 190 m := testModuleInline(t, map[string]string{ 191 "main.tf": ` 192 resource "test_instance" "a" { 193 value = "udpated" 194 } 195 `, 196 }) 197 198 p := testProvider("test") 199 p.PlanResourceChangeFn = testDiffFn 200 201 var orderMu sync.Mutex 202 var order []string 203 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 204 id := req.PriorState.GetAttr("id").AsString() 205 if id == "b" { 206 // slow down the b destroy, since a should wait for it 207 time.Sleep(100 * time.Millisecond) 208 } 209 210 orderMu.Lock() 211 order = append(order, id) 212 orderMu.Unlock() 213 214 resp.NewState = req.PlannedState 215 return resp 216 } 217 218 addrA := mustResourceInstanceAddr(`test_instance.a`) 219 addrB := mustResourceInstanceAddr(`test_instance.b`) 220 221 state := states.BuildState(func(s *states.SyncState) { 222 s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ 223 AttrsJSON: []byte(`{"id":"a","value":"old","type":"test"}`), 224 Status: states.ObjectReady, 225 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 226 227 // test_instance.b depended on test_instance.a, and therefor should be 228 // destroyed before any changes to test_instance.a 229 s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{ 230 AttrsJSON: []byte(`{"id":"b"}`), 231 Status: states.ObjectReady, 232 Dependencies: []addrs.ConfigResource{addrA.ContainingResource().Config()}, 233 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) 234 }) 235 236 ctx := testContext2(t, &ContextOpts{ 237 Providers: map[addrs.Provider]providers.Factory{ 238 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 239 }, 240 }) 241 242 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 243 assertNoErrors(t, diags) 244 245 _, diags = ctx.Apply(plan, m) 246 if diags.HasErrors() { 247 t.Fatal(diags.Err()) 248 } 249 250 if order[0] != "b" { 251 t.Fatalf("expected apply order [b, a], got: %v\n", order) 252 } 253 } 254 255 // verify that dependencies are updated in the state during refresh and apply 256 func TestApply_updateDependencies(t *testing.T) { 257 state := states.NewState() 258 root := state.EnsureModule(addrs.RootModuleInstance) 259 260 fooAddr := mustResourceInstanceAddr("aws_instance.foo") 261 barAddr := mustResourceInstanceAddr("aws_instance.bar") 262 bazAddr := mustResourceInstanceAddr("aws_instance.baz") 263 bamAddr := mustResourceInstanceAddr("aws_instance.bam") 264 binAddr := mustResourceInstanceAddr("aws_instance.bin") 265 root.SetResourceInstanceCurrent( 266 fooAddr.Resource, 267 &states.ResourceInstanceObjectSrc{ 268 Status: states.ObjectReady, 269 AttrsJSON: []byte(`{"id":"foo"}`), 270 Dependencies: []addrs.ConfigResource{ 271 bazAddr.ContainingResource().Config(), 272 }, 273 }, 274 mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), 275 ) 276 root.SetResourceInstanceCurrent( 277 binAddr.Resource, 278 &states.ResourceInstanceObjectSrc{ 279 Status: states.ObjectReady, 280 AttrsJSON: []byte(`{"id":"bin","type":"aws_instance","unknown":"ok"}`), 281 Dependencies: []addrs.ConfigResource{ 282 bazAddr.ContainingResource().Config(), 283 }, 284 }, 285 mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), 286 ) 287 root.SetResourceInstanceCurrent( 288 bazAddr.Resource, 289 &states.ResourceInstanceObjectSrc{ 290 Status: states.ObjectReady, 291 AttrsJSON: []byte(`{"id":"baz"}`), 292 Dependencies: []addrs.ConfigResource{ 293 // Existing dependencies should not be removed from orphaned instances 294 bamAddr.ContainingResource().Config(), 295 }, 296 }, 297 mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), 298 ) 299 root.SetResourceInstanceCurrent( 300 barAddr.Resource, 301 &states.ResourceInstanceObjectSrc{ 302 Status: states.ObjectReady, 303 AttrsJSON: []byte(`{"id":"bar","foo":"foo"}`), 304 }, 305 mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), 306 ) 307 308 m := testModuleInline(t, map[string]string{ 309 "main.tf": ` 310 resource "aws_instance" "bar" { 311 foo = aws_instance.foo.id 312 } 313 314 resource "aws_instance" "foo" { 315 } 316 317 resource "aws_instance" "bin" { 318 } 319 `, 320 }) 321 322 p := testProvider("aws") 323 324 ctx := testContext2(t, &ContextOpts{ 325 Providers: map[addrs.Provider]providers.Factory{ 326 addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), 327 }, 328 }) 329 330 plan, diags := ctx.Plan(m, state, DefaultPlanOpts) 331 assertNoErrors(t, diags) 332 333 bar := plan.PriorState.ResourceInstance(barAddr) 334 if len(bar.Current.Dependencies) == 0 || !bar.Current.Dependencies[0].Equal(fooAddr.ContainingResource().Config()) { 335 t.Fatalf("bar should depend on foo after refresh, but got %s", bar.Current.Dependencies) 336 } 337 338 foo := plan.PriorState.ResourceInstance(fooAddr) 339 if len(foo.Current.Dependencies) == 0 || !foo.Current.Dependencies[0].Equal(bazAddr.ContainingResource().Config()) { 340 t.Fatalf("foo should depend on baz after refresh because of the update, but got %s", foo.Current.Dependencies) 341 } 342 343 bin := plan.PriorState.ResourceInstance(binAddr) 344 if len(bin.Current.Dependencies) != 0 { 345 t.Fatalf("bin should depend on nothing after refresh because there is no change, but got %s", bin.Current.Dependencies) 346 } 347 348 baz := plan.PriorState.ResourceInstance(bazAddr) 349 if len(baz.Current.Dependencies) == 0 || !baz.Current.Dependencies[0].Equal(bamAddr.ContainingResource().Config()) { 350 t.Fatalf("baz should depend on bam after refresh, but got %s", baz.Current.Dependencies) 351 } 352 353 state, diags = ctx.Apply(plan, m) 354 if diags.HasErrors() { 355 t.Fatal(diags.Err()) 356 } 357 358 bar = state.ResourceInstance(barAddr) 359 if len(bar.Current.Dependencies) == 0 || !bar.Current.Dependencies[0].Equal(fooAddr.ContainingResource().Config()) { 360 t.Fatalf("bar should still depend on foo after apply, but got %s", bar.Current.Dependencies) 361 } 362 363 foo = state.ResourceInstance(fooAddr) 364 if len(foo.Current.Dependencies) != 0 { 365 t.Fatalf("foo should have no deps after apply, but got %s", foo.Current.Dependencies) 366 } 367 368 } 369 370 func TestContext2Apply_additionalSensitiveFromState(t *testing.T) { 371 // Ensure we're not trying to double-mark values decoded from state 372 m := testModuleInline(t, map[string]string{ 373 "main.tf": ` 374 variable "secret" { 375 sensitive = true 376 default = ["secret"] 377 } 378 379 resource "test_resource" "a" { 380 sensitive_attr = var.secret 381 } 382 383 resource "test_resource" "b" { 384 value = test_resource.a.id 385 } 386 `, 387 }) 388 389 p := new(MockProvider) 390 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 391 ResourceTypes: map[string]*configschema.Block{ 392 "test_resource": { 393 Attributes: map[string]*configschema.Attribute{ 394 "id": { 395 Type: cty.String, 396 Computed: true, 397 }, 398 "value": { 399 Type: cty.String, 400 Optional: true, 401 }, 402 "sensitive_attr": { 403 Type: cty.List(cty.String), 404 Optional: true, 405 Sensitive: true, 406 }, 407 }, 408 }, 409 }, 410 }) 411 412 state := states.BuildState(func(s *states.SyncState) { 413 s.SetResourceInstanceCurrent( 414 mustResourceInstanceAddr(`test_resource.a`), 415 &states.ResourceInstanceObjectSrc{ 416 AttrsJSON: []byte(`{"id":"a","sensitive_attr":["secret"]}`), 417 AttrSensitivePaths: []cty.PathValueMarks{ 418 { 419 Path: cty.GetAttrPath("sensitive_attr"), 420 Marks: cty.NewValueMarks(marks.Sensitive), 421 }, 422 }, 423 Status: states.ObjectReady, 424 }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 425 ) 426 }) 427 428 ctx := testContext2(t, &ContextOpts{ 429 Providers: map[addrs.Provider]providers.Factory{ 430 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 431 }, 432 }) 433 434 plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) 435 assertNoErrors(t, diags) 436 437 _, diags = ctx.Apply(plan, m) 438 if diags.HasErrors() { 439 t.Fatal(diags.ErrWithWarnings()) 440 } 441 } 442 443 func TestContext2Apply_sensitiveOutputPassthrough(t *testing.T) { 444 // Ensure we're not trying to double-mark values decoded from state 445 m := testModuleInline(t, map[string]string{ 446 "main.tf": ` 447 module "mod" { 448 source = "./mod" 449 } 450 451 resource "test_object" "a" { 452 test_string = module.mod.out 453 } 454 `, 455 456 "mod/main.tf": ` 457 variable "in" { 458 sensitive = true 459 default = "foo" 460 } 461 output "out" { 462 value = var.in 463 } 464 `, 465 }) 466 467 p := simpleMockProvider() 468 469 ctx := testContext2(t, &ContextOpts{ 470 Providers: map[addrs.Provider]providers.Factory{ 471 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 472 }, 473 }) 474 475 plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 476 assertNoErrors(t, diags) 477 478 state, diags := ctx.Apply(plan, m) 479 if diags.HasErrors() { 480 t.Fatal(diags.ErrWithWarnings()) 481 } 482 483 obj := state.ResourceInstance(mustResourceInstanceAddr("test_object.a")) 484 if len(obj.Current.AttrSensitivePaths) != 1 { 485 t.Fatalf("Expected 1 sensitive mark for test_object.a, got %#v\n", obj.Current.AttrSensitivePaths) 486 } 487 488 plan, diags = ctx.Plan(m, state, DefaultPlanOpts) 489 assertNoErrors(t, diags) 490 491 // make sure the same marks are compared in the next plan as well 492 for _, c := range plan.Changes.Resources { 493 if c.Action != plans.NoOp { 494 t.Errorf("Unexpcetd %s change for %s", c.Action, c.Addr) 495 } 496 } 497 } 498 499 func TestContext2Apply_ignoreImpureFunctionChanges(t *testing.T) { 500 // The impure function call should not cause a planned change with 501 // ignore_changes 502 m := testModuleInline(t, map[string]string{ 503 "main.tf": ` 504 variable "pw" { 505 sensitive = true 506 default = "foo" 507 } 508 509 resource "test_object" "x" { 510 test_map = { 511 string = "X${bcrypt(var.pw)}" 512 } 513 lifecycle { 514 ignore_changes = [ test_map["string"] ] 515 } 516 } 517 518 resource "test_object" "y" { 519 test_map = { 520 string = "X${bcrypt(var.pw)}" 521 } 522 lifecycle { 523 ignore_changes = [ test_map ] 524 } 525 } 526 527 `, 528 }) 529 530 p := simpleMockProvider() 531 532 ctx := testContext2(t, &ContextOpts{ 533 Providers: map[addrs.Provider]providers.Factory{ 534 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 535 }, 536 }) 537 538 plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) 539 assertNoErrors(t, diags) 540 541 state, diags := ctx.Apply(plan, m) 542 assertNoErrors(t, diags) 543 544 // FINAL PLAN: 545 plan, diags = ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) 546 assertNoErrors(t, diags) 547 548 // make sure the same marks are compared in the next plan as well 549 for _, c := range plan.Changes.Resources { 550 if c.Action != plans.NoOp { 551 t.Logf("marks before: %#v", c.BeforeValMarks) 552 t.Logf("marks after: %#v", c.AfterValMarks) 553 t.Errorf("Unexpcetd %s change for %s", c.Action, c.Addr) 554 } 555 } 556 } 557 558 func TestContext2Apply_destroyWithDeposed(t *testing.T) { 559 m := testModuleInline(t, map[string]string{ 560 "main.tf": ` 561 resource "test_object" "x" { 562 test_string = "ok" 563 lifecycle { 564 create_before_destroy = true 565 } 566 }`, 567 }) 568 569 p := simpleMockProvider() 570 571 deposedKey := states.NewDeposedKey() 572 573 state := states.NewState() 574 root := state.EnsureModule(addrs.RootModuleInstance) 575 root.SetResourceInstanceDeposed( 576 mustResourceInstanceAddr("test_object.x").Resource, 577 deposedKey, 578 &states.ResourceInstanceObjectSrc{ 579 Status: states.ObjectTainted, 580 AttrsJSON: []byte(`{"test_string":"deposed"}`), 581 }, 582 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 583 ) 584 585 ctx := testContext2(t, &ContextOpts{ 586 Providers: map[addrs.Provider]providers.Factory{ 587 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 588 }, 589 }) 590 591 plan, diags := ctx.Plan(m, state, &PlanOpts{ 592 Mode: plans.DestroyMode, 593 }) 594 if diags.HasErrors() { 595 t.Fatalf("plan: %s", diags.Err()) 596 } 597 598 _, diags = ctx.Apply(plan, m) 599 if diags.HasErrors() { 600 t.Fatalf("apply: %s", diags.Err()) 601 } 602 603 } 604 605 func TestContext2Apply_nullableVariables(t *testing.T) { 606 m := testModule(t, "apply-nullable-variables") 607 state := states.NewState() 608 ctx := testContext2(t, &ContextOpts{}) 609 plan, diags := ctx.Plan(m, state, &PlanOpts{}) 610 if diags.HasErrors() { 611 t.Fatalf("plan: %s", diags.Err()) 612 } 613 state, diags = ctx.Apply(plan, m) 614 if diags.HasErrors() { 615 t.Fatalf("apply: %s", diags.Err()) 616 } 617 618 outputs := state.Module(addrs.RootModuleInstance).OutputValues 619 // we check for null outputs be seeing that they don't exists 620 if _, ok := outputs["nullable_null_default"]; ok { 621 t.Error("nullable_null_default: expected no output value") 622 } 623 if _, ok := outputs["nullable_non_null_default"]; ok { 624 t.Error("nullable_non_null_default: expected no output value") 625 } 626 if _, ok := outputs["nullable_no_default"]; ok { 627 t.Error("nullable_no_default: expected no output value") 628 } 629 630 if v := outputs["non_nullable_default"].Value; v.AsString() != "ok" { 631 t.Fatalf("incorrect 'non_nullable_default' output value: %#v\n", v) 632 } 633 if v := outputs["non_nullable_no_default"].Value; v.AsString() != "ok" { 634 t.Fatalf("incorrect 'non_nullable_no_default' output value: %#v\n", v) 635 } 636 } 637 638 func TestContext2Apply_targetedDestroyWithMoved(t *testing.T) { 639 m := testModuleInline(t, map[string]string{ 640 "main.tf": ` 641 module "modb" { 642 source = "./mod" 643 for_each = toset(["a", "b"]) 644 } 645 `, 646 "./mod/main.tf": ` 647 resource "test_object" "a" { 648 } 649 650 module "sub" { 651 for_each = toset(["a", "b"]) 652 source = "./sub" 653 } 654 655 moved { 656 from = module.old 657 to = module.sub 658 } 659 `, 660 "./mod/sub/main.tf": ` 661 resource "test_object" "s" { 662 } 663 `}) 664 665 p := simpleMockProvider() 666 667 ctx := testContext2(t, &ContextOpts{ 668 Providers: map[addrs.Provider]providers.Factory{ 669 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 670 }, 671 }) 672 673 plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) 674 assertNoErrors(t, diags) 675 676 state, diags := ctx.Apply(plan, m) 677 assertNoErrors(t, diags) 678 679 // destroy only a single instance not included in the moved statements 680 _, diags = ctx.Plan(m, state, &PlanOpts{ 681 Mode: plans.DestroyMode, 682 Targets: []addrs.Targetable{mustResourceInstanceAddr(`module.modb["a"].test_object.a`)}, 683 }) 684 assertNoErrors(t, diags) 685 } 686 687 func TestContext2Apply_graphError(t *testing.T) { 688 m := testModuleInline(t, map[string]string{ 689 "main.tf": ` 690 resource "test_object" "a" { 691 test_string = "ok" 692 } 693 694 resource "test_object" "b" { 695 test_string = test_object.a.test_string 696 } 697 `, 698 }) 699 700 p := simpleMockProvider() 701 702 state := states.NewState() 703 root := state.EnsureModule(addrs.RootModuleInstance) 704 root.SetResourceInstanceCurrent( 705 mustResourceInstanceAddr("test_object.a").Resource, 706 &states.ResourceInstanceObjectSrc{ 707 Status: states.ObjectTainted, 708 AttrsJSON: []byte(`{"test_string":"ok"}`), 709 }, 710 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 711 ) 712 root.SetResourceInstanceCurrent( 713 mustResourceInstanceAddr("test_object.b").Resource, 714 &states.ResourceInstanceObjectSrc{ 715 Status: states.ObjectTainted, 716 AttrsJSON: []byte(`{"test_string":"ok"}`), 717 }, 718 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 719 ) 720 721 ctx := testContext2(t, &ContextOpts{ 722 Providers: map[addrs.Provider]providers.Factory{ 723 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 724 }, 725 }) 726 727 plan, diags := ctx.Plan(m, state, &PlanOpts{ 728 Mode: plans.DestroyMode, 729 }) 730 if diags.HasErrors() { 731 t.Fatalf("plan: %s", diags.Err()) 732 } 733 734 // We're going to corrupt the stored state so that the dependencies will 735 // cause a cycle when building the apply graph. 736 testObjA := plan.PriorState.Modules[""].Resources["test_object.a"].Instances[addrs.NoKey].Current 737 testObjA.Dependencies = append(testObjA.Dependencies, mustResourceInstanceAddr("test_object.b").ContainingResource().Config()) 738 739 _, diags = ctx.Apply(plan, m) 740 if !diags.HasErrors() { 741 t.Fatal("expected cycle error from apply") 742 } 743 } 744 745 func TestContext2Apply_resourcePostcondition(t *testing.T) { 746 m := testModuleInline(t, map[string]string{ 747 "main.tf": ` 748 variable "boop" { 749 type = string 750 } 751 752 resource "test_resource" "a" { 753 value = var.boop 754 } 755 756 resource "test_resource" "b" { 757 value = test_resource.a.output 758 lifecycle { 759 postcondition { 760 condition = self.output != "" 761 error_message = "Output must not be blank." 762 } 763 } 764 } 765 766 resource "test_resource" "c" { 767 value = test_resource.b.output 768 } 769 `, 770 }) 771 772 p := testProvider("test") 773 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 774 ResourceTypes: map[string]*configschema.Block{ 775 "test_resource": { 776 Attributes: map[string]*configschema.Attribute{ 777 "value": { 778 Type: cty.String, 779 Required: true, 780 }, 781 "output": { 782 Type: cty.String, 783 Computed: true, 784 }, 785 }, 786 }, 787 }, 788 }) 789 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 790 m := req.ProposedNewState.AsValueMap() 791 m["output"] = cty.UnknownVal(cty.String) 792 793 resp.PlannedState = cty.ObjectVal(m) 794 resp.LegacyTypeSystem = true 795 return resp 796 } 797 ctx := testContext2(t, &ContextOpts{ 798 Providers: map[addrs.Provider]providers.Factory{ 799 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 800 }, 801 }) 802 803 t.Run("condition pass", func(t *testing.T) { 804 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 805 Mode: plans.NormalMode, 806 SetVariables: InputValues{ 807 "boop": &InputValue{ 808 Value: cty.StringVal("boop"), 809 SourceType: ValueFromCLIArg, 810 }, 811 }, 812 }) 813 assertNoErrors(t, diags) 814 if len(plan.Changes.Resources) != 3 { 815 t.Fatalf("unexpected plan changes: %#v", plan.Changes) 816 } 817 818 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 819 m := req.PlannedState.AsValueMap() 820 m["output"] = cty.StringVal(fmt.Sprintf("new-%s", m["value"].AsString())) 821 822 resp.NewState = cty.ObjectVal(m) 823 return resp 824 } 825 state, diags := ctx.Apply(plan, m) 826 assertNoErrors(t, diags) 827 828 wantResourceAttrs := map[string]struct{ value, output string }{ 829 "a": {"boop", "new-boop"}, 830 "b": {"new-boop", "new-new-boop"}, 831 "c": {"new-new-boop", "new-new-new-boop"}, 832 } 833 for name, attrs := range wantResourceAttrs { 834 addr := mustResourceInstanceAddr(fmt.Sprintf("test_resource.%s", name)) 835 r := state.ResourceInstance(addr) 836 rd, err := r.Current.Decode(cty.Object(map[string]cty.Type{ 837 "value": cty.String, 838 "output": cty.String, 839 })) 840 if err != nil { 841 t.Fatalf("error decoding test_resource.a: %s", err) 842 } 843 want := cty.ObjectVal(map[string]cty.Value{ 844 "value": cty.StringVal(attrs.value), 845 "output": cty.StringVal(attrs.output), 846 }) 847 if !cmp.Equal(want, rd.Value, valueComparer) { 848 t.Errorf("wrong attrs for %s\n%s", addr, cmp.Diff(want, rd.Value, valueComparer)) 849 } 850 } 851 }) 852 t.Run("condition fail", func(t *testing.T) { 853 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 854 Mode: plans.NormalMode, 855 SetVariables: InputValues{ 856 "boop": &InputValue{ 857 Value: cty.StringVal("boop"), 858 SourceType: ValueFromCLIArg, 859 }, 860 }, 861 }) 862 assertNoErrors(t, diags) 863 if len(plan.Changes.Resources) != 3 { 864 t.Fatalf("unexpected plan changes: %#v", plan.Changes) 865 } 866 867 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 868 m := req.PlannedState.AsValueMap() 869 870 // For the resource with a constraint, fudge the output to make the 871 // condition fail. 872 if value := m["value"].AsString(); value == "new-boop" { 873 m["output"] = cty.StringVal("") 874 } else { 875 m["output"] = cty.StringVal(fmt.Sprintf("new-%s", value)) 876 } 877 878 resp.NewState = cty.ObjectVal(m) 879 return resp 880 } 881 state, diags := ctx.Apply(plan, m) 882 if !diags.HasErrors() { 883 t.Fatal("succeeded; want errors") 884 } 885 if got, want := diags.Err().Error(), "Resource postcondition failed: Output must not be blank."; got != want { 886 t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want) 887 } 888 889 // Resources a and b should still be recorded in state 890 wantResourceAttrs := map[string]struct{ value, output string }{ 891 "a": {"boop", "new-boop"}, 892 "b": {"new-boop", ""}, 893 } 894 for name, attrs := range wantResourceAttrs { 895 addr := mustResourceInstanceAddr(fmt.Sprintf("test_resource.%s", name)) 896 r := state.ResourceInstance(addr) 897 rd, err := r.Current.Decode(cty.Object(map[string]cty.Type{ 898 "value": cty.String, 899 "output": cty.String, 900 })) 901 if err != nil { 902 t.Fatalf("error decoding test_resource.a: %s", err) 903 } 904 want := cty.ObjectVal(map[string]cty.Value{ 905 "value": cty.StringVal(attrs.value), 906 "output": cty.StringVal(attrs.output), 907 }) 908 if !cmp.Equal(want, rd.Value, valueComparer) { 909 t.Errorf("wrong attrs for %s\n%s", addr, cmp.Diff(want, rd.Value, valueComparer)) 910 } 911 } 912 913 // Resource c should not be in state 914 if state.ResourceInstance(mustResourceInstanceAddr("test_resource.c")) != nil { 915 t.Error("test_resource.c should not exist in state, but is") 916 } 917 }) 918 } 919 920 func TestContext2Apply_resourceConditionApplyTimeFail(t *testing.T) { 921 // This tests the less common situation where a condition fails due to 922 // a change in a resource other than the one the condition is attached to, 923 // and the condition result is unknown during planning. 924 // 925 // This edge case is a tricky one because it relies on Terraform still 926 // visiting test_resource.b (in the configuration below) to evaluate 927 // its conditions even though there aren't any changes directly planned 928 // for it, so that we can consider whether changes to test_resource.a 929 // have changed the outcome. 930 931 m := testModuleInline(t, map[string]string{ 932 "main.tf": ` 933 variable "input" { 934 type = string 935 } 936 937 resource "test_resource" "a" { 938 value = var.input 939 } 940 941 resource "test_resource" "b" { 942 value = "beep" 943 944 lifecycle { 945 postcondition { 946 condition = test_resource.a.output == self.output 947 error_message = "Outputs must match." 948 } 949 } 950 } 951 `, 952 }) 953 954 p := testProvider("test") 955 p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ 956 ResourceTypes: map[string]*configschema.Block{ 957 "test_resource": { 958 Attributes: map[string]*configschema.Attribute{ 959 "value": { 960 Type: cty.String, 961 Required: true, 962 }, 963 "output": { 964 Type: cty.String, 965 Computed: true, 966 }, 967 }, 968 }, 969 }, 970 }) 971 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 972 // Whenever "value" changes, "output" follows it during the apply step, 973 // but is initially unknown during the plan step. 974 975 m := req.ProposedNewState.AsValueMap() 976 priorVal := cty.NullVal(cty.String) 977 if !req.PriorState.IsNull() { 978 priorVal = req.PriorState.GetAttr("value") 979 } 980 if m["output"].IsNull() || !priorVal.RawEquals(m["value"]) { 981 m["output"] = cty.UnknownVal(cty.String) 982 } 983 984 resp.PlannedState = cty.ObjectVal(m) 985 resp.LegacyTypeSystem = true 986 return resp 987 } 988 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 989 m := req.PlannedState.AsValueMap() 990 m["output"] = m["value"] 991 resp.NewState = cty.ObjectVal(m) 992 return resp 993 } 994 ctx := testContext2(t, &ContextOpts{ 995 Providers: map[addrs.Provider]providers.Factory{ 996 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 997 }, 998 }) 999 instA := mustResourceInstanceAddr("test_resource.a") 1000 instB := mustResourceInstanceAddr("test_resource.b") 1001 1002 // Preparation: an initial plan and apply with a correct input variable 1003 // should succeed and give us a valid and complete state to use for the 1004 // subsequent plan and apply that we'll expect to fail. 1005 var prevRunState *states.State 1006 { 1007 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 1008 Mode: plans.NormalMode, 1009 SetVariables: InputValues{ 1010 "input": &InputValue{ 1011 Value: cty.StringVal("beep"), 1012 SourceType: ValueFromCLIArg, 1013 }, 1014 }, 1015 }) 1016 assertNoErrors(t, diags) 1017 planA := plan.Changes.ResourceInstance(instA) 1018 if planA == nil || planA.Action != plans.Create { 1019 t.Fatalf("incorrect initial plan for instance A\nwant a 'create' change\ngot: %s", spew.Sdump(planA)) 1020 } 1021 planB := plan.Changes.ResourceInstance(instB) 1022 if planB == nil || planB.Action != plans.Create { 1023 t.Fatalf("incorrect initial plan for instance B\nwant a 'create' change\ngot: %s", spew.Sdump(planB)) 1024 } 1025 1026 state, diags := ctx.Apply(plan, m) 1027 assertNoErrors(t, diags) 1028 1029 stateA := state.ResourceInstance(instA) 1030 if stateA == nil || stateA.Current == nil || !bytes.Contains(stateA.Current.AttrsJSON, []byte(`"beep"`)) { 1031 t.Fatalf("incorrect initial state for instance A\ngot: %s", spew.Sdump(stateA)) 1032 } 1033 stateB := state.ResourceInstance(instB) 1034 if stateB == nil || stateB.Current == nil || !bytes.Contains(stateB.Current.AttrsJSON, []byte(`"beep"`)) { 1035 t.Fatalf("incorrect initial state for instance B\ngot: %s", spew.Sdump(stateB)) 1036 } 1037 prevRunState = state 1038 } 1039 1040 // Now we'll run another plan and apply with a different value for 1041 // var.input that should cause the test_resource.b condition to be unknown 1042 // during planning and then fail during apply. 1043 { 1044 plan, diags := ctx.Plan(m, prevRunState, &PlanOpts{ 1045 Mode: plans.NormalMode, 1046 SetVariables: InputValues{ 1047 "input": &InputValue{ 1048 Value: cty.StringVal("boop"), // NOTE: This has changed 1049 SourceType: ValueFromCLIArg, 1050 }, 1051 }, 1052 }) 1053 assertNoErrors(t, diags) 1054 planA := plan.Changes.ResourceInstance(instA) 1055 if planA == nil || planA.Action != plans.Update { 1056 t.Fatalf("incorrect initial plan for instance A\nwant an 'update' change\ngot: %s", spew.Sdump(planA)) 1057 } 1058 planB := plan.Changes.ResourceInstance(instB) 1059 if planB == nil || planB.Action != plans.NoOp { 1060 t.Fatalf("incorrect initial plan for instance B\nwant a 'no-op' change\ngot: %s", spew.Sdump(planB)) 1061 } 1062 1063 _, diags = ctx.Apply(plan, m) 1064 if !diags.HasErrors() { 1065 t.Fatal("final apply succeeded, but should've failed with a postcondition error") 1066 } 1067 if len(diags) != 1 { 1068 t.Fatalf("expected exactly one diagnostic, but got: %s", diags.Err().Error()) 1069 } 1070 if got, want := diags[0].Description().Summary, "Resource postcondition failed"; got != want { 1071 t.Fatalf("wrong diagnostic summary\ngot: %s\nwant: %s", got, want) 1072 } 1073 } 1074 } 1075 1076 // pass an input through some expanded values, and back to a provider to make 1077 // sure we can fully evaluate a provider configuration during a destroy plan. 1078 func TestContext2Apply_destroyWithConfiguredProvider(t *testing.T) { 1079 m := testModuleInline(t, map[string]string{ 1080 "main.tf": ` 1081 variable "in" { 1082 type = map(string) 1083 default = { 1084 "a" = "first" 1085 "b" = "second" 1086 } 1087 } 1088 1089 module "mod" { 1090 source = "./mod" 1091 for_each = var.in 1092 in = each.value 1093 } 1094 1095 locals { 1096 config = [for each in module.mod : each.out] 1097 } 1098 1099 provider "other" { 1100 output = [for each in module.mod : each.out] 1101 local = local.config 1102 var = var.in 1103 } 1104 1105 resource "other_object" "other" { 1106 } 1107 `, 1108 "./mod/main.tf": ` 1109 variable "in" { 1110 type = string 1111 } 1112 1113 data "test_object" "d" { 1114 test_string = var.in 1115 } 1116 1117 resource "test_object" "a" { 1118 test_string = var.in 1119 } 1120 1121 output "out" { 1122 value = data.test_object.d.output 1123 } 1124 `}) 1125 1126 testProvider := &MockProvider{ 1127 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 1128 Provider: providers.Schema{Block: simpleTestSchema()}, 1129 ResourceTypes: map[string]providers.Schema{ 1130 "test_object": providers.Schema{Block: simpleTestSchema()}, 1131 }, 1132 DataSources: map[string]providers.Schema{ 1133 "test_object": providers.Schema{ 1134 Block: &configschema.Block{ 1135 Attributes: map[string]*configschema.Attribute{ 1136 "test_string": { 1137 Type: cty.String, 1138 Optional: true, 1139 }, 1140 "output": { 1141 Type: cty.String, 1142 Computed: true, 1143 }, 1144 }, 1145 }, 1146 }, 1147 }, 1148 }, 1149 } 1150 1151 testProvider.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { 1152 cfg := req.Config.AsValueMap() 1153 s := cfg["test_string"].AsString() 1154 if !strings.Contains("firstsecond", s) { 1155 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("expected 'first' or 'second', got %s", s)) 1156 return resp 1157 } 1158 1159 cfg["output"] = cty.StringVal(s + "-ok") 1160 resp.State = cty.ObjectVal(cfg) 1161 return resp 1162 } 1163 1164 otherProvider := &MockProvider{ 1165 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 1166 Provider: providers.Schema{ 1167 Block: &configschema.Block{ 1168 Attributes: map[string]*configschema.Attribute{ 1169 "output": { 1170 Type: cty.List(cty.String), 1171 Optional: true, 1172 }, 1173 "local": { 1174 Type: cty.List(cty.String), 1175 Optional: true, 1176 }, 1177 "var": { 1178 Type: cty.Map(cty.String), 1179 Optional: true, 1180 }, 1181 }, 1182 }, 1183 }, 1184 ResourceTypes: map[string]providers.Schema{ 1185 "other_object": providers.Schema{Block: simpleTestSchema()}, 1186 }, 1187 }, 1188 } 1189 1190 ctx := testContext2(t, &ContextOpts{ 1191 Providers: map[addrs.Provider]providers.Factory{ 1192 addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider), 1193 addrs.NewDefaultProvider("other"): testProviderFuncFixed(otherProvider), 1194 }, 1195 }) 1196 1197 opts := SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)) 1198 plan, diags := ctx.Plan(m, states.NewState(), opts) 1199 assertNoErrors(t, diags) 1200 1201 state, diags := ctx.Apply(plan, m) 1202 assertNoErrors(t, diags) 1203 1204 otherProvider.ConfigureProviderCalled = false 1205 otherProvider.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 1206 // check that our config is complete, even during a destroy plan 1207 expected := cty.ObjectVal(map[string]cty.Value{ 1208 "local": cty.ListVal([]cty.Value{cty.StringVal("first-ok"), cty.StringVal("second-ok")}), 1209 "output": cty.ListVal([]cty.Value{cty.StringVal("first-ok"), cty.StringVal("second-ok")}), 1210 "var": cty.MapVal(map[string]cty.Value{ 1211 "a": cty.StringVal("first"), 1212 "b": cty.StringVal("second"), 1213 }), 1214 }) 1215 1216 if !req.Config.RawEquals(expected) { 1217 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf( 1218 `incorrect provider config: 1219 expected: %#v 1220 got: %#v`, 1221 expected, req.Config)) 1222 } 1223 1224 return resp 1225 } 1226 1227 opts.Mode = plans.DestroyMode 1228 // skip refresh so that we don't configure the provider before the destroy plan 1229 opts.SkipRefresh = true 1230 1231 // destroy only a single instance not included in the moved statements 1232 _, diags = ctx.Plan(m, state, opts) 1233 assertNoErrors(t, diags) 1234 1235 if !otherProvider.ConfigureProviderCalled { 1236 t.Fatal("failed to configure provider during destroy plan") 1237 } 1238 } 1239 1240 // check that a provider can verify a planned destroy 1241 func TestContext2Apply_plannedDestroy(t *testing.T) { 1242 m := testModuleInline(t, map[string]string{ 1243 "main.tf": ` 1244 resource "test_object" "x" { 1245 test_string = "ok" 1246 }`, 1247 }) 1248 1249 p := simpleMockProvider() 1250 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 1251 if !req.ProposedNewState.IsNull() { 1252 // we should only be destroying in this test 1253 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unexpected plan with %#v", req.ProposedNewState)) 1254 return resp 1255 } 1256 1257 resp.PlannedState = req.ProposedNewState 1258 // we're going to verify the destroy plan by inserting private data required for destroy 1259 resp.PlannedPrivate = append(resp.PlannedPrivate, []byte("planned")...) 1260 return resp 1261 } 1262 1263 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { 1264 // if the value is nil, we return that directly to correspond to a delete 1265 if !req.PlannedState.IsNull() { 1266 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unexpected apply with %#v", req.PlannedState)) 1267 return resp 1268 } 1269 1270 resp.NewState = req.PlannedState 1271 1272 // make sure we get our private data from the plan 1273 private := string(req.PlannedPrivate) 1274 if private != "planned" { 1275 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("missing private data from plan, got %q", private)) 1276 } 1277 return resp 1278 } 1279 1280 state := states.NewState() 1281 root := state.EnsureModule(addrs.RootModuleInstance) 1282 root.SetResourceInstanceCurrent( 1283 mustResourceInstanceAddr("test_object.x").Resource, 1284 &states.ResourceInstanceObjectSrc{ 1285 Status: states.ObjectReady, 1286 AttrsJSON: []byte(`{"test_string":"ok"}`), 1287 }, 1288 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 1289 ) 1290 1291 ctx := testContext2(t, &ContextOpts{ 1292 Providers: map[addrs.Provider]providers.Factory{ 1293 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 1294 }, 1295 }) 1296 1297 plan, diags := ctx.Plan(m, state, &PlanOpts{ 1298 Mode: plans.DestroyMode, 1299 // we don't want to refresh, because that actually runs a normal plan 1300 SkipRefresh: true, 1301 }) 1302 if diags.HasErrors() { 1303 t.Fatalf("plan: %s", diags.Err()) 1304 } 1305 1306 _, diags = ctx.Apply(plan, m) 1307 if diags.HasErrors() { 1308 t.Fatalf("apply: %s", diags.Err()) 1309 } 1310 }