diff --git a/terraform/context_apply2_test.go b/terraform/context_apply2_test.go index a9e816fc4..a739db2ad 100644 --- a/terraform/context_apply2_test.go +++ b/terraform/context_apply2_test.go @@ -1,11 +1,14 @@ package terraform import ( + "errors" + "fmt" "testing" "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 @@ -69,3 +72,108 @@ func TestContext2Apply_createBeforeDestroy_deposedKeyPreApply(t *testing.T) { 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()) + } +}