package terraform import ( "errors" "fmt" "sync" "testing" "time" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/states" "github.com/zclconf/go-cty/cty" ) // Test that the PreApply hook is called with the correct deposed key func TestContext2Apply_createBeforeDestroy_deposedKeyPreApply(t *testing.T) { m := testModule(t, "apply-cbd-deposed-only") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn deposedKey := states.NewDeposedKey() state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceDeposed( mustResourceInstanceAddr("aws_instance.bar").Resource, deposedKey, &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) hook := new(MockHook) ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{hook}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // Verify PreApply was called correctly if !hook.PreApplyCalled { t.Fatalf("PreApply hook not called") } if addr, wantAddr := hook.PreApplyAddr, mustResourceInstanceAddr("aws_instance.bar"); !addr.Equal(wantAddr) { t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) } if gen := hook.PreApplyGen; gen != deposedKey { t.Errorf("expected gen to be %q, but was %q", deposedKey, gen) } } func TestContext2Apply_destroyWithDataSourceExpansion(t *testing.T) { // While managed resources store their destroy-time dependencies, data // sources do not. This means that if a provider were only included in a // destroy graph because of data sources, it could have dependencies which // are not correctly ordered. Here we verify that the provider is not // included in the destroy operation, and all dependency evaluations // succeed. m := testModuleInline(t, map[string]string{ "main.tf": ` module "mod" { source = "./mod" } provider "other" { foo = module.mod.data } # this should not require the provider be present during destroy data "other_data_source" "a" { } `, "mod/main.tf": ` data "test_data_source" "a" { count = 1 } data "test_data_source" "b" { count = data.test_data_source.a[0].foo == "ok" ? 1 : 0 } output "data" { value = data.test_data_source.a[0].foo == "ok" ? data.test_data_source.b[0].foo : "nope" } `, }) testP := testProvider("test") otherP := testProvider("other") readData := func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { return providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("data_source"), "foo": cty.StringVal("ok"), }), } } testP.ReadDataSourceFn = readData otherP.ReadDataSourceFn = readData ps := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(testP), addrs.NewDefaultProvider("other"): testProviderFuncFixed(otherP), } otherP.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { foo := req.Config.GetAttr("foo") if foo.IsNull() || foo.AsString() != "ok" { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("incorrect config val: %#v\n", foo)) } return resp } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: ps, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.Err()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatal(diags.Err()) } // now destroy the whole thing ctx = testContext2(t, &ContextOpts{ Config: m, Providers: ps, Destroy: true, }) _, diags = ctx.Plan() if diags.HasErrors() { t.Fatal(diags.Err()) } otherP.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { // should not be used to destroy data sources resp.Diagnostics = resp.Diagnostics.Append(errors.New("provider should not be used")) return resp } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatal(diags.Err()) } } func TestContext2Apply_destroyThenUpdate(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` resource "test_instance" "a" { value = "udpated" } `, }) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn var orderMu sync.Mutex var order []string p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { id := req.PriorState.GetAttr("id").AsString() if id == "b" { // slow down the b destroy, since a should wait for it time.Sleep(100 * time.Millisecond) } orderMu.Lock() order = append(order, id) orderMu.Unlock() resp.NewState = req.PlannedState return resp } addrA := mustResourceInstanceAddr(`test_instance.a`) addrB := mustResourceInstanceAddr(`test_instance.b`) state := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"a","value":"old","type":"test"}`), Status: states.ObjectReady, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) // test_instance.b depended on test_instance.a, and therefor should be // destroyed before any changes to test_instance.a s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"b"}`), Status: states.ObjectReady, Dependencies: []addrs.ConfigResource{addrA.ContainingResource().Config()}, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) }) ctx := testContext2(t, &ContextOpts{ Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatal(diags.Err()) } _, diags := ctx.Apply() if diags.HasErrors() { t.Fatal(diags.Err()) } if order[0] != "b" { t.Fatalf("expected apply order [b, a], got: %v\n", order) } }