package terraform import ( "bytes" "encoding/json" "errors" "fmt" "log" "reflect" "runtime" "sort" "strings" "sync" "sync/atomic" "testing" "time" "github.com/davecgh/go-spew/spew" "github.com/go-test/deep" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/gocty" ) func TestContext2Apply_basic(t *testing.T) { m := testModule(t, "apply-good") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } mod := state.RootModule() if len(mod.Resources) < 2 { t.Fatalf("bad: %#v", mod.Resources) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_unstable(t *testing.T) { // This tests behavior when the configuration contains an unstable value, // such as the result of uuid() or timestamp(), where each call produces // a different result. // // This is an important case to test because we need to ensure that // we don't re-call the function during the apply phase: the value should // be fixed during plan m := testModule(t, "apply-unstable") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("unexpected error during Plan: %s", diags.Err()) } addr := addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"].Block rds := plan.Changes.ResourceInstance(addr) rd, err := rds.Decode(schema.ImpliedType()) if err != nil { t.Fatal(err) } if rd.After.GetAttr("random").IsKnown() { t.Fatalf("Attribute 'random' has known value %#v; should be unknown in plan", rd.After.GetAttr("random")) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("unexpected error during Apply: %s", diags.Err()) } mod := state.Module(addr.Module) rss := state.ResourceInstance(addr) if len(mod.Resources) != 1 { t.Fatalf("wrong number of resources %d; want 1", len(mod.Resources)) } rs, err := rss.Current.Decode(schema.ImpliedType()) if err != nil { t.Fatalf("decode error: %v", err) } got := rs.Value.GetAttr("random") if !got.IsKnown() { t.Fatalf("random is still unknown after apply") } if got, want := len(got.AsString()), 36; got != want { t.Fatalf("random string has wrong length %d; want %d", got, want) } } func TestContext2Apply_escape(t *testing.T) { m := testModule(t, "apply-escape") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ` aws_instance.bar: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = "bar" type = aws_instance `) } func TestContext2Apply_resourceCountOneList(t *testing.T) { m := testModule(t, "apply-resource-count-one-list") p := testProvider("null") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() assertNoDiagnostics(t, diags) got := strings.TrimSpace(state.String()) want := strings.TrimSpace(`null_resource.foo.0: ID = foo provider = provider["registry.terraform.io/hashicorp/null"] Outputs: test = [foo]`) if got != want { t.Fatalf("got:\n%s\n\nwant:\n%s\n", got, want) } } func TestContext2Apply_resourceCountZeroList(t *testing.T) { m := testModule(t, "apply-resource-count-zero-list") p := testProvider("null") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } got := strings.TrimSpace(state.String()) want := strings.TrimSpace(` Outputs: test = []`) if got != want { t.Fatalf("wrong state\n\ngot:\n%s\n\nwant:\n%s\n", got, want) } } func TestContext2Apply_resourceDependsOnModule(t *testing.T) { m := testModule(t, "apply-resource-depends-on-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn // verify the apply happens in the correct order var mu sync.Mutex var order []string p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { ami := req.PlannedState.GetAttr("ami").AsString() switch ami { case "child": // make the child slower than the parent time.Sleep(50 * time.Millisecond) mu.Lock() order = append(order, "child") mu.Unlock() case "parent": mu.Lock() order = append(order, "parent") mu.Unlock() } return testApplyFn(req) } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if !reflect.DeepEqual(order, []string{"child", "parent"}) { t.Fatal("resources applied out of order") } checkStateString(t, state, testTerraformApplyResourceDependsOnModuleStr) } // Test that without a config, the Dependencies in the state are enough // to maintain proper ordering. func TestContext2Apply_resourceDependsOnModuleStateOnly(t *testing.T) { m := testModule(t, "apply-resource-depends-on-module-empty") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.a").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"parent"}`), Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("module.child.aws_instance.child")}, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) child.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.child").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"child"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) { // verify the apply happens in the correct order var mu sync.Mutex var order []string p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { id := req.PriorState.GetAttr("id") if id.IsKnown() && id.AsString() == "parent" { // make the dep slower than the parent time.Sleep(50 * time.Millisecond) mu.Lock() order = append(order, "child") mu.Unlock() } else { mu.Lock() order = append(order, "parent") mu.Unlock() } return testApplyFn(req) } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state, diags := ctx.Apply() assertNoErrors(t, diags) if !reflect.DeepEqual(order, []string{"child", "parent"}) { t.Fatal("resources applied out of order") } checkStateString(t, state, "") } } func TestContext2Apply_resourceDependsOnModuleDestroy(t *testing.T) { m := testModule(t, "apply-resource-depends-on-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn var globalState *states.State { ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } globalState = state } { // Wait for the dependency, sleep, and verify the graph never // called a child. var called int32 var checked bool p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { ami := req.PriorState.GetAttr("ami").AsString() if ami == "parent" { checked = true // Sleep to allow parallel execution time.Sleep(50 * time.Millisecond) // Verify that called is 0 (dep not called) if atomic.LoadInt32(&called) != 0 { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("module child should not be called")) return resp } } atomic.AddInt32(&called, 1) return testApplyFn(req) } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: globalState, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if !checked { t.Fatal("should check") } checkStateString(t, state, ``) } } func TestContext2Apply_resourceDependsOnModuleGrandchild(t *testing.T) { m := testModule(t, "apply-resource-depends-on-module-deep") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn { // Wait for the dependency, sleep, and verify the graph never // called a child. var called int32 var checked bool p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { planned := req.PlannedState.AsValueMap() if ami, ok := planned["ami"]; ok && ami.AsString() == "grandchild" { checked = true // Sleep to allow parallel execution time.Sleep(50 * time.Millisecond) // Verify that called is 0 (dep not called) if atomic.LoadInt32(&called) != 0 { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("aws_instance.a should not be called")) return resp } } atomic.AddInt32(&called, 1) return testApplyFn(req) } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if !checked { t.Fatal("should check") } checkStateString(t, state, testTerraformApplyResourceDependsOnModuleDeepStr) } } func TestContext2Apply_resourceDependsOnModuleInModule(t *testing.T) { m := testModule(t, "apply-resource-depends-on-module-in-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn { // Wait for the dependency, sleep, and verify the graph never // called a child. var called int32 var checked bool p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { planned := req.PlannedState.AsValueMap() if ami, ok := planned["ami"]; ok && ami.AsString() == "grandchild" { checked = true // Sleep to allow parallel execution time.Sleep(50 * time.Millisecond) // Verify that called is 0 (dep not called) if atomic.LoadInt32(&called) != 0 { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("something else was applied before grandchild; grandchild should be first")) return resp } } atomic.AddInt32(&called, 1) return testApplyFn(req) } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if !checked { t.Fatal("should check") } checkStateString(t, state, testTerraformApplyResourceDependsOnModuleInModuleStr) } } func TestContext2Apply_mapVarBetweenModules(t *testing.T) { m := testModule(t, "apply-map-var-through-module") p := testProvider("null") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(` Outputs: amis_from_module = {eu-west-1:ami-789012 eu-west-2:ami-989484 us-west-1:ami-123456 us-west-2:ami-456789 } module.test: null_resource.noop: ID = foo provider = provider["registry.terraform.io/hashicorp/null"] Outputs: amis_out = {eu-west-1:ami-789012 eu-west-2:ami-989484 us-west-1:ami-123456 us-west-2:ami-456789 }`) if actual != expected { t.Fatalf("expected: \n%s\n\ngot: \n%s\n", expected, actual) } } func TestContext2Apply_refCount(t *testing.T) { m := testModule(t, "apply-ref-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } mod := state.RootModule() if len(mod.Resources) < 2 { t.Fatalf("bad: %#v", mod.Resources) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyRefCountStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_providerAlias(t *testing.T) { m := testModule(t, "apply-provider-alias") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } mod := state.RootModule() if len(mod.Resources) < 2 { t.Fatalf("bad: %#v", mod.Resources) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProviderAliasStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } // Two providers that are configured should both be configured prior to apply func TestContext2Apply_providerAliasConfigure(t *testing.T) { m := testModule(t, "apply-provider-alias-configure") p2 := testProvider("another") p2.ApplyResourceChangeFn = testApplyFn p2.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("another"): testProviderFuncFixed(p2), }, }) if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { t.Logf(legacyDiffComparisonString(p.Changes)) } // Configure to record calls AFTER Plan above var configCount int32 p2.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { atomic.AddInt32(&configCount, 1) foo := req.Config.GetAttr("foo").AsString() if foo != "bar" { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("foo: %#v", foo)) } return } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if configCount != 2 { t.Fatalf("provider config expected 2 calls, got: %d", configCount) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProviderAliasConfigStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } // GH-2870 func TestContext2Apply_providerWarning(t *testing.T) { m := testModule(t, "apply-provider-warning") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn p.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) (resp providers.ValidateResourceConfigResponse) { resp.Diagnostics = resp.Diagnostics.Append(tfdiags.SimpleWarning("just a warning")) return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(` aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance `) if actual != expected { t.Fatalf("got: \n%s\n\nexpected:\n%s", actual, expected) } if !p.ConfigureProviderCalled { t.Fatalf("provider Configure() was never called!") } } func TestContext2Apply_emptyModule(t *testing.T) { // A module with only outputs (no resources) m := testModule(t, "apply-empty-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) actual = strings.Replace(actual, " ", "", -1) expected := strings.TrimSpace(testTerraformApplyEmptyModuleStr) if actual != expected { t.Fatalf("bad: \n%s\nexpect:\n%s", actual, expected) } } func TestContext2Apply_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-good-create-before") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar", "require_new": "abc"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, 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()) } mod := state.RootModule() if got, want := len(mod.Resources), 1; got != want { t.Logf("state:\n%s", state) t.Fatalf("wrong number of resources %d; want %d", got, want) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyCreateBeforeStr) if actual != expected { t.Fatalf("expected:\n%s\ngot:\n%s", expected, actual) } } func TestContext2Apply_createBeforeDestroyUpdate(t *testing.T) { m := testModule(t, "apply-good-create-before-update") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn // signal that resource foo has started applying fooChan := make(chan struct{}) p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { id := req.PriorState.GetAttr("id").AsString() switch id { case "bar": select { case <-fooChan: resp.Diagnostics = resp.Diagnostics.Append(errors.New("bar must be updated before foo is destroyed")) return resp case <-time.After(100 * time.Millisecond): // wait a moment to ensure that foo is not going to be destroyed first } case "foo": close(fooChan) } return testApplyFn(req) } state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) fooAddr := mustResourceInstanceAddr("aws_instance.foo") root.SetResourceInstanceCurrent( fooAddr.Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo","foo":"bar"}`), CreateBeforeDestroy: true, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","foo":"bar"}`), CreateBeforeDestroy: true, Dependencies: []addrs.ConfigResource{fooAddr.ContainingResource().Config()}, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, 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()) } mod := state.RootModule() if len(mod.Resources) != 1 { t.Fatalf("bad: %s", state) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyCreateBeforeUpdateStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } // This tests that when a CBD resource depends on a non-CBD resource, // we can still properly apply changes that require new for both. func TestContext2Apply_createBeforeDestroy_dependsNonCBD(t *testing.T) { m := testModule(t, "apply-cbd-depends-non-cbd") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar", "require_new": "abc"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo", "require_new": "abc"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, 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()) } checkStateString(t, state, ` aws_instance.bar: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] require_new = yes type = aws_instance value = foo Dependencies: aws_instance.foo aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] require_new = yes type = aws_instance `) } func TestContext2Apply_createBeforeDestroy_hook(t *testing.T) { h := new(MockHook) m := testModule(t, "apply-good-create-before") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar", "require_new": "abc"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) var actual []cty.Value var actualLock sync.Mutex h.PostApplyFn = func(addr addrs.AbsResourceInstance, gen states.Generation, sv cty.Value, e error) (HookAction, error) { actualLock.Lock() defer actualLock.Unlock() actual = append(actual, sv) return HookActionContinue, nil } ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, 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)) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } expected := []cty.Value{ cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), "require_new": cty.StringVal("xyz"), "type": cty.StringVal("aws_instance"), }), cty.NullVal(cty.DynamicPseudoType), } cmpOpt := cmp.Transformer("ctyshim", hcl2shim.ConfigValueFromHCL2) if !cmp.Equal(actual, expected, cmpOpt) { t.Fatalf("wrong state snapshot sequence\n%s", cmp.Diff(expected, actual, cmpOpt)) } } // Test that we can perform an apply with CBD in a count with deposed instances. func TestContext2Apply_createBeforeDestroy_deposedCount(t *testing.T) { m := testModule(t, "apply-cbd-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar[0]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceDeposed( mustResourceInstanceAddr("aws_instance.bar[0]").Resource, states.NewDeposedKey(), &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar[1]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceDeposed( mustResourceInstanceAddr("aws_instance.bar[1]").Resource, states.NewDeposedKey(), &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, 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()) } checkStateString(t, state, ` aws_instance.bar.0: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = bar type = aws_instance aws_instance.bar.1: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = bar type = aws_instance `) } // Test that when we have a deposed instance but a good primary, we still // destroy the deposed instance. func TestContext2Apply_createBeforeDestroy_deposedOnly(t *testing.T) { m := testModule(t, "apply-cbd-deposed-only") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn 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, states.NewDeposedKey(), &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, 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()) } checkStateString(t, state, ` aws_instance.bar: ID = bar provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance `) } func TestContext2Apply_destroyComputed(t *testing.T) { m := testModule(t, "apply-destroy-computed") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo", "output": "value"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, }) if p, diags := ctx.Plan(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("plan failed") } else { t.Logf("plan:\n\n%s", legacyDiffComparisonString(p.Changes)) } if _, diags := ctx.Apply(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("apply failed") } } // Test that the destroy operation uses depends_on as a source of ordering. func TestContext2Apply_destroyDependsOn(t *testing.T) { // It is possible for this to be racy, so we loop a number of times // just to check. for i := 0; i < 10; i++ { testContext2Apply_destroyDependsOn(t) } } func testContext2Apply_destroyDependsOn(t *testing.T) { m := testModule(t, "apply-destroy-depends-on") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn 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.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("aws_instance.bar")}, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) // Record the order we see Apply var actual []string var actualLock sync.Mutex p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { actualLock.Lock() defer actualLock.Unlock() id := req.PriorState.GetAttr("id").AsString() actual = append(actual, id) return testApplyFn(req) } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, Parallelism: 1, // To check ordering }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } expected := []string{"foo", "bar"} if !reflect.DeepEqual(actual, expected) { t.Fatalf("wrong order\ngot: %#v\nwant: %#v", actual, expected) } } // Test that destroy ordering is correct with dependencies only // in the state. func TestContext2Apply_destroyDependsOnStateOnly(t *testing.T) { newState := states.NewState() root := newState.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "foo", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), Dependencies: []addrs.ConfigResource{}, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "bar", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), Dependencies: []addrs.ConfigResource{ addrs.ConfigResource{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "foo", }, Module: root.Addr.Module(), }, }, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) // It is possible for this to be racy, so we loop a number of times // just to check. for i := 0; i < 10; i++ { t.Run("new", func(t *testing.T) { testContext2Apply_destroyDependsOnStateOnly(t, newState) }) } } func testContext2Apply_destroyDependsOnStateOnly(t *testing.T, state *states.State) { state = state.DeepCopy() m := testModule(t, "empty") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn // Record the order we see Apply var actual []string var actualLock sync.Mutex p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { actualLock.Lock() defer actualLock.Unlock() id := req.PriorState.GetAttr("id").AsString() actual = append(actual, id) return testApplyFn(req) } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, Parallelism: 1, // To check ordering }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } expected := []string{"bar", "foo"} if !reflect.DeepEqual(actual, expected) { t.Fatalf("wrong order\ngot: %#v\nwant: %#v", actual, expected) } } // Test that destroy ordering is correct with dependencies only // in the state within a module (GH-11749) func TestContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T) { newState := states.NewState() child := newState.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) child.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "foo", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), Dependencies: []addrs.ConfigResource{}, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) child.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "bar", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), Dependencies: []addrs.ConfigResource{ addrs.ConfigResource{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "foo", }, Module: child.Addr.Module(), }, }, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) // It is possible for this to be racy, so we loop a number of times // just to check. for i := 0; i < 10; i++ { t.Run("new", func(t *testing.T) { testContext2Apply_destroyDependsOnStateOnlyModule(t, newState) }) } } func testContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T, state *states.State) { state = state.DeepCopy() m := testModule(t, "empty") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn // Record the order we see Apply var actual []string var actualLock sync.Mutex p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { actualLock.Lock() defer actualLock.Unlock() id := req.PriorState.GetAttr("id").AsString() actual = append(actual, id) return testApplyFn(req) } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, Parallelism: 1, // To check ordering }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } expected := []string{"bar", "foo"} if !reflect.DeepEqual(actual, expected) { t.Fatalf("wrong order\ngot: %#v\nwant: %#v", actual, expected) } } func TestContext2Apply_dataBasic(t *testing.T) { m := testModule(t, "apply-data-basic") p := testProvider("null") p.PlanResourceChangeFn = testDiffFn p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("yo"), "foo": cty.NullVal(cty.String), }), } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), }, }) if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() assertNoErrors(t, diags) actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyDataBasicStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_destroyData(t *testing.T) { m := testModule(t, "apply-destroy-data-resource") p := testProvider("null") p.PlanResourceChangeFn = testDiffFn p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { return providers.ReadDataSourceResponse{ State: req.Config, } } state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("data.null_data_source.testing").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"-"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/null"]`), ) hook := &testHook{} ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, Hooks: []Hook{hook}, }) if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { t.Logf(legacyDiffComparisonString(p.Changes)) } newState, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if got := len(newState.Modules); got != 1 { t.Fatalf("state has %d modules after destroy; want 1", got) } if got := len(newState.RootModule().Resources); got != 0 { t.Fatalf("state has %d resources after destroy; want 0", got) } wantHookCalls := []*testHookCall{ {"PreDiff", "data.null_data_source.testing"}, {"PostDiff", "data.null_data_source.testing"}, {"PreDiff", "data.null_data_source.testing"}, {"PostDiff", "data.null_data_source.testing"}, {"PostStateUpdate", ""}, } if !reflect.DeepEqual(hook.Calls, wantHookCalls) { t.Errorf("wrong hook calls\ngot: %swant: %s", spew.Sdump(hook.Calls), spew.Sdump(wantHookCalls)) } } // https://github.com/hashicorp/terraform/pull/5096 func TestContext2Apply_destroySkipsCBD(t *testing.T) { // Config contains CBD resource depending on non-CBD resource, which triggers // a cycle if they are both replaced, but should _not_ trigger a cycle when // just doing a `terraform destroy`. m := testModule(t, "apply-destroy-cbd") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, }) if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { t.Logf(legacyDiffComparisonString(p.Changes)) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } func TestContext2Apply_destroyModuleVarProviderConfig(t *testing.T) { m := testModule(t, "apply-destroy-mod-var-provider-config") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } _, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } } func TestContext2Apply_destroyCrossProviders(t *testing.T) { m := testModule(t, "apply-destroy-cross-providers") p_aws := testProvider("aws") p_aws.ApplyResourceChangeFn = testApplyFn p_aws.PlanResourceChangeFn = testDiffFn p_aws.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, Computed: true, }, }, }, "aws_vpc": { Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, Computed: true, }, "value": { Type: cty.String, Optional: true, }, }, }, }, }) providers := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p_aws), } ctx := getContextForApply_destroyCrossProviders(t, m, providers) if _, diags := ctx.Plan(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("plan failed") } if _, diags := ctx.Apply(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("apply failed") } } func getContextForApply_destroyCrossProviders(t *testing.T, m *configs.Config, providerFactories map[addrs.Provider]providers.Factory) *Context { state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.shared").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"test"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) child.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_vpc.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id": "vpc-aaabbb12", "value":"test"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: providerFactories, State: state, PlanMode: plans.DestroyMode, }) return ctx } func TestContext2Apply_minimal(t *testing.T) { m := testModule(t, "apply-minimal") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyMinimalStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_cancel(t *testing.T) { stopped := false m := testModule(t, "apply-cancel") p := testProvider("aws") ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { if !stopped { stopped = true go ctx.Stop() for { if ctx.sh.Stopped() { break } time.Sleep(10 * time.Millisecond) } } return testApplyFn(req) } p.PlanResourceChangeFn = testDiffFn if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } // Start the Apply in a goroutine var applyDiags tfdiags.Diagnostics stateCh := make(chan *states.State) go func() { state, diags := ctx.Apply() applyDiags = diags stateCh <- state }() state := <-stateCh // only expecting an early exit error if !applyDiags.HasErrors() { t.Fatal("expected early exit error") } for _, d := range applyDiags { desc := d.Description() if desc.Summary != "execution halted" { t.Fatalf("unexpected error: %v", applyDiags.Err()) } } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyCancelStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } if !p.StopCalled { t.Fatal("stop should be called") } } func TestContext2Apply_cancelBlock(t *testing.T) { m := testModule(t, "apply-cancel-block") p := testProvider("aws") ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) applyCh := make(chan struct{}) p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { close(applyCh) for !ctx.sh.Stopped() { // Wait for stop to be called. We call Gosched here so that // the other goroutines can always be scheduled to set Stopped. runtime.Gosched() } // Sleep time.Sleep(100 * time.Millisecond) return testApplyFn(req) } if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } // Start the Apply in a goroutine var applyDiags tfdiags.Diagnostics stateCh := make(chan *states.State) go func() { state, diags := ctx.Apply() applyDiags = diags stateCh <- state }() stopDone := make(chan struct{}) go func() { defer close(stopDone) <-applyCh ctx.Stop() }() // Make sure that stop blocks select { case <-stopDone: t.Fatal("stop should block") case <-time.After(10 * time.Millisecond): } // Wait for stop select { case <-stopDone: case <-time.After(500 * time.Millisecond): t.Fatal("stop should be done") } // Wait for apply to complete state := <-stateCh // only expecting an early exit error if !applyDiags.HasErrors() { t.Fatal("expected early exit error") } for _, d := range applyDiags { desc := d.Description() if desc.Summary != "execution halted" { t.Fatalf("unexpected error: %v", applyDiags.Err()) } } checkStateString(t, state, ` aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] num = 2 type = aws_instance `) } func TestContext2Apply_cancelProvisioner(t *testing.T) { m := testModule(t, "apply-cancel-provisioner") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pr := testProvisioner() pr.GetSchemaResponse = provisioners.GetSchemaResponse{ Provisioner: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { Type: cty.String, Optional: true, }, }, }, } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) prStopped := make(chan struct{}) pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { // Start the stop process go ctx.Stop() <-prStopped return } pr.StopFn = func() error { close(prStopped) return nil } if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } // Start the Apply in a goroutine var applyDiags tfdiags.Diagnostics stateCh := make(chan *states.State) go func() { state, diags := ctx.Apply() applyDiags = diags stateCh <- state }() // Wait for completion state := <-stateCh // we are expecting only an early exit error if !applyDiags.HasErrors() { t.Fatal("expected early exit error") } for _, d := range applyDiags { desc := d.Description() if desc.Summary != "execution halted" { t.Fatalf("unexpected error: %v", applyDiags.Err()) } } checkStateString(t, state, ` aws_instance.foo: (tainted) ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] num = 2 type = aws_instance `) if !pr.StopCalled { t.Fatal("stop should be called") } } func TestContext2Apply_compute(t *testing.T) { m := testModule(t, "apply-compute") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ "num": { Type: cty.Number, Optional: true, }, "compute": { Type: cty.String, Optional: true, }, "compute_value": { Type: cty.String, Optional: true, }, "foo": { Type: cty.String, Optional: true, }, "id": { Type: cty.String, Computed: true, }, "type": { Type: cty.String, Computed: true, }, "value": { // Populated from compute_value because compute = "value" in the config fixture Type: cty.String, Computed: true, }, }, }, }, }) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) ctx.variables = InputValues{ "value": &InputValue{ Value: cty.NumberIntVal(1), SourceType: ValueFromCaller, }, } if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyComputeStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_countDecrease(t *testing.T) { m := testModule(t, "apply-count-dec") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[0]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","foo": "foo","type": "aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[1]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","foo": "foo","type": "aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[2]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar", "foo": "foo", "type": "aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("plan failed") } s, diags := ctx.Apply() assertNoErrors(t, diags) actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(testTerraformApplyCountDecStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_countDecreaseToOneX(t *testing.T) { m := testModule(t, "apply-count-dec-one") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[0]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar", "foo": "foo", "type": "aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[1]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[2]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(testTerraformApplyCountDecToOneStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } // https://github.com/PeoplePerHour/terraform/pull/11 // // This tests a case where both a "resource" and "resource.0" are in // the state file, which apparently is a reasonable backwards compatibility // concern found in the above 3rd party repo. func TestContext2Apply_countDecreaseToOneCorrupted(t *testing.T) { m := testModule(t, "apply-count-dec-one") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar", "foo": "foo", "type": "aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[0]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"baz", "type": "aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, 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 { got := strings.TrimSpace(legacyPlanComparisonString(ctx.State(), p.Changes)) want := strings.TrimSpace(testTerraformApplyCountDecToOneCorruptedPlanStr) if got != want { t.Fatalf("wrong plan result\ngot:\n%s\nwant:\n%s", got, want) } } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(testTerraformApplyCountDecToOneCorruptedStr) if actual != expected { t.Fatalf("wrong final state\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_countTainted(t *testing.T) { m := testModule(t, "apply-count-tainted") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[0]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"bar", "type": "aws_instance", "foo": "foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) { plan, diags := ctx.Plan() assertNoErrors(t, diags) got := strings.TrimSpace(legacyDiffComparisonString(plan.Changes)) want := strings.TrimSpace(` DESTROY/CREATE: aws_instance.foo[0] foo: "foo" => "foo" id: "bar" => "" type: "aws_instance" => "" CREATE: aws_instance.foo[1] foo: "" => "foo" id: "" => "" type: "" => "" `) if got != want { t.Fatalf("wrong plan\n\ngot:\n%s\n\nwant:\n%s", got, want) } } s, diags := ctx.Apply() assertNoErrors(t, diags) got := strings.TrimSpace(s.String()) want := strings.TrimSpace(` aws_instance.foo.0: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = foo type = aws_instance aws_instance.foo.1: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = foo type = aws_instance `) if got != want { t.Fatalf("wrong final state\n\ngot:\n%s\n\nwant:\n%s", got, want) } } func TestContext2Apply_countVariable(t *testing.T) { m := testModule(t, "apply-count-variable") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyCountVariableStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_countVariableRef(t *testing.T) { m := testModule(t, "apply-count-variable-ref") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyCountVariableRefStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_provisionerInterpCount(t *testing.T) { // This test ensures that a provisioner can interpolate a resource count // even though the provisioner expression is evaluated during the plan // walk. https://github.com/hashicorp/terraform/issues/16840 m, snap := testModuleWithSnapshot(t, "apply-provisioner-interp-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn pr := testProvisioner() Providers := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), } provisioners := map[string]provisioners.Factory{ "local-exec": testProvisionerFuncFixed(pr), } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: Providers, Provisioners: provisioners, }) plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("plan failed unexpectedly: %s", diags.Err()) } // We'll marshal and unmarshal the plan here, to ensure that we have // a clean new context as would be created if we separately ran // terraform plan -out=tfplan && terraform apply tfplan ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatal(err) } ctxOpts.Providers = Providers ctxOpts.Provisioners = provisioners ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("failed to create context for plan: %s", diags.Err()) } // Applying the plan should now succeed _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply failed unexpectedly: %s", diags.Err()) } // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner was not called") } } func TestContext2Apply_foreachVariable(t *testing.T) { m := testModule(t, "plan-for-each-unknown-value") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Variables: InputValues{ "foo": &InputValue{ Value: cty.StringVal("hello"), }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyForEachVariableStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_moduleBasic(t *testing.T) { m := testModule(t, "apply-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyModuleStr) if actual != expected { t.Fatalf("bad, expected:\n%s\n\nactual:\n%s", expected, actual) } } func TestContext2Apply_moduleDestroyOrder(t *testing.T) { m := testModule(t, "apply-module-destroy-order") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn // Create a custom apply function to track the order they were destroyed var order []string var orderLock sync.Mutex p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { id := req.PriorState.GetAttr("id").AsString() if id == "b" { // Pause briefly to make any race conditions more visible, since // missing edges here can cause undeterministic ordering. time.Sleep(100 * time.Millisecond) } orderLock.Lock() defer orderLock.Unlock() order = append(order, id) resp.NewState = req.PlannedState return resp } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Required: true}, "blah": {Type: cty.String, Optional: true}, "value": {Type: cty.String, Optional: true}, }, }, }, }) state := states.NewState() child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) child.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.a").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"a"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.b").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"b"}`), Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("module.child.aws_instance.a")}, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } expected := []string{"b", "a"} if !reflect.DeepEqual(order, expected) { t.Errorf("wrong order\ngot: %#v\nwant: %#v", order, expected) } { actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyModuleDestroyOrderStr) if actual != expected { t.Errorf("wrong final state\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } } func TestContext2Apply_moduleInheritAlias(t *testing.T) { m := testModule(t, "apply-module-provider-inherit-alias") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { val := req.Config.GetAttr("value") if val.IsNull() { return } root := req.Config.GetAttr("root") if !root.IsNull() { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("child should not get root")) } return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ` module.child: aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"].eu type = aws_instance `) } func TestContext2Apply_orphanResource(t *testing.T) { // This is a two-step test: // 1. Apply a configuration with resources that have count set. // This should place the empty resource object in the state to record // that each exists, and record any instances. // 2. Apply an empty configuration against the same state, which should // then clean up both the instances and the containing resource objects. p := testProvider("test") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "foo": {Type: cty.String, Optional: true}, }, }, }, }) // Step 1: create the resources and instances m := testModule(t, "apply-orphan-resource") ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() assertNoErrors(t, diags) state, diags := ctx.Apply() assertNoErrors(t, diags) // At this point both resources should be recorded in the state, along // with the single instance associated with test_thing.one. want := states.BuildState(func(s *states.SyncState) { providerAddr := addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, } oneAddr := addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "one", }.Absolute(addrs.RootModuleInstance) s.SetResourceProvider(oneAddr, providerAddr) s.SetResourceInstanceCurrent(oneAddr.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, providerAddr) }) if state.String() != want.String() { t.Fatalf("wrong state after step 1\n%s", cmp.Diff(want, state)) } // Step 2: update with an empty config, to destroy everything m = testModule(t, "empty") ctx = testContext2(t, &ContextOpts{ Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags = ctx.Plan() assertNoErrors(t, diags) state, diags = ctx.Apply() assertNoErrors(t, diags) // The state should now be _totally_ empty, with just an empty root module // (since that always exists) and no resources at all. want = states.NewState() if !cmp.Equal(state, want) { t.Fatalf("wrong state after step 2\ngot: %swant: %s", spew.Sdump(state), spew.Sdump(want)) } } func TestContext2Apply_moduleOrphanInheritAlias(t *testing.T) { m := testModule(t, "apply-module-provider-inherit-alias-orphan") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { val := req.Config.GetAttr("value") if val.IsNull() { return } root := req.Config.GetAttr("root") if !root.IsNull() { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("child should not get root")) } return } // Create a state with an orphan module state := states.NewState() child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) child.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if !p.ConfigureProviderCalled { t.Fatal("must call configure") } checkStateString(t, state, "") } func TestContext2Apply_moduleOrphanProvider(t *testing.T) { m := testModule(t, "apply-module-orphan-provider-inherit") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { val := req.Config.GetAttr("value") if val.IsNull() { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("value is not found")) } return } // Create a state with an orphan module state := states.NewState() child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) child.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } func TestContext2Apply_moduleOrphanGrandchildProvider(t *testing.T) { m := testModule(t, "apply-module-orphan-provider-inherit") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { val := req.Config.GetAttr("value") if val.IsNull() { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("value is not found")) } return } // Create a state with an orphan module that is nested (grandchild) state := states.NewState() child := state.EnsureModule(addrs.RootModuleInstance.Child("parent", addrs.NoKey).Child("child", addrs.NoKey)) child.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } func TestContext2Apply_moduleGrandchildProvider(t *testing.T) { m := testModule(t, "apply-module-grandchild-provider-inherit") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn var callLock sync.Mutex called := false p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { val := req.Config.GetAttr("value") if val.IsNull() { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("value is not found")) } callLock.Lock() called = true callLock.Unlock() return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } callLock.Lock() defer callLock.Unlock() if called != true { t.Fatalf("err: configure never called") } } // This tests an issue where all the providers in a module but not // in the root weren't being added to the root properly. In this test // case: aws is explicitly added to root, but "test" should be added to. // With the bug, it wasn't. func TestContext2Apply_moduleOnlyProvider(t *testing.T) { m := testModule(t, "apply-module-only-provider") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pTest := testProvider("test") pTest.ApplyResourceChangeFn = testApplyFn pTest.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), addrs.NewDefaultProvider("test"): testProviderFuncFixed(pTest), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyModuleOnlyProviderStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_moduleProviderAlias(t *testing.T) { m := testModule(t, "apply-module-provider-alias") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyModuleProviderAliasStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_moduleProviderAliasTargets(t *testing.T) { m := testModule(t, "apply-module-provider-alias") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.ConfigResource{ Module: addrs.RootModule, Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "nonexistent", Name: "thing", }, }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(` `) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_moduleProviderCloseNested(t *testing.T) { m := testModule(t, "apply-module-provider-close-nested") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } // Tests that variables used as module vars that reference data that // already exists in the state and requires no diff works properly. This // fixes an issue faced where module variables were pruned because they were // accessing "non-existent" resources (they existed, just not in the graph // cause they weren't in the diff). func TestContext2Apply_moduleVarRefExisting(t *testing.T) { m := testModule(t, "apply-ref-existing") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo","foo":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyModuleVarRefExistingStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_moduleVarResourceCount(t *testing.T) { m := testModule(t, "apply-module-var-resource-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Variables: InputValues{ "num": &InputValue{ Value: cty.NumberIntVal(2), SourceType: ValueFromCaller, }, }, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Variables: InputValues{ "num": &InputValue{ Value: cty.NumberIntVal(5), SourceType: ValueFromCaller, }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } // GH-819 func TestContext2Apply_moduleBool(t *testing.T) { m := testModule(t, "apply-module-bool") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyModuleBoolStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } // Tests that a module can be targeted and everything is properly created. // This adds to the plan test to also just verify that apply works. func TestContext2Apply_moduleTarget(t *testing.T) { m := testModule(t, "plan-targeted-cross-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Child("B", addrs.NoKey), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ` module.A: aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = bar type = aws_instance Outputs: value = foo module.B: aws_instance.bar: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = foo type = aws_instance Dependencies: module.A.aws_instance.foo `) } func TestContext2Apply_multiProvider(t *testing.T) { m := testModule(t, "apply-multi-provider") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pDO := testProvider("do") pDO.ApplyResourceChangeFn = testApplyFn pDO.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), addrs.NewDefaultProvider("do"): testProviderFuncFixed(pDO), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } mod := state.RootModule() if len(mod.Resources) < 2 { t.Fatalf("bad: %#v", mod.Resources) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyMultiProviderStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_multiProviderDestroy(t *testing.T) { m := testModule(t, "apply-multi-provider-destroy") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "addr": {Type: cty.String, Optional: true}, }, }, ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "foo": {Type: cty.String, Optional: true}, }, }, }, }) p2 := testProvider("vault") p2.ApplyResourceChangeFn = testApplyFn p2.PlanResourceChangeFn = testDiffFn p2.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "vault_instance": { Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, }, }, }) var state *states.State // First, create the instances { ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), addrs.NewDefaultProvider("vault"): testProviderFuncFixed(p2), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("errors during create plan: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("errors during create apply: %s", diags.Err()) } state = s } // Destroy them { // Verify that aws_instance.bar is destroyed first var checked bool var called int32 var lock sync.Mutex applyFn := func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { lock.Lock() defer lock.Unlock() if req.TypeName == "aws_instance" { checked = true // Sleep to allow parallel execution time.Sleep(50 * time.Millisecond) // Verify that called is 0 (dep not called) if atomic.LoadInt32(&called) != 0 { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("nothing else should be called")) return resp } } atomic.AddInt32(&called, 1) return testApplyFn(req) } // Set the apply functions p.ApplyResourceChangeFn = applyFn p2.ApplyResourceChangeFn = applyFn ctx := testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, State: state, Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), addrs.NewDefaultProvider("vault"): testProviderFuncFixed(p2), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("errors during destroy plan: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("errors during destroy apply: %s", diags.Err()) } if !checked { t.Fatal("should be checked") } state = s } checkStateString(t, state, ``) } // This is like the multiProviderDestroy test except it tests that // dependent resources within a child module that inherit provider // configuration are still destroyed first. func TestContext2Apply_multiProviderDestroyChild(t *testing.T) { m := testModule(t, "apply-multi-provider-destroy-child") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, }, }, ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "foo": {Type: cty.String, Optional: true}, }, }, }, }) p2 := testProvider("vault") p2.ApplyResourceChangeFn = testApplyFn p2.PlanResourceChangeFn = testDiffFn p2.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "vault_instance": { Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, }, }, }) var state *states.State // First, create the instances { ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), addrs.NewDefaultProvider("vault"): testProviderFuncFixed(p2), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state = s } // Destroy them { // Verify that aws_instance.bar is destroyed first var checked bool var called int32 var lock sync.Mutex applyFn := func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { lock.Lock() defer lock.Unlock() if req.TypeName == "aws_instance" { checked = true // Sleep to allow parallel execution time.Sleep(50 * time.Millisecond) // Verify that called is 0 (dep not called) if atomic.LoadInt32(&called) != 0 { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("nothing else should be called")) return resp } } atomic.AddInt32(&called, 1) return testApplyFn(req) } // Set the apply functions p.ApplyResourceChangeFn = applyFn p2.ApplyResourceChangeFn = applyFn ctx := testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, State: state, Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), addrs.NewDefaultProvider("vault"): testProviderFuncFixed(p2), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if !checked { t.Fatal("should be checked") } state = s } checkStateString(t, state, ` `) } func TestContext2Apply_multiVar(t *testing.T) { m := testModule(t, "apply-multi-var") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn // First, apply with a count of 3 ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Variables: InputValues{ "num": &InputValue{ Value: cty.NumberIntVal(3), SourceType: ValueFromCaller, }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := state.RootModule().OutputValues["output"] expected := cty.StringVal("bar0,bar1,bar2") if actual == nil || actual.Value != expected { t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) } t.Logf("Initial state: %s", state.String()) // Apply again, reduce the count to 1 { ctx := testContext2(t, &ContextOpts{ Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Variables: InputValues{ "num": &InputValue{ Value: cty.NumberIntVal(1), SourceType: ValueFromCaller, }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } t.Logf("End state: %s", state.String()) actual := state.RootModule().OutputValues["output"] if actual == nil { t.Fatal("missing output") } expected := cty.StringVal("bar0") if actual.Value != expected { t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) } } } // This is a holistic test of multi-var (aka "splat variable") handling // across several different Terraform subsystems. This is here because // historically there were quirky differences in handling across different // parts of Terraform and so here we want to assert the expected behavior and // ensure that it remains consistent in future. func TestContext2Apply_multiVarComprehensive(t *testing.T) { m := testModule(t, "apply-multi-var-comprehensive") p := testProvider("test") configs := map[string]cty.Value{} var configsLock sync.Mutex p.ApplyResourceChangeFn = testApplyFn p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { proposed := req.ProposedNewState configsLock.Lock() defer configsLock.Unlock() key := proposed.GetAttr("key").AsString() // This test was originally written using the legacy p.PlanResourceChangeFn interface, // and so the assertions below expect an old-style ResourceConfig, which // we'll construct via our shim for now to avoid rewriting all of the // assertions. configs[key] = req.ProposedNewState retVals := make(map[string]cty.Value) for it := proposed.ElementIterator(); it.Next(); { idxVal, val := it.Element() idx := idxVal.AsString() switch idx { case "id": retVals[idx] = cty.UnknownVal(cty.String) case "name": retVals[idx] = cty.StringVal(key) default: retVals[idx] = val } } return providers.PlanResourceChangeResponse{ PlannedState: cty.ObjectVal(retVals), } } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ "key": {Type: cty.String, Required: true}, "source_id": {Type: cty.String, Optional: true}, "source_name": {Type: cty.String, Optional: true}, "first_source_id": {Type: cty.String, Optional: true}, "first_source_name": {Type: cty.String, Optional: true}, "source_ids": {Type: cty.List(cty.String), Optional: true}, "source_names": {Type: cty.List(cty.String), Optional: true}, "source_ids_from_func": {Type: cty.List(cty.String), Optional: true}, "source_names_from_func": {Type: cty.List(cty.String), Optional: true}, "source_ids_wrapped": {Type: cty.List(cty.List(cty.String)), Optional: true}, "source_names_wrapped": {Type: cty.List(cty.List(cty.String)), Optional: true}, "id": {Type: cty.String, Computed: true}, "name": {Type: cty.String, Computed: true}, }, }, }, }) // First, apply with a count of 3 ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, Variables: InputValues{ "num": &InputValue{ Value: cty.NumberIntVal(3), SourceType: ValueFromCaller, }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatalf("errors during plan") } checkConfig := func(key string, want cty.Value) { configsLock.Lock() defer configsLock.Unlock() got, ok := configs[key] if !ok { t.Errorf("no config recorded for %s; expected a configuration", key) return } t.Run("config for "+key, func(t *testing.T) { for _, problem := range deep.Equal(got, want) { t.Errorf(problem) } }) } checkConfig("multi_count_var.0", cty.ObjectVal(map[string]cty.Value{ "source_id": cty.UnknownVal(cty.String), "source_name": cty.StringVal("source.0"), })) checkConfig("multi_count_var.2", cty.ObjectVal(map[string]cty.Value{ "source_id": cty.UnknownVal(cty.String), "source_name": cty.StringVal("source.2"), })) checkConfig("multi_count_derived.0", cty.ObjectVal(map[string]cty.Value{ "source_id": cty.UnknownVal(cty.String), "source_name": cty.StringVal("source.0"), })) checkConfig("multi_count_derived.2", cty.ObjectVal(map[string]cty.Value{ "source_id": cty.UnknownVal(cty.String), "source_name": cty.StringVal("source.2"), })) checkConfig("whole_splat", cty.ObjectVal(map[string]cty.Value{ "source_ids": cty.ListVal([]cty.Value{ cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), }), "source_names": cty.ListVal([]cty.Value{ cty.StringVal("source.0"), cty.StringVal("source.1"), cty.StringVal("source.2"), }), "source_ids_from_func": cty.UnknownVal(cty.String), "source_names_from_func": cty.ListVal([]cty.Value{ cty.StringVal("source.0"), cty.StringVal("source.1"), cty.StringVal("source.2"), }), "source_ids_wrapped": cty.ListVal([]cty.Value{ cty.ListVal([]cty.Value{ cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), }), }), "source_names_wrapped": cty.ListVal([]cty.Value{ cty.ListVal([]cty.Value{ cty.StringVal("source.0"), cty.StringVal("source.1"), cty.StringVal("source.2"), }), }), "first_source_id": cty.UnknownVal(cty.String), "first_source_name": cty.StringVal("source.0"), })) checkConfig("child.whole_splat", cty.ObjectVal(map[string]cty.Value{ "source_ids": cty.ListVal([]cty.Value{ cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), }), "source_names": cty.ListVal([]cty.Value{ cty.StringVal("source.0"), cty.StringVal("source.1"), cty.StringVal("source.2"), }), "source_ids_wrapped": cty.ListVal([]cty.Value{ cty.ListVal([]cty.Value{ cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), }), }), "source_names_wrapped": cty.ListVal([]cty.Value{ cty.ListVal([]cty.Value{ cty.StringVal("source.0"), cty.StringVal("source.1"), cty.StringVal("source.2"), }), }), })) t.Run("apply", func(t *testing.T) { state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("error during apply: %s", diags.Err()) } want := map[string]interface{}{ "source_ids": []interface{}{"foo", "foo", "foo"}, "source_names": []interface{}{ "source.0", "source.1", "source.2", }, } got := map[string]interface{}{} for k, s := range state.RootModule().OutputValues { got[k] = hcl2shim.ConfigValueFromHCL2(s.Value) } if !reflect.DeepEqual(got, want) { t.Errorf( "wrong outputs\ngot: %s\nwant: %s", spew.Sdump(got), spew.Sdump(want), ) } }) } // Test that multi-var (splat) access is ordered by count, not by // value. func TestContext2Apply_multiVarOrder(t *testing.T) { m := testModule(t, "apply-multi-var-order") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn // First, apply with a count of 3 ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } t.Logf("State: %s", state.String()) actual := state.RootModule().OutputValues["should-be-11"] expected := cty.StringVal("index-11") if actual == nil || actual.Value != expected { t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) } } // Test that multi-var (splat) access is ordered by count, not by // value, through interpolations. func TestContext2Apply_multiVarOrderInterp(t *testing.T) { m := testModule(t, "apply-multi-var-order-interp") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn // First, apply with a count of 3 ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } t.Logf("State: %s", state.String()) actual := state.RootModule().OutputValues["should-be-11"] expected := cty.StringVal("baz-index-11") if actual == nil || actual.Value != expected { t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) } } // Based on GH-10440 where a graph edge wasn't properly being created // between a modified resource and a count instance being destroyed. func TestContext2Apply_multiVarCountDec(t *testing.T) { var s *states.State // First create resources. Nothing sneaky here. { m := testModule(t, "apply-multi-var-count-dec") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Variables: InputValues{ "num": &InputValue{ Value: cty.NumberIntVal(2), SourceType: ValueFromCaller, }, }, }) log.Print("\n========\nStep 1 Plan\n========") if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } log.Print("\n========\nStep 1 Apply\n========") state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } t.Logf("Step 1 state:\n%s", state) s = state } // Decrease the count by 1 and verify that everything happens in the // right order. m := testModule(t, "apply-multi-var-count-dec") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn // Verify that aws_instance.bar is modified first and nothing // else happens at the same time. { var checked bool var called int32 var lock sync.Mutex p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { lock.Lock() defer lock.Unlock() if !req.PlannedState.IsNull() { s := req.PlannedState.AsValueMap() if ami, ok := s["ami"]; ok && !ami.IsNull() && ami.AsString() == "special" { checked = true // Sleep to allow parallel execution time.Sleep(50 * time.Millisecond) // Verify that called is 0 (dep not called) if atomic.LoadInt32(&called) != 1 { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("nothing else should be called")) return } } } atomic.AddInt32(&called, 1) return testApplyFn(req) } ctx := testContext2(t, &ContextOpts{ State: s, Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Variables: InputValues{ "num": &InputValue{ Value: cty.NumberIntVal(1), SourceType: ValueFromCaller, }, }, }) log.Print("\n========\nStep 2 Plan\n========") plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } t.Logf("Step 2 plan:\n%s", legacyDiffComparisonString(plan.Changes)) log.Print("\n========\nStep 2 Apply\n========") _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } if !checked { t.Error("apply never called") } } } // Test that we can resolve a multi-var (splat) for the first resource // created in a non-root module, which happens when the module state doesn't // exist yet. // https://github.com/hashicorp/terraform/issues/14438 func TestContext2Apply_multiVarMissingState(t *testing.T) { m := testModule(t, "apply-multi-var-missing-state") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ "a_ids": {Type: cty.String, Optional: true}, "id": {Type: cty.String, Computed: true}, }, }, }, }) // First, apply with a count of 3 ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan failed: %s", diags.Err()) } // Before the relevant bug was fixed, Tdiagsaform would panic during apply. if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply failed: %s", diags.Err()) } // If we get here with no errors or panics then our test was successful. } func TestContext2Apply_outputOrphan(t *testing.T) { m := testModule(t, "apply-output-orphan") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetOutputValue("foo", cty.StringVal("bar"), false) root.SetOutputValue("bar", cty.StringVal("baz"), false) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyOutputOrphanStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_outputOrphanModule(t *testing.T) { m := testModule(t, "apply-output-orphan-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(testTerraformApplyOutputOrphanModuleStr) if actual != expected { t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual) } // now apply with no module in the config, which should remove the // remaining output ctx = testContext2(t, &ContextOpts{ Config: configs.NewEmptyConfig(), Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state.DeepCopy(), }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if !state.Empty() { t.Fatalf("wrong final state %s\nwant empty state", spew.Sdump(state)) } } func TestContext2Apply_providerComputedVar(t *testing.T) { m := testModule(t, "apply-provider-computed") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn pTest := testProvider("test") pTest.ApplyResourceChangeFn = testApplyFn pTest.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), addrs.NewDefaultProvider("test"): testProviderFuncFixed(pTest), }, }) p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { val := req.Config.GetAttr("value") if val.IsNull() { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("value is not found")) return } return } if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } func TestContext2Apply_providerConfigureDisabled(t *testing.T) { m := testModule(t, "apply-provider-configure-disabled") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { val := req.Config.GetAttr("value") if val.IsNull() { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("value is not found")) } return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } if !p.ConfigureProviderCalled { t.Fatal("configure never called") } } func TestContext2Apply_provisionerModule(t *testing.T) { m := testModule(t, "apply-provisioner-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pr := testProvisioner() pr.GetSchemaResponse = provisioners.GetSchemaResponse{ Provisioner: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, }, } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerModuleStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } } func TestContext2Apply_Provisioner_compute(t *testing.T) { m := testModule(t, "apply-provisioner-compute") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { val := req.Config.GetAttr("command").AsString() if val != "computed_value" { t.Fatalf("bad value for foo: %q", val) } req.UIOutput.Output(fmt.Sprintf("Executing: %q", val)) return } h := new(MockHook) ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, Variables: InputValues{ "value": &InputValue{ Value: cty.NumberIntVal(1), SourceType: ValueFromCaller, }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } // Verify output was rendered if !h.ProvisionOutputCalled { t.Fatalf("ProvisionOutput hook not called") } if got, want := h.ProvisionOutputMessage, `Executing: "computed_value"`; got != want { t.Errorf("expected output to be %q, but was %q", want, got) } } func TestContext2Apply_provisionerCreateFail(t *testing.T) { m := testModule(t, "apply-provisioner-fail-create") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { resp := testApplyFn(req) resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error")) return resp } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should error") } got := strings.TrimSpace(state.String()) want := strings.TrimSpace(testTerraformApplyProvisionerFailCreateStr) if got != want { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", got, want) } } func TestContext2Apply_provisionerCreateFailNoId(t *testing.T) { m := testModule(t, "apply-provisioner-fail-create") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error")) return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should error") } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerFailCreateNoIdStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_provisionerFail(t *testing.T) { m := testModule(t, "apply-provisioner-fail") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pr := testProvisioner() pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("EXPLOSION")) return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should error") } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerFailStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-provisioner-fail-create-before") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("EXPLOSION")) return } state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","require_new":"abc"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should error") } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerFailCreateBeforeDestroyStr) if actual != expected { t.Fatalf("expected:\n%s\n:got\n%s", expected, actual) } } func TestContext2Apply_error_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-error-create-before") p := testProvider("aws") state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar", "require_new": "abc","type":"aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("placeholder error from ApplyFn")) return } p.PlanResourceChangeFn = testDiffFn if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should have error") } if got, want := diags.Err().Error(), "placeholder error from ApplyFn"; got != want { // We're looking for our artificial error from ApplyFn above, whose // message is literally "placeholder error from ApplyFn". t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyErrorCreateBeforeDestroyStr) if actual != expected { t.Fatalf("wrong final state\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_errorDestroy_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-error-create-before") p := testProvider("aws") state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar", "require_new": "abc"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { // Fail the destroy! if req.PlannedState.IsNull() { resp.NewState = req.PriorState resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error")) return } return testApplyFn(req) } p.PlanResourceChangeFn = testDiffFn if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should have error") } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyErrorDestroyCreateBeforeDestroyStr) if actual != expected { t.Fatalf("bad: actual:\n%s\n\nexpected:\n%s", actual, expected) } } func TestContext2Apply_multiDepose_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-multi-depose-create-before-destroy") p := testProvider("aws") ps := map[addrs.Provider]providers.Factory{addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p)} p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ "require_new": {Type: cty.String, Optional: true}, "id": {Type: cty.String, Computed: true}, }, }, }, }) state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.web").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: ps, State: state, Variables: InputValues{ "require_new": &InputValue{ Value: cty.StringVal("yes"), }, }, }) createdInstanceId := "bar" // Create works createFunc := func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { s := req.PlannedState.AsValueMap() s["id"] = cty.StringVal(createdInstanceId) resp.NewState = cty.ObjectVal(s) return } // Destroy starts broken destroyFunc := func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { resp.NewState = req.PriorState resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("destroy failed")) return } p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { if req.PlannedState.IsNull() { return destroyFunc(req) } else { return createFunc(req) } } if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } // Destroy is broken, so even though CBD successfully replaces the instance, // we'll have to save the Deposed instance to destroy later state, diags := ctx.Apply() if diags == nil { t.Fatal("should have error") } checkStateString(t, state, ` aws_instance.web: (1 deposed) ID = bar provider = provider["registry.terraform.io/hashicorp/aws"] require_new = yes Deposed ID 1 = foo `) createdInstanceId = "baz" ctx = testContext2(t, &ContextOpts{ Config: m, Providers: ps, State: state, Variables: InputValues{ "require_new": &InputValue{ Value: cty.StringVal("baz"), }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } // We're replacing the primary instance once again. Destroy is _still_ // broken, so the Deposed list gets longer state, diags = ctx.Apply() if diags == nil { t.Fatal("should have error") } // For this one we can't rely on checkStateString because its result is // not deterministic when multiple deposed objects are present. Instead, // we will probe the state object directly. { is := state.RootModule().Resources["aws_instance.web"].Instances[addrs.NoKey] if is.Current == nil { t.Fatalf("no current object for aws_instance web; should have one") } if !bytes.Contains(is.Current.AttrsJSON, []byte("baz")) { t.Fatalf("incorrect current object attrs %s; want id=baz", is.Current.AttrsJSON) } if got, want := len(is.Deposed), 2; got != want { t.Fatalf("wrong number of deposed instances %d; want %d", got, want) } var foos, bars int for _, obj := range is.Deposed { if bytes.Contains(obj.AttrsJSON, []byte("foo")) { foos++ } if bytes.Contains(obj.AttrsJSON, []byte("bar")) { bars++ } } if got, want := foos, 1; got != want { t.Fatalf("wrong number of deposed instances with id=foo %d; want %d", got, want) } if got, want := bars, 1; got != want { t.Fatalf("wrong number of deposed instances with id=bar %d; want %d", got, want) } } // Destroy partially fixed! destroyFunc = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { s := req.PriorState.AsValueMap() id := s["id"].AsString() if id == "foo" || id == "baz" { resp.NewState = cty.NullVal(req.PriorState.Type()) } else { resp.NewState = req.PriorState resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("destroy partially failed")) } return } createdInstanceId = "qux" ctx = testContext2(t, &ContextOpts{ Config: m, Providers: ps, State: state, Variables: InputValues{ "require_new": &InputValue{ Value: cty.StringVal("qux"), }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags = ctx.Apply() // Expect error because 1/2 of Deposed destroys failed if diags == nil { t.Fatal("should have error") } // foo and baz are now gone, bar sticks around checkStateString(t, state, ` aws_instance.web: (1 deposed) ID = qux provider = provider["registry.terraform.io/hashicorp/aws"] require_new = qux Deposed ID 1 = bar `) // Destroy working fully! destroyFunc = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { resp.NewState = cty.NullVal(req.PriorState.Type()) return } createdInstanceId = "quux" ctx = testContext2(t, &ContextOpts{ Config: m, Providers: ps, State: state, Variables: InputValues{ "require_new": &InputValue{ Value: cty.StringVal("quux"), }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatal("should not have error:", diags.Err()) } // And finally the state is clean checkStateString(t, state, ` aws_instance.web: ID = quux provider = provider["registry.terraform.io/hashicorp/aws"] require_new = quux `) } // Verify that a normal provisioner with on_failure "continue" set won't // taint the resource and continues executing. func TestContext2Apply_provisionerFailContinue(t *testing.T) { m := testModule(t, "apply-provisioner-fail-continue") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provisioner error")) return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ` aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = bar type = aws_instance `) // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } } // Verify that a normal provisioner with on_failure "continue" records // the error with the hook. func TestContext2Apply_provisionerFailContinueHook(t *testing.T) { h := new(MockHook) m := testModule(t, "apply-provisioner-fail-continue") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provisioner error")) return } ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } if !h.PostProvisionInstanceStepCalled { t.Fatal("PostProvisionInstanceStep not called") } if h.PostProvisionInstanceStepErrorArg == nil { t.Fatal("should have error") } } func TestContext2Apply_provisionerDestroy(t *testing.T) { m := testModule(t, "apply-provisioner-destroy") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { val := req.Config.GetAttr("command").AsString() if val != "destroy a bar" { t.Fatalf("bad value for foo: %q", val) } return } state := states.NewState() root := state.RootModule() root.SetResourceInstanceCurrent( mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","foo":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, State: state, PlanMode: plans.DestroyMode, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ``) // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } } // Verify that on destroy provisioner failure, nothing happens to the instance func TestContext2Apply_provisionerDestroyFail(t *testing.T) { m := testModule(t, "apply-provisioner-destroy") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provisioner error")) return } state := states.NewState() root := state.RootModule() root.SetResourceInstanceCurrent( mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","foo":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, State: state, PlanMode: plans.DestroyMode, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should error") } checkStateString(t, state, ` aws_instance.foo["a"]: ID = bar provider = provider["registry.terraform.io/hashicorp/aws"] foo = bar `) // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } } // Verify that on destroy provisioner failure with "continue" that // we continue to the next provisioner. func TestContext2Apply_provisionerDestroyFailContinue(t *testing.T) { m := testModule(t, "apply-provisioner-destroy-continue") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn var l sync.Mutex var calls []string pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { val := req.Config.GetAttr("command") if val.IsNull() { t.Fatalf("bad value for foo: %#v", val) } l.Lock() defer l.Unlock() calls = append(calls, val.AsString()) resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provisioner error")) return } state := states.NewState() root := state.RootModule() root.SetResourceInstanceCurrent( mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, State: state, PlanMode: plans.DestroyMode, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ``) // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } expected := []string{"one", "two"} if !reflect.DeepEqual(calls, expected) { t.Fatalf("wrong commands\ngot: %#v\nwant: %#v", calls, expected) } } // Verify that on destroy provisioner failure with "continue" that // we continue to the next provisioner. But if the next provisioner defines // to fail, then we fail after running it. func TestContext2Apply_provisionerDestroyFailContinueFail(t *testing.T) { m := testModule(t, "apply-provisioner-destroy-fail") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn var l sync.Mutex var calls []string pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { val := req.Config.GetAttr("command") if val.IsNull() { t.Fatalf("bad value for foo: %#v", val) } l.Lock() defer l.Unlock() calls = append(calls, val.AsString()) resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provisioner error")) return } state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, State: state, PlanMode: plans.DestroyMode, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("apply succeeded; wanted error from second provisioner") } checkStateString(t, state, ` aws_instance.foo: ID = bar provider = provider["registry.terraform.io/hashicorp/aws"] `) // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } expected := []string{"one", "two"} if !reflect.DeepEqual(calls, expected) { t.Fatalf("bad: %#v", calls) } } // Verify destroy provisioners are not run for tainted instances. func TestContext2Apply_provisionerDestroyTainted(t *testing.T) { m := testModule(t, "apply-provisioner-destroy") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn destroyCalled := false pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { expected := "create a b" val := req.Config.GetAttr("command") if val.AsString() != expected { t.Fatalf("bad value for command: %#v", val) } return } state := states.NewState() root := state.RootModule() root.SetResourceInstanceCurrent( mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, Variables: InputValues{ "input": &InputValue{ Value: cty.MapVal(map[string]cty.Value{ "a": cty.StringVal("b"), }), SourceType: ValueFromInput, }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ` aws_instance.foo["a"]: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = bar type = aws_instance `) // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } if destroyCalled { t.Fatal("destroy should not be called") } } func TestContext2Apply_provisionerResourceRef(t *testing.T) { m := testModule(t, "apply-provisioner-resource-ref") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pr := testProvisioner() pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { val := req.Config.GetAttr("command") if val.AsString() != "2" { t.Fatalf("bad value for command: %#v", val) } return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerResourceRefStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } } func TestContext2Apply_provisionerSelfRef(t *testing.T) { m := testModule(t, "apply-provisioner-self-ref") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { val := req.Config.GetAttr("command") if val.AsString() != "bar" { t.Fatalf("bad value for command: %#v", val) } return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerSelfRefStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } } func TestContext2Apply_provisionerMultiSelfRef(t *testing.T) { var lock sync.Mutex commands := make([]string, 0, 5) m := testModule(t, "apply-provisioner-multi-self-ref") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { lock.Lock() defer lock.Unlock() val := req.Config.GetAttr("command") if val.IsNull() { t.Fatalf("bad value for command: %#v", val) } commands = append(commands, val.AsString()) return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerMultiSelfRefStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } // Verify our result sort.Strings(commands) expectedCommands := []string{"number 0", "number 1", "number 2"} if !reflect.DeepEqual(commands, expectedCommands) { t.Fatalf("bad: %#v", commands) } } func TestContext2Apply_provisionerMultiSelfRefSingle(t *testing.T) { var lock sync.Mutex order := make([]string, 0, 5) m := testModule(t, "apply-provisioner-multi-self-ref-single") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { lock.Lock() defer lock.Unlock() val := req.Config.GetAttr("order") if val.IsNull() { t.Fatalf("no val for order") } order = append(order, val.AsString()) return } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerMultiSelfRefSingleStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } // Verify our result sort.Strings(order) expectedOrder := []string{"0", "1", "2"} if !reflect.DeepEqual(order, expectedOrder) { t.Fatalf("bad: %#v", order) } } func TestContext2Apply_provisionerExplicitSelfRef(t *testing.T) { m := testModule(t, "apply-provisioner-explicit-self-ref") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { val := req.Config.GetAttr("command") if val.IsNull() || val.AsString() != "bar" { t.Fatalf("bad value for command: %#v", val) } return } var state *states.State { ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner not invoked") } } { ctx := testContext2(t, &ContextOpts{ Config: m, PlanMode: plans.DestroyMode, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ``) } } func TestContext2Apply_provisionerForEachSelfRef(t *testing.T) { m := testModule(t, "apply-provisioner-for-each-self") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { val := req.Config.GetAttr("command") if val.IsNull() { t.Fatalf("bad value for command: %#v", val) } return resp } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } _, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } } // Provisioner should NOT run on a diff, only create func TestContext2Apply_Provisioner_Diff(t *testing.T) { m := testModule(t, "apply-provisioner-diff") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("plan failed") } state, diags := ctx.Apply() if diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("apply failed") } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerDiffStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner was not called on first apply") } pr.ProvisionResourceCalled = false // Change the state to force a diff mod := state.RootModule() obj := mod.Resources["aws_instance.bar"].Instances[addrs.NoKey].Current var attrs map[string]interface{} err := json.Unmarshal(obj.AttrsJSON, &attrs) if err != nil { t.Fatal(err) } attrs["foo"] = "baz" obj.AttrsJSON, err = json.Marshal(attrs) if err != nil { t.Fatal(err) } // Re-create context with state ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("plan failed") } state2, diags := ctx.Apply() if diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("apply failed") } actual = strings.TrimSpace(state2.String()) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Verify apply was NOT invoked if pr.ProvisionResourceCalled { t.Fatalf("provisioner was called on second apply; should not have been") } } func TestContext2Apply_outputDiffVars(t *testing.T) { m := testModule(t, "apply-good") p := testProvider("aws") state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.baz").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) p.PlanResourceChangeFn = testDiffFn //func(info *InstanceInfo, s *InstanceState, rc *ResourceConfig) (*InstanceDiff, error) { // d := &InstanceDiff{ // Attributes: map[string]*ResourceAttrDiff{}, // } // if new, ok := rc.Get("value"); ok { // d.Attributes["value"] = &ResourceAttrDiff{ // New: new.(string), // } // } // if new, ok := rc.Get("foo"); ok { // d.Attributes["foo"] = &ResourceAttrDiff{ // New: new.(string), // } // } else if rc.IsComputed("foo") { // d.Attributes["foo"] = &ResourceAttrDiff{ // NewComputed: true, // Type: DiffAttrOutput, // This doesn't actually really do anything anymore, but this test originally set it. // } // } // if new, ok := rc.Get("num"); ok { // d.Attributes["num"] = &ResourceAttrDiff{ // New: fmt.Sprintf("%#v", new), // } // } // return d, nil //} if _, diags := ctx.Plan(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("plan failed") } if _, diags := ctx.Apply(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("apply failed") } } func TestContext2Apply_destroyX(t *testing.T) { m := testModule(t, "apply-destroy") h := new(HookRecordApplyOrder) p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // Next, plan and apply a destroy operation h.Active = true ctx = testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, State: state, Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // Test that things were destroyed actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyDestroyStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Test that things were destroyed _in the right order_ expected2 := []string{"aws_instance.bar", "aws_instance.foo"} actual2 := h.IDs if !reflect.DeepEqual(actual2, expected2) { t.Fatalf("expected: %#v\n\ngot:%#v", expected2, actual2) } } func TestContext2Apply_destroyOrder(t *testing.T) { m := testModule(t, "apply-destroy") h := new(HookRecordApplyOrder) p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } t.Logf("State 1: %s", state) // Next, plan and apply a destroy h.Active = true ctx = testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, State: state, Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // Test that things were destroyed actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyDestroyStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Test that things were destroyed _in the right order_ expected2 := []string{"aws_instance.bar", "aws_instance.foo"} actual2 := h.IDs if !reflect.DeepEqual(actual2, expected2) { t.Fatalf("expected: %#v\n\ngot:%#v", expected2, actual2) } } // https://github.com/hashicorp/terraform/issues/2767 func TestContext2Apply_destroyModulePrefix(t *testing.T) { m := testModule(t, "apply-destroy-module-resource-prefix") h := new(MockHook) p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // Verify that we got the apply info correct if v := h.PreApplyAddr.String(); v != "module.child.aws_instance.foo" { t.Fatalf("bad: %s", v) } // Next, plan and apply a destroy operation and reset the hook h = new(MockHook) ctx = testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, State: state, Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // Test that things were destroyed if v := h.PreApplyAddr.String(); v != "module.child.aws_instance.foo" { t.Fatalf("bad: %s", v) } } func TestContext2Apply_destroyNestedModule(t *testing.T) { m := testModule(t, "apply-destroy-nested-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn 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"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) // First plan and apply a create operation if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // Test that things were destroyed actual := strings.TrimSpace(s.String()) if actual != "" { t.Fatalf("expected no state, got: %s", actual) } } func TestContext2Apply_destroyDeeplyNestedModule(t *testing.T) { m := testModule(t, "apply-destroy-deeply-nested-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn 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"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) // First plan and apply a create operation if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // Test that things were destroyed if !s.Empty() { t.Fatalf("wrong final state %s\nwant empty state", spew.Sdump(s)) } } // https://github.com/hashicorp/terraform/issues/5440 func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) { m, snap := testModuleWithSnapshot(t, "apply-destroy-module-with-attrs") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn var state *states.State { ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan diags: %s", diags.Err()) } else { t.Logf("Step 1 plan: %s", legacyDiffComparisonString(p.Changes)) } var diags tfdiags.Diagnostics state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errs: %s", diags.Err()) } t.Logf("Step 1 state: %s", state) } h := new(HookRecordApplyOrder) h.Active = true { ctx := testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, Config: m, State: state, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("destroy plan err: %s", diags.Err()) } t.Logf("Step 2 plan: %s", legacyDiffComparisonString(plan.Changes)) ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatalf("failed to round-trip through planfile: %s", err) } ctxOpts.Providers = map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), } ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("destroy apply err: %s", diags.Err()) } t.Logf("Step 2 state: %s", state) } //Test that things were destroyed if state.HasResources() { t.Fatal("expected empty state, got:", state) } } func TestContext2Apply_destroyWithModuleVariableAndCount(t *testing.T) { m, snap := testModuleWithSnapshot(t, "apply-destroy-mod-var-and-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn var state *states.State var diags tfdiags.Diagnostics { ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan err: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply err: %s", diags.Err()) } } h := new(HookRecordApplyOrder) h.Active = true { ctx := testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, Config: m, State: state, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("destroy plan err: %s", diags.Err()) } ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatalf("failed to round-trip through planfile: %s", err) } ctxOpts.Providers = map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), } ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("destroy apply err: %s", diags.Err()) } } //Test that things were destroyed actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(` `) if actual != expected { t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual) } } func TestContext2Apply_destroyTargetWithModuleVariableAndCount(t *testing.T) { m := testModule(t, "apply-destroy-mod-var-and-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn var state *states.State var diags tfdiags.Diagnostics { ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan err: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply err: %s", diags.Err()) } } { ctx := testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Child("child", addrs.NoKey), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("plan err: %s", diags) } if len(diags) != 1 { // Should have one warning that -target is in effect. t.Fatalf("got %d diagnostics in plan; want 1", len(diags)) } if got, want := diags[0].Severity(), tfdiags.Warning; got != want { t.Errorf("wrong diagnostic severity %#v; want %#v", got, want) } if got, want := diags[0].Description().Summary, "Resource targeting is in effect"; got != want { t.Errorf("wrong diagnostic summary %#v; want %#v", got, want) } // Destroy, targeting the module explicitly state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("destroy apply err: %s", diags) } if len(diags) != 1 { t.Fatalf("got %d diagnostics; want 1", len(diags)) } if got, want := diags[0].Severity(), tfdiags.Warning; got != want { t.Errorf("wrong diagnostic severity %#v; want %#v", got, want) } if got, want := diags[0].Description().Summary, "Applied changes may be incomplete"; got != want { t.Errorf("wrong diagnostic summary %#v; want %#v", got, want) } } //Test that things were destroyed actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(``) if actual != expected { t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual) } } func TestContext2Apply_destroyWithModuleVariableAndCountNested(t *testing.T) { m, snap := testModuleWithSnapshot(t, "apply-destroy-mod-var-and-count-nested") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn var state *states.State var diags tfdiags.Diagnostics { ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan err: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply err: %s", diags.Err()) } } h := new(HookRecordApplyOrder) h.Active = true { ctx := testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, Config: m, State: state, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("destroy plan err: %s", diags.Err()) } ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatalf("failed to round-trip through planfile: %s", err) } ctxOpts.Providers = map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), } ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("destroy apply err: %s", diags.Err()) } } //Test that things were destroyed actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(` `) if actual != expected { t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual) } } func TestContext2Apply_destroyOutputs(t *testing.T) { m := testModule(t, "apply-destroy-outputs") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { // add the required id m := req.Config.AsValueMap() m["id"] = cty.StringVal("foo") return providers.ReadDataSourceResponse{ State: cty.ObjectVal(m), } } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // Next, plan and apply a destroy operation ctx = testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, State: state, Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } mod := state.RootModule() if len(mod.Resources) > 0 { t.Fatalf("expected no resources, got: %#v", mod) } // destroying again should produce no errors ctx = testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, State: state, Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatal(diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatal(diags.Err()) } } func TestContext2Apply_destroyOrphan(t *testing.T) { m := testModule(t, "apply-error") p := testProvider("aws") state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.baz").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) p.PlanResourceChangeFn = testDiffFn if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } mod := s.RootModule() if _, ok := mod.Resources["aws_instance.baz"]; ok { t.Fatalf("bad: %#v", mod.Resources) } } func TestContext2Apply_destroyTaintedProvisioner(t *testing.T) { m := testModule(t, "apply-destroy-provisioner") p := testProvider("aws") pr := testProvisioner() p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, State: state, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if pr.ProvisionResourceCalled { t.Fatal("provisioner should not be called") } actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace("") if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_error(t *testing.T) { errored := false m := testModule(t, "apply-error") p := testProvider("aws") ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { if errored { resp.NewState = req.PlannedState resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error")) return } errored = true return testApplyFn(req) } p.PlanResourceChangeFn = testDiffFn if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should have error") } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyErrorStr) if actual != expected { t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual) } } func TestContext2Apply_errorDestroy(t *testing.T) { m := testModule(t, "empty") p := testProvider("test") p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true}, }, }, }, }) p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { // Should actually be called for this test, because Terraform Core // constructs the plan for a destroy operation itself. return providers.PlanResourceChangeResponse{ PlannedState: req.ProposedNewState, } } p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { // The apply (in this case, a destroy) always fails, so we can verify // that the object stays in the state after a destroy fails even though // we aren't returning a new state object here. return providers.ApplyResourceChangeResponse{ Diagnostics: tfdiags.Diagnostics(nil).Append(fmt.Errorf("failed")), } } ctx := testContext2(t, &ContextOpts{ Config: m, State: states.BuildState(func(ss *states.SyncState) { ss.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"baz"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) }), Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should have error") } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(` test_thing.foo: ID = baz provider = provider["registry.terraform.io/hashicorp/test"] `) // test_thing.foo is still here, even though provider returned no new state along with its error if actual != expected { t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual) } } func TestContext2Apply_errorCreateInvalidNew(t *testing.T) { m := testModule(t, "apply-error") p := testProvider("aws") p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, "foo": {Type: cty.String, Optional: true}, }, }, }, }) p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { return providers.PlanResourceChangeResponse{ PlannedState: req.ProposedNewState, } } p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { // We're intentionally returning an inconsistent new state here // because we want to test that Terraform ignores the inconsistency // when accompanied by another error. return providers.ApplyResourceChangeResponse{ NewState: cty.ObjectVal(map[string]cty.Value{ "value": cty.StringVal("wrong wrong wrong wrong"), "foo": cty.StringVal("absolutely brimming over with wrongability"), }), Diagnostics: tfdiags.Diagnostics(nil).Append(fmt.Errorf("forced error")), } } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should have error") } if got, want := len(diags), 1; got != want { // There should be no additional diagnostics generated by Terraform's own eval logic, // because the provider's own error supersedes them. t.Errorf("wrong number of diagnostics %d; want %d\n%s", got, want, diags.Err()) } if got, want := diags.Err().Error(), "forced error"; !strings.Contains(got, want) { t.Errorf("returned error does not contain %q, but it should\n%s", want, diags.Err()) } if got, want := len(state.RootModule().Resources), 2; got != want { t.Errorf("%d resources in state before prune; should have %d\n%s", got, want, spew.Sdump(state)) } state.PruneResourceHusks() // aws_instance.bar with no instances gets left behind when we bail out, but that's okay if got, want := len(state.RootModule().Resources), 1; got != want { t.Errorf("%d resources in state after prune; should have only one (aws_instance.foo, tainted)\n%s", got, spew.Sdump(state)) } } func TestContext2Apply_errorUpdateNullNew(t *testing.T) { m := testModule(t, "apply-error") p := testProvider("aws") p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, "foo": {Type: cty.String, Optional: true}, }, }, }, }) p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { return providers.PlanResourceChangeResponse{ PlannedState: req.ProposedNewState, } } p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { // We're intentionally returning no NewState here because we want to // test that Terraform retains the prior state, rather than treating // the returned null as "no state" (object deleted). return providers.ApplyResourceChangeResponse{ Diagnostics: tfdiags.Diagnostics(nil).Append(fmt.Errorf("forced error")), } } ctx := testContext2(t, &ContextOpts{ Config: m, State: states.BuildState(func(ss *states.SyncState) { ss.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"value":"old"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) }), Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags == nil { t.Fatal("should have error") } if got, want := len(diags), 1; got != want { // There should be no additional diagnostics generated by Terraform's own eval logic, // because the provider's own error supersedes them. t.Errorf("wrong number of diagnostics %d; want %d\n%s", got, want, diags.Err()) } if got, want := diags.Err().Error(), "forced error"; !strings.Contains(got, want) { t.Errorf("returned error does not contain %q, but it should\n%s", want, diags.Err()) } state.PruneResourceHusks() if got, want := len(state.RootModule().Resources), 1; got != want { t.Fatalf("%d resources in state; should have only one (aws_instance.foo, unmodified)\n%s", got, spew.Sdump(state)) } is := state.ResourceInstance(addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)) if is == nil { t.Fatalf("aws_instance.foo is not in the state after apply") } if got, want := is.Current.AttrsJSON, []byte(`"old"`); !bytes.Contains(got, want) { t.Fatalf("incorrect attributes for aws_instance.foo\ngot: %s\nwant: JSON containing %s\n\n%s", got, want, spew.Sdump(is)) } } func TestContext2Apply_errorPartial(t *testing.T) { errored := false m := testModule(t, "apply-error") p := testProvider("aws") state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","type":"aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { if errored { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("error")) return } errored = true return testApplyFn(req) } p.PlanResourceChangeFn = testDiffFn if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } s, diags := ctx.Apply() if diags == nil { t.Fatal("should have error") } mod := s.RootModule() if len(mod.Resources) != 2 { t.Fatalf("bad: %#v", mod.Resources) } actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(testTerraformApplyErrorPartialStr) if actual != expected { t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual) } } func TestContext2Apply_hook(t *testing.T) { m := testModule(t, "apply-good") h := new(MockHook) p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } if !h.PreApplyCalled { t.Fatal("should be called") } if !h.PostApplyCalled { t.Fatal("should be called") } if !h.PostStateUpdateCalled { t.Fatalf("should call post state update") } } func TestContext2Apply_hookOrphan(t *testing.T) { m := testModule(t, "apply-blank") h := new(MockHook) p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn 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"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, State: state, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } if !h.PreApplyCalled { t.Fatal("should be called") } if !h.PostApplyCalled { t.Fatal("should be called") } if !h.PostStateUpdateCalled { t.Fatalf("should call post state update") } } func TestContext2Apply_idAttr(t *testing.T) { m := testModule(t, "apply-idattr") p := testProvider("aws") ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } mod := state.RootModule() rs, ok := mod.Resources["aws_instance.foo"] if !ok { t.Fatal("not in state") } var attrs map[string]interface{} err := json.Unmarshal(rs.Instances[addrs.NoKey].Current.AttrsJSON, &attrs) if err != nil { t.Fatal(err) } if got, want := attrs["id"], "foo"; got != want { t.Fatalf("wrong id\ngot: %#v\nwant: %#v", got, want) } } func TestContext2Apply_outputBasic(t *testing.T) { m := testModule(t, "apply-output") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyOutputStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_outputAdd(t *testing.T) { m1 := testModule(t, "apply-output-add-before") p1 := testProvider("aws") p1.ApplyResourceChangeFn = testApplyFn p1.PlanResourceChangeFn = testDiffFn ctx1 := testContext2(t, &ContextOpts{ Config: m1, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p1), }, }) if _, diags := ctx1.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state1, diags := ctx1.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } m2 := testModule(t, "apply-output-add-after") p2 := testProvider("aws") p2.ApplyResourceChangeFn = testApplyFn p2.PlanResourceChangeFn = testDiffFn ctx2 := testContext2(t, &ContextOpts{ Config: m2, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p2), }, State: state1, }) if _, diags := ctx2.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state2, diags := ctx2.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state2.String()) expected := strings.TrimSpace(testTerraformApplyOutputAddStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_outputList(t *testing.T) { m := testModule(t, "apply-output-list") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyOutputListStr) if actual != expected { t.Fatalf("expected: \n%s\n\nbad: \n%s", expected, actual) } } func TestContext2Apply_outputMulti(t *testing.T) { m := testModule(t, "apply-output-multi") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyOutputMultiStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_outputMultiIndex(t *testing.T) { m := testModule(t, "apply-output-multi-index") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyOutputMultiIndexStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_taintX(t *testing.T) { m := testModule(t, "apply-taint") p := testProvider("aws") // destroyCount tests against regression of // https://github.com/hashicorp/terraform/issues/1056 var destroyCount = int32(0) var once sync.Once simulateProviderDelay := func() { time.Sleep(10 * time.Millisecond) } p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { once.Do(simulateProviderDelay) if req.PlannedState.IsNull() { atomic.AddInt32(&destroyCount, 1) } return testApplyFn(req) } p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"baz","num": "2", "type": "aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, 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("plan: %s", legacyDiffComparisonString(p.Changes)) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(testTerraformApplyTaintStr) if actual != expected { t.Fatalf("bad:\n%s", actual) } if destroyCount != 1 { t.Fatalf("Expected 1 destroy, got %d", destroyCount) } } func TestContext2Apply_taintDep(t *testing.T) { m := testModule(t, "apply-taint-dep") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"baz","num": "2", "type": "aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","num": "2", "type": "aws_instance", "foo": "baz"}`), Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("aws_instance.foo")}, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, 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("plan: %s", legacyDiffComparisonString(p.Changes)) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(testTerraformApplyTaintDepStr) if actual != expected { t.Fatalf("bad:\n%s", actual) } } func TestContext2Apply_taintDepRequiresNew(t *testing.T) { m := testModule(t, "apply-taint-dep-requires-new") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"baz","num": "2", "type": "aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","num": "2", "type": "aws_instance", "foo": "baz"}`), Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("aws_instance.foo")}, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, 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("plan: %s", legacyDiffComparisonString(p.Changes)) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(testTerraformApplyTaintDepRequireNewStr) if actual != expected { t.Fatalf("bad:\n%s", actual) } } func TestContext2Apply_targeted(t *testing.T) { m := testModule(t, "apply-targeted") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "foo", ), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } mod := state.RootModule() if len(mod.Resources) != 1 { t.Fatalf("expected 1 resource, got: %#v", mod.Resources) } checkStateString(t, state, ` aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] num = 2 type = aws_instance `) } func TestContext2Apply_targetedCount(t *testing.T) { m := testModule(t, "apply-targeted-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "foo", ), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ` aws_instance.foo.0: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance aws_instance.foo.1: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance aws_instance.foo.2: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance `) } func TestContext2Apply_targetedCountIndex(t *testing.T) { m := testModule(t, "apply-targeted-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.IntKey(1), ), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ` aws_instance.foo.1: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance `) } func TestContext2Apply_targetedDestroy(t *testing.T) { m := testModule(t, "destroy-targeted") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.a").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetOutputValue("out", cty.StringVal("bar"), false) child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) child.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.b").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-bcd345"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "a", ), }, PlanMode: plans.DestroyMode, }) if diags := ctx.Validate(); diags.HasErrors() { t.Fatalf("validate errors: %s", diags.Err()) } if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } mod := state.RootModule() if len(mod.Resources) != 0 { t.Fatalf("expected 0 resources, got: %#v", mod.Resources) } // the root output should not get removed; only the targeted resource. // // Note: earlier versions of this test expected 0 outputs, but it turns out // that was because Validate - not apply or destroy - removed the output // (which depends on the targeted resource) from state. That version of this // test did not match actual terraform behavior: the output remains in // state. // // TODO: Future refactoring may enable us to remove the output from state in // this case, and that would be Just Fine - this test can be modified to // expect 0 outputs. if len(mod.OutputValues) != 1 { t.Fatalf("expected 1 outputs, got: %#v", mod.OutputValues) } // the module instance should remain mod = state.Module(addrs.RootModuleInstance.Child("child", addrs.NoKey)) if len(mod.Resources) != 1 { t.Fatalf("expected 1 resources, got: %#v", mod.Resources) } } func TestContext2Apply_targetedDestroyCountDeps(t *testing.T) { m := testModule(t, "apply-destroy-targeted-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-bcd345"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-abc123"}`), Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("aws_instance.foo")}, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "foo", ), }, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ``) } // https://github.com/hashicorp/terraform/issues/4462 func TestContext2Apply_targetedDestroyModule(t *testing.T) { m := testModule(t, "apply-targeted-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-bcd345"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-abc123"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) child.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-bcd345"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) child.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-abc123"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Child("child", addrs.NoKey).Resource( addrs.ManagedResourceMode, "aws_instance", "foo", ), }, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ` aws_instance.bar: ID = i-abc123 provider = provider["registry.terraform.io/hashicorp/aws"] aws_instance.foo: ID = i-bcd345 provider = provider["registry.terraform.io/hashicorp/aws"] module.child: aws_instance.bar: ID = i-abc123 provider = provider["registry.terraform.io/hashicorp/aws"] `) } func TestContext2Apply_targetedDestroyCountIndex(t *testing.T) { m := testModule(t, "apply-targeted-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn foo := &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-bcd345"}`), } bar := &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-abc123"}`), } state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[0]").Resource, foo, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[1]").Resource, foo, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[2]").Resource, foo, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar[0]").Resource, bar, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar[1]").Resource, bar, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar[2]").Resource, bar, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, Targets: []addrs.Targetable{ addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.IntKey(2), ), addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "bar", addrs.IntKey(1), ), }, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ` aws_instance.bar.0: ID = i-abc123 provider = provider["registry.terraform.io/hashicorp/aws"] aws_instance.bar.2: ID = i-abc123 provider = provider["registry.terraform.io/hashicorp/aws"] aws_instance.foo.0: ID = i-bcd345 provider = provider["registry.terraform.io/hashicorp/aws"] aws_instance.foo.1: ID = i-bcd345 provider = provider["registry.terraform.io/hashicorp/aws"] `) } func TestContext2Apply_targetedModule(t *testing.T) { m := testModule(t, "apply-targeted-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Child("child", addrs.NoKey), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } mod := state.Module(addrs.RootModuleInstance.Child("child", addrs.NoKey)) if mod == nil { t.Fatalf("no child module found in the state!\n\n%#v", state) } if len(mod.Resources) != 2 { t.Fatalf("expected 2 resources, got: %#v", mod.Resources) } checkStateString(t, state, ` module.child: aws_instance.bar: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] num = 2 type = aws_instance aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] num = 2 type = aws_instance `) } // GH-1858 func TestContext2Apply_targetedModuleDep(t *testing.T) { m := testModule(t, "apply-targeted-module-dep") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "foo", ), }, }) if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { t.Logf("Diff: %s", legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, ` aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = foo type = aws_instance Dependencies: module.child.aws_instance.mod module.child: aws_instance.mod: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance Outputs: output = foo `) } // GH-10911 untargeted outputs should not be in the graph, and therefore // not execute. func TestContext2Apply_targetedModuleUnrelatedOutputs(t *testing.T) { m := testModule(t, "apply-targeted-module-unrelated-outputs") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() _ = state.EnsureModule(addrs.RootModuleInstance.Child("child2", addrs.NoKey)) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Child("child2", addrs.NoKey), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // - module.child1's instance_id output is dropped because we don't preserve // non-root module outputs between runs (they can be recalculated from config) // - module.child2's instance_id is updated because its dependency is updated // - child2_id is updated because if its transitive dependency via module.child2 checkStateString(t, s, ` Outputs: child2_id = foo module.child2: aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance Outputs: instance_id = foo `) } func TestContext2Apply_targetedModuleResource(t *testing.T) { m := testModule(t, "apply-targeted-module-resource") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Child("child", addrs.NoKey).Resource( addrs.ManagedResourceMode, "aws_instance", "foo", ), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } mod := state.Module(addrs.RootModuleInstance.Child("child", addrs.NoKey)) if mod == nil || len(mod.Resources) != 1 { t.Fatalf("expected 1 resource, got: %#v", mod) } checkStateString(t, state, ` module.child: aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] num = 2 type = aws_instance `) } func TestContext2Apply_targetedResourceOrphanModule(t *testing.T) { m := testModule(t, "apply-targeted-resource-orphan-module") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() child := state.EnsureModule(addrs.RootModuleInstance.Child("parent", addrs.NoKey)) child.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.bar").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"type":"aws_instance"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "foo", ), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } func TestContext2Apply_unknownAttribute(t *testing.T) { m := testModule(t, "apply-unknown") p := testProvider("aws") p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { resp = testDiffFn(req) planned := resp.PlannedState.AsValueMap() planned["unknown"] = cty.UnknownVal(cty.String) resp.PlannedState = cty.ObjectVal(planned) return resp } p.ApplyResourceChangeFn = testApplyFn p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "num": {Type: cty.Number, Optional: true}, "unknown": {Type: cty.String, Computed: true}, "type": {Type: cty.String, Computed: true}, }, }, }, }) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if !diags.HasErrors() { t.Error("should error, because attribute 'unknown' is still unknown after apply") } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyUnknownAttrStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_unknownAttributeInterpolate(t *testing.T) { m := testModule(t, "apply-unknown-interpolate") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags == nil { t.Fatal("should error") } } func TestContext2Apply_vars(t *testing.T) { fixture := contextFixtureApplyVars(t) opts := fixture.ContextOpts() opts.Variables = InputValues{ "foo": &InputValue{ Value: cty.StringVal("us-east-1"), SourceType: ValueFromCaller, }, "test_list": &InputValue{ Value: cty.ListVal([]cty.Value{ cty.StringVal("Hello"), cty.StringVal("World"), }), SourceType: ValueFromCaller, }, "test_map": &InputValue{ Value: cty.MapVal(map[string]cty.Value{ "Hello": cty.StringVal("World"), "Foo": cty.StringVal("Bar"), "Baz": cty.StringVal("Foo"), }), SourceType: ValueFromCaller, }, "amis": &InputValue{ Value: cty.MapVal(map[string]cty.Value{ "us-east-1": cty.StringVal("override"), }), SourceType: ValueFromCaller, }, } ctx := testContext2(t, opts) diags := ctx.Validate() if len(diags) != 0 { t.Fatalf("bad: %s", diags.ErrWithWarnings()) } if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } got := strings.TrimSpace(state.String()) want := strings.TrimSpace(testTerraformApplyVarsStr) if got != want { t.Errorf("wrong result\n\ngot:\n%s\n\nwant:\n%s", got, want) } } func TestContext2Apply_varsEnv(t *testing.T) { fixture := contextFixtureApplyVarsEnv(t) opts := fixture.ContextOpts() opts.Variables = InputValues{ "string": &InputValue{ Value: cty.StringVal("baz"), SourceType: ValueFromEnvVar, }, "list": &InputValue{ Value: cty.ListVal([]cty.Value{ cty.StringVal("Hello"), cty.StringVal("World"), }), SourceType: ValueFromEnvVar, }, "map": &InputValue{ Value: cty.MapVal(map[string]cty.Value{ "Hello": cty.StringVal("World"), "Foo": cty.StringVal("Bar"), "Baz": cty.StringVal("Foo"), }), SourceType: ValueFromEnvVar, }, } ctx := testContext2(t, opts) diags := ctx.Validate() if len(diags) != 0 { t.Fatalf("bad: %s", diags.ErrWithWarnings()) } if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyVarsEnvStr) if actual != expected { t.Errorf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_createBefore_depends(t *testing.T) { m := testModule(t, "apply-depends-create-before") h := new(HookRecordApplyOrder) p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "web", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","require_new":"ami-old"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "lb", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"baz","instance":"bar"}`), Dependencies: []addrs.ConfigResource{ addrs.ConfigResource{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "web", }, Module: addrs.RootModule, }, }, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) if p, diags := ctx.Plan(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("plan failed") } else { t.Logf("plan:\n%s", legacyDiffComparisonString(p.Changes)) } h.Active = true state, diags := ctx.Apply() if diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("apply failed") } mod := state.RootModule() if len(mod.Resources) < 2 { t.Logf("state after apply:\n%s", state.String()) t.Fatalf("only %d resources in root module; want at least 2", len(mod.Resources)) } got := strings.TrimSpace(state.String()) want := strings.TrimSpace(testTerraformApplyDependsCreateBeforeStr) if got != want { t.Fatalf("wrong final state\ngot:\n%s\n\nwant:\n%s", got, want) } // Test that things were managed _in the right order_ order := h.States diffs := h.Diffs if !order[0].IsNull() || diffs[0].Action == plans.Delete { t.Fatalf("should create new instance first: %#v", order) } if order[1].GetAttr("id").AsString() != "baz" { t.Fatalf("update must happen after create: %#v", order[1]) } if order[2].GetAttr("id").AsString() != "bar" || diffs[2].Action != plans.Delete { t.Fatalf("destroy must happen after update: %#v", order[2]) } } func TestContext2Apply_singleDestroy(t *testing.T) { m := testModule(t, "apply-depends-create-before") h := new(HookRecordApplyOrder) p := testProvider("aws") invokeCount := 0 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { invokeCount++ switch invokeCount { case 1: if req.PlannedState.IsNull() { t.Fatalf("should not destroy") } if id := req.PlannedState.GetAttr("id"); id.IsKnown() { t.Fatalf("should not have ID") } case 2: if req.PlannedState.IsNull() { t.Fatalf("should not destroy") } if id := req.PlannedState.GetAttr("id"); id.AsString() != "baz" { t.Fatalf("should have id") } case 3: if !req.PlannedState.IsNull() { t.Fatalf("should destroy") } default: t.Fatalf("bad invoke count %d", invokeCount) } return testApplyFn(req) } p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "web", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar","require_new":"ami-old"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "lb", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"baz","instance":"bar"}`), Dependencies: []addrs.ConfigResource{ addrs.ConfigResource{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "web", }, Module: addrs.RootModule, }, }, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } h.Active = true _, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } if invokeCount != 3 { t.Fatalf("bad: %d", invokeCount) } } // GH-7824 func TestContext2Apply_issue7824(t *testing.T) { p := testProvider("template") p.PlanResourceChangeFn = testDiffFn p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "template_file": { Attributes: map[string]*configschema.Attribute{ "template": {Type: cty.String, Optional: true}, "__template_requires_new": {Type: cty.Bool, Optional: true}, }, }, }, }) m, snap := testModuleWithSnapshot(t, "issue-7824") // Apply cleanly step 0 ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("template"): testProviderFuncFixed(p), }, }) plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } // Write / Read plan to simulate running it through a Plan file ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatalf("failed to round-trip through planfile: %s", err) } ctxOpts.Providers = map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("template"): testProviderFuncFixed(p), } ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } } // This deals with the situation where a splat expression is used referring // to another resource whose count is non-constant. func TestContext2Apply_issue5254(t *testing.T) { // Create a provider. We use "template" here just to match the repro // we got from the issue itself. p := testProvider("template") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "template_file": { Attributes: map[string]*configschema.Attribute{ "template": {Type: cty.String, Optional: true}, "__template_requires_new": {Type: cty.Bool, Optional: true}, "id": {Type: cty.String, Computed: true}, "type": {Type: cty.String, Computed: true}, }, }, }, }) // Apply cleanly step 0 ctx := testContext2(t, &ContextOpts{ Config: testModule(t, "issue-5254/step-0"), Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("template"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } m, snap := testModuleWithSnapshot(t, "issue-5254/step-1") // Application success. Now make the modification and store a plan ctx = testContext2(t, &ContextOpts{ Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("template"): testProviderFuncFixed(p), }, }) plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } // Write / Read plan to simulate running it through a Plan file ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatalf("failed to round-trip through planfile: %s", err) } ctxOpts.Providers = map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("template"): testProviderFuncFixed(p), } ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(` template_file.child: ID = foo provider = provider["registry.terraform.io/hashicorp/template"] __template_requires_new = true template = Hi type = template_file Dependencies: template_file.parent template_file.parent.0: ID = foo provider = provider["registry.terraform.io/hashicorp/template"] template = Hi type = template_file `) if actual != expected { t.Fatalf("wrong final state\ngot:\n%s\n\nwant:\n%s", actual, expected) } } func TestContext2Apply_targetedWithTaintedInState(t *testing.T) { p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn m, snap := testModuleWithSnapshot(t, "apply-tainted-targets") state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.ifailedprovisioners").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"ifailedprovisioners"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Resource( addrs.ManagedResourceMode, "aws_instance", "iambeingadded", ), }, State: state, }) plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } // Write / Read plan to simulate running it through a Plan file ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatalf("failed to round-trip through planfile: %s", err) } ctxOpts.Providers = map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), } ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(` aws_instance.iambeingadded: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance aws_instance.ifailedprovisioners: (tainted) ID = ifailedprovisioners provider = provider["registry.terraform.io/hashicorp/aws"] `) if actual != expected { t.Fatalf("expected state: \n%s\ngot: \n%s", expected, actual) } } // Higher level test exposing the bug this covers in // TestResource_ignoreChangesRequired func TestContext2Apply_ignoreChangesCreate(t *testing.T) { m := testModule(t, "apply-ignore-changes-create") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn instanceSchema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block instanceSchema.Attributes["required_field"] = &configschema.Attribute{ Type: cty.String, Required: true, } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) 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()) } mod := state.RootModule() if len(mod.Resources) != 1 { t.Fatalf("bad: %s", state) } actual := strings.TrimSpace(state.String()) // Expect no changes from original state expected := strings.TrimSpace(` aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] required_field = set type = aws_instance `) if actual != expected { t.Fatalf("expected:\n%s\ngot:\n%s", expected, actual) } } func TestContext2Apply_ignoreChangesWithDep(t *testing.T) { m := testModule(t, "apply-ignore-changes-dep") p := testProvider("aws") p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { resp.PlannedState = req.ProposedNewState switch req.TypeName { case "aws_instance": resp.RequiresReplace = append(resp.RequiresReplace, cty.Path{cty.GetAttrStep{Name: "ami"}}) case "aws_eip": return testDiffFn(req) default: t.Fatalf("Unexpected type: %s", req.TypeName) } return } state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[0]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-abc123","ami":"ami-abcd1234"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo[1]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"i-bcd234","ami":"i-bcd234"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_eip.foo[0]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"eip-abc123","instance":"i-abc123"}`), Dependencies: []addrs.ConfigResource{ addrs.ConfigResource{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "foo", }, Module: addrs.RootModule, }, }, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_eip.foo[1]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"eip-bcd234","instance":"i-bcd234"}`), Dependencies: []addrs.ConfigResource{ addrs.ConfigResource{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "foo", }, Module: addrs.RootModule, }, }, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state.DeepCopy(), }) _, diags := ctx.Plan() assertNoErrors(t, diags) s, diags := ctx.Apply() assertNoErrors(t, diags) actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(state.String()) if actual != expected { t.Fatalf("expected:\n%s\n\ngot:\n%s", expected, actual) } } func TestContext2Apply_ignoreChangesAll(t *testing.T) { m := testModule(t, "apply-ignore-changes-all") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn instanceSchema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block instanceSchema.Attributes["required_field"] = &configschema.Attribute{ Type: cty.String, Required: true, } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if p, diags := ctx.Plan(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("plan failed") } else { t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() assertNoErrors(t, diags) mod := state.RootModule() if len(mod.Resources) != 1 { t.Fatalf("bad: %s", state) } actual := strings.TrimSpace(state.String()) // Expect no changes from original state expected := strings.TrimSpace(` aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] required_field = set type = aws_instance `) if actual != expected { t.Fatalf("expected:\n%s\ngot:\n%s", expected, actual) } } // https://github.com/hashicorp/terraform/issues/7378 func TestContext2Apply_destroyNestedModuleWithAttrsReferencingResource(t *testing.T) { m, snap := testModuleWithSnapshot(t, "apply-destroy-nested-module-with-attrs") p := testProvider("null") p.PlanResourceChangeFn = testDiffFn var state *states.State var diags tfdiags.Diagnostics { ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), }, }) // First plan and apply a create operation if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan err: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply err: %s", diags.Err()) } } { ctx := testContext2(t, &ContextOpts{ PlanMode: plans.DestroyMode, Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), }, }) plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("destroy plan err: %s", diags.Err()) } ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatalf("failed to round-trip through planfile: %s", err) } ctxOpts.Providers = map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), } ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("destroy apply err: %s", diags.Err()) } } if !state.Empty() { t.Fatalf("state after apply: %s\nwant empty state", spew.Sdump(state)) } } // If a data source explicitly depends on another resource, it's because we need // that resource to be applied first. func TestContext2Apply_dataDependsOn(t *testing.T) { p := testProvider("null") m := testModuleInline(t, map[string]string{ "main.tf": ` resource "null_instance" "write" { foo = "attribute" } data "null_data_source" "read" { count = 1 depends_on = ["null_instance.write"] } resource "null_instance" "depends" { foo = data.null_data_source.read[0].foo } `}) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), }, }) // the "provisioner" here writes to this variable, because the intent is to // create a dependency which can't be viewed through the graph, and depends // solely on the configuration providing "depends_on" provisionerOutput := "" p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { // the side effect of the resource being applied provisionerOutput = "APPLIED" return testApplyFn(req) } p.PlanResourceChangeFn = testDiffFn p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { return providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("boop"), "foo": cty.StringVal(provisionerOutput), }), } } _, diags := ctx.Plan() assertNoErrors(t, diags) state, diags := ctx.Apply() assertNoErrors(t, diags) root := state.Module(addrs.RootModuleInstance) is := root.ResourceInstance(addrs.Resource{ Mode: addrs.DataResourceMode, Type: "null_data_source", Name: "read", }.Instance(addrs.IntKey(0))) if is == nil { t.Fatal("data resource instance is not present in state; should be") } var attrs map[string]interface{} err := json.Unmarshal(is.Current.AttrsJSON, &attrs) if err != nil { t.Fatal(err) } actual := attrs["foo"] expected := "APPLIED" if actual != expected { t.Fatalf("bad:\n%s", strings.TrimSpace(state.String())) } // run another plan to make sure the data source doesn't show as a change plan, diags := ctx.Plan() assertNoErrors(t, diags) for _, c := range plan.Changes.Resources { if c.Action != plans.NoOp { t.Fatalf("unexpected change for %s", c.Addr) } } // now we cause a change in the first resource, which should trigger a plan // in the data source, and the resource that depends on the data source // must plan a change as well. m = testModuleInline(t, map[string]string{ "main.tf": ` resource "null_instance" "write" { foo = "new" } data "null_data_source" "read" { depends_on = ["null_instance.write"] } resource "null_instance" "depends" { foo = data.null_data_source.read.foo } `}) p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { // the side effect of the resource being applied provisionerOutput = "APPLIED_AGAIN" return testApplyFn(req) } ctx = testContext2(t, &ContextOpts{ Config: m, State: state, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), }, }) plan, diags = ctx.Plan() assertNoErrors(t, diags) expectedChanges := map[string]plans.Action{ "null_instance.write": plans.Update, "data.null_data_source.read": plans.Read, "null_instance.depends": plans.Update, } for _, c := range plan.Changes.Resources { if c.Action != expectedChanges[c.Addr.String()] { t.Errorf("unexpected %s for %s", c.Action, c.Addr) } } } func TestContext2Apply_terraformWorkspace(t *testing.T) { m := testModule(t, "apply-terraform-workspace") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Meta: &ContextMeta{Env: "foo"}, Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } actual := state.RootModule().OutputValues["output"] expected := cty.StringVal("foo") if actual == nil || actual.Value != expected { t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) } } // verify that multiple config references only create a single depends_on entry func TestContext2Apply_multiRef(t *testing.T) { m := testModule(t, "apply-multi-ref") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } deps := state.Modules[""].Resources["aws_instance.other"].Instances[addrs.NoKey].Current.Dependencies if len(deps) != 1 || deps[0].String() != "aws_instance.create" { t.Fatalf("expected 1 depends_on entry for aws_instance.create, got %q", deps) } } func TestContext2Apply_targetedModuleRecursive(t *testing.T) { m := testModule(t, "apply-targeted-module-recursive") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Targets: []addrs.Targetable{ addrs.RootModuleInstance.Child("child", addrs.NoKey), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } mod := state.Module( addrs.RootModuleInstance.Child("child", addrs.NoKey).Child("subchild", addrs.NoKey), ) if mod == nil { t.Fatalf("no subchild module found in the state!\n\n%#v", state) } if len(mod.Resources) != 1 { t.Fatalf("expected 1 resources, got: %#v", mod.Resources) } checkStateString(t, state, ` module.child.subchild: aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] num = 2 type = aws_instance `) } func TestContext2Apply_localVal(t *testing.T) { m := testModule(t, "apply-local-val") ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{}, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("error during plan: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("error during apply: %s", diags.Err()) } got := strings.TrimSpace(state.String()) want := strings.TrimSpace(` Outputs: result_1 = hello result_3 = hello world `) if got != want { t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s", got, want) } } func TestContext2Apply_destroyWithLocals(t *testing.T) { m := testModule(t, "apply-destroy-with-locals") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetOutputValue("name", cty.StringVal("test-bar"), false) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } s, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("error during apply: %s", diags.Err()) } got := strings.TrimSpace(s.String()) want := strings.TrimSpace(``) if got != want { t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s", got, want) } } func TestContext2Apply_providerWithLocals(t *testing.T) { m := testModule(t, "provider-with-locals") p := testProvider("aws") providerRegion := "" // this should not be overridden during destroy p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { val := req.Config.GetAttr("region") if !val.IsNull() { providerRegion = val.AsString() } return } p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } if state.HasResources() { t.Fatal("expected no state, got:", state) } if providerRegion != "bar" { t.Fatalf("expected region %q, got: %q", "bar", providerRegion) } } func TestContext2Apply_destroyWithProviders(t *testing.T) { m := testModule(t, "destroy-module-with-provider") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn state := states.NewState() removed := state.EnsureModule(addrs.RootModuleInstance.Child("mod", addrs.NoKey).Child("removed", addrs.NoKey)) removed.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.child").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"].baz`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, }) // test that we can't destroy if the provider is missing if _, diags := ctx.Plan(); diags == nil { t.Fatal("expected plan error, provider.aws.baz doesn't exist") } // correct the state state.Modules["module.mod.module.removed"].Resources["aws_instance.child"].ProviderConfig = mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"].bar`) ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatal(diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("error during apply: %s", diags.Err()) } got := strings.TrimSpace(state.String()) want := strings.TrimSpace("") if got != want { t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s", got, want) } } func TestContext2Apply_providersFromState(t *testing.T) { m := configs.NewEmptyConfig() p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn implicitProviderState := states.NewState() impRoot := implicitProviderState.EnsureModule(addrs.RootModuleInstance) impRoot.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.a").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) aliasedProviderState := states.NewState() aliasRoot := aliasedProviderState.EnsureModule(addrs.RootModuleInstance) aliasRoot.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.a").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"].bar`), ) moduleProviderState := states.NewState() moduleProviderRoot := moduleProviderState.EnsureModule(addrs.RootModuleInstance) moduleProviderRoot.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.a").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"bar"}`), }, mustProviderConfig(`module.child.provider["registry.terraform.io/hashicorp/aws"]`), ) for _, tc := range []struct { name string state *states.State output string err bool }{ { name: "add implicit provider", state: implicitProviderState, err: false, output: "", }, // an aliased provider must be in the config to remove a resource { name: "add aliased provider", state: aliasedProviderState, err: true, }, // a provider in a module implies some sort of config, so this isn't // allowed even without an alias { name: "add unaliased module provider", state: moduleProviderState, err: true, }, } { t.Run(tc.name, func(t *testing.T) { ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: tc.state, }) _, diags := ctx.Plan() if tc.err { if diags == nil { t.Fatal("expected error") } else { return } } if !tc.err && diags.HasErrors() { t.Fatal(diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } checkStateString(t, state, "") }) } } func TestContext2Apply_plannedInterpolatedCount(t *testing.T) { m, snap := testModuleWithSnapshot(t, "apply-interpolated-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn Providers := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), } state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.test").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: Providers, State: state, }) plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("plan failed: %s", diags.Err()) } // We'll marshal and unmarshal the plan here, to ensure that we have // a clean new context as would be created if we separately ran // terraform plan -out=tfplan && terraform apply tfplan ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatalf("failed to round-trip through planfile: %s", err) } ctxOpts.Providers = Providers ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } // Applying the plan should now succeed _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply failed: %s", diags.Err()) } } func TestContext2Apply_plannedDestroyInterpolatedCount(t *testing.T) { m, snap := testModuleWithSnapshot(t, "plan-destroy-interpolated-count") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn providers := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), } state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.a[0]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.a[1]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetOutputValue("out", cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("foo")}), false) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: providers, State: state, PlanMode: plans.DestroyMode, }) plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("plan failed: %s", diags.Err()) } // We'll marshal and unmarshal the plan here, to ensure that we have // a clean new context as would be created if we separately ran // terraform plan -out=tfplan && terraform apply tfplan ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatalf("failed to round-trip through planfile: %s", err) } ctxOpts.Providers = providers ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } // Applying the plan should now succeed state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply failed: %s", diags.Err()) } if !state.Empty() { t.Fatalf("state not empty: %s\n", state) } } func TestContext2Apply_scaleInMultivarRef(t *testing.T) { m := testModule(t, "apply-resource-scale-in") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn Providers := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), } state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.one").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("aws_instance.two").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: Providers, State: state, Variables: InputValues{ "instance_count": { Value: cty.NumberIntVal(0), SourceType: ValueFromCaller, }, }, }) _, diags := ctx.Plan() assertNoErrors(t, diags) // Applying the plan should now succeed _, diags = ctx.Apply() assertNoErrors(t, diags) } func TestContext2Apply_inconsistentWithPlan(t *testing.T) { m := testModule(t, "apply-inconsistent-with-plan") p := testProvider("test") p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "test": { Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, }, }, }) p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { return providers.PlanResourceChangeResponse{ PlannedState: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("before"), }), } } p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { return providers.ApplyResourceChangeResponse{ NewState: cty.ObjectVal(map[string]cty.Value{ // This is intentionally incorrect: because id was fixed at "before" // during plan, it must not change during apply. "id": cty.StringVal("after"), }), } } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } _, diags := ctx.Apply() if !diags.HasErrors() { t.Fatalf("apply succeeded; want error") } if got, want := diags.Err().Error(), "Provider produced inconsistent result after apply"; !strings.Contains(got, want) { t.Fatalf("wrong error\ngot: %s\nshould contain: %s", got, want) } } // Issue 19908 was about retaining an existing object in the state when an // update to it fails and the provider does not return a partially-updated // value for it. Previously we were incorrectly removing it from the state // in that case, but instead it should be retained so the update can be // retried. func TestContext2Apply_issue19908(t *testing.T) { m := testModule(t, "apply-issue19908") p := testProvider("test") p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "test": { Attributes: map[string]*configschema.Attribute{ "baz": {Type: cty.String, Required: true}, }, }, }, }) p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { return providers.PlanResourceChangeResponse{ PlannedState: req.ProposedNewState, } } p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { var diags tfdiags.Diagnostics diags = diags.Append(fmt.Errorf("update failed")) return providers.ApplyResourceChangeResponse{ Diagnostics: diags, } } ctx := testContext2(t, &ContextOpts{ Config: m, State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"baz":"old"}`), Status: states.ObjectReady, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) }), Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if !diags.HasErrors() { t.Fatalf("apply succeeded; want error") } if got, want := diags.Err().Error(), "update failed"; !strings.Contains(got, want) { t.Fatalf("wrong error\ngot: %s\nshould contain: %s", got, want) } mod := state.RootModule() rs := mod.Resources["test.foo"] if rs == nil { t.Fatalf("test.foo not in state after apply, but should be") } is := rs.Instances[addrs.NoKey] if is == nil { t.Fatalf("test.foo not in state after apply, but should be") } obj := is.Current if obj == nil { t.Fatalf("test.foo has no current object in state after apply, but should do") } if got, want := obj.Status, states.ObjectReady; got != want { t.Errorf("test.foo has wrong status %s after apply; want %s", got, want) } if got, want := obj.AttrsJSON, []byte(`"old"`); !bytes.Contains(got, want) { t.Errorf("test.foo attributes JSON doesn't contain %s after apply\ngot: %s", want, got) } } func TestContext2Apply_invalidIndexRef(t *testing.T) { p := testProvider("test") p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true, Computed: true}, }, }, }, }) p.PlanResourceChangeFn = testDiffFn m := testModule(t, "apply-invalid-index") c := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) diags := c.Validate() if diags.HasErrors() { t.Fatalf("unexpected validation failure: %s", diags.Err()) } wantErr := `The given key does not identify an element in this collection value` _, diags = c.Plan() if !diags.HasErrors() { t.Fatalf("plan succeeded; want error") } gotErr := diags.Err().Error() if !strings.Contains(gotErr, wantErr) { t.Fatalf("missing expected error\ngot: %s\n\nwant: error containing %q", gotErr, wantErr) } } func TestContext2Apply_moduleReplaceCycle(t *testing.T) { for _, mode := range []string{"normal", "cbd"} { var m *configs.Config switch mode { case "normal": m = testModule(t, "apply-module-replace-cycle") case "cbd": m = testModule(t, "apply-module-replace-cycle-cbd") } p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn instanceSchema := &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "require_new": {Type: cty.String, Optional: true}, }, } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": instanceSchema, }, }) state := states.NewState() modA := state.EnsureModule(addrs.RootModuleInstance.Child("a", addrs.NoKey)) modA.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "a", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"a","require_new":"old"}`), CreateBeforeDestroy: mode == "cbd", }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) modB := state.EnsureModule(addrs.RootModuleInstance.Child("b", addrs.NoKey)) modB.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "b", }.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"b","require_new":"old"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ) aBefore, _ := plans.NewDynamicValue( cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("a"), "require_new": cty.StringVal("old"), }), instanceSchema.ImpliedType()) aAfter, _ := plans.NewDynamicValue( cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "require_new": cty.StringVal("new"), }), instanceSchema.ImpliedType()) bBefore, _ := plans.NewDynamicValue( cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("b"), "require_new": cty.StringVal("old"), }), instanceSchema.ImpliedType()) bAfter, _ := plans.NewDynamicValue( cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "require_new": cty.UnknownVal(cty.String), }), instanceSchema.ImpliedType()) var aAction plans.Action switch mode { case "normal": aAction = plans.DeleteThenCreate case "cbd": aAction = plans.CreateThenDelete } changes := &plans.Changes{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "a", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("a", addrs.NoKey)), ProviderAddr: addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ChangeSrc: plans.ChangeSrc{ Action: aAction, Before: aBefore, After: aAfter, }, }, { Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "b", }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance.Child("b", addrs.NoKey)), ProviderAddr: addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("aws"), Module: addrs.RootModule, }, ChangeSrc: plans.ChangeSrc{ Action: plans.DeleteThenCreate, Before: bBefore, After: bAfter, }, }, }, } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, State: state, Changes: changes, }) t.Run(mode, func(t *testing.T) { _, diags := ctx.Apply() if diags.HasErrors() { t.Fatal(diags.Err()) } }) } } func TestContext2Apply_destroyDataCycle(t *testing.T) { m, snap := testModuleWithSnapshot(t, "apply-destroy-data-cycle") p := testProvider("null") p.PlanResourceChangeFn = testDiffFn p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { return providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("new"), "foo": cty.NullVal(cty.String), }), } } tp := testProvider("test") tp.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "null_resource", Name: "a", }.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"a"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("null"), Module: addrs.RootModule, }, ) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "a", }.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"a"}`), Dependencies: []addrs.ConfigResource{ addrs.ConfigResource{ Resource: addrs.Resource{ Mode: addrs.DataResourceMode, Type: "null_data_source", Name: "d", }, Module: addrs.RootModule, }, }, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.DataResourceMode, Type: "null_data_source", Name: "d", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"old"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("null"), Module: addrs.RootModule, }, ) Providers := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), addrs.NewDefaultProvider("test"): testProviderFuncFixed(tp), } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: Providers, State: state, PlanMode: plans.DestroyMode, }) plan, diags := ctx.Plan() diags.HasErrors() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // We'll marshal and unmarshal the plan here, to ensure that we have // a clean new context as would be created if we separately ran // terraform plan -out=tfplan && terraform apply tfplan ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatal(err) } ctxOpts.Providers = Providers ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("failed to create context for plan: %s", diags.Err()) } tp.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { foo := req.Config.GetAttr("foo") if !foo.IsKnown() { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown config value foo")) return resp } if foo.AsString() != "new" { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("wrong config value: %q", foo.AsString())) } return resp } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } } func TestContext2Apply_taintedDestroyFailure(t *testing.T) { m := testModule(t, "apply-destroy-tainted") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { // All destroys fail. if req.PlannedState.IsNull() { resp.Diagnostics = resp.Diagnostics.Append(errors.New("failure")) return } // c will also fail to create, meaning the existing tainted instance // becomes deposed, ans is then promoted back to current. // only C has a foo attribute planned := req.PlannedState.AsValueMap() foo, ok := planned["foo"] if ok && !foo.IsNull() && foo.AsString() == "c" { resp.Diagnostics = resp.Diagnostics.Append(errors.New("failure")) return } return testApplyFn(req) } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, Computed: true, }, "foo": { Type: cty.String, Optional: true, }, }, }, }, }) state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "a", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"a","foo":"a"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "b", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"b","foo":"b"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "c", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"id":"c","foo":"old"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) Providers := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: Providers, State: state, Hooks: []Hook{&testHook{}}, }) _, diags := ctx.Plan() diags.HasErrors() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } state, diags = ctx.Apply() if !diags.HasErrors() { t.Fatal("expected error") } root = state.Module(addrs.RootModuleInstance) // the instance that failed to destroy should remain tainted a := root.ResourceInstance(addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "a", }.Instance(addrs.NoKey)) if a.Current.Status != states.ObjectTainted { t.Fatal("test_instance.a should be tainted") } // b is create_before_destroy, and the destroy failed, so there should be 1 // deposed instance. b := root.ResourceInstance(addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "b", }.Instance(addrs.NoKey)) if b.Current.Status != states.ObjectReady { t.Fatal("test_instance.b should be Ready") } if len(b.Deposed) != 1 { t.Fatal("test_instance.b failed to keep deposed instance") } // the desposed c instance should be promoted back to Current, and remain // tainted c := root.ResourceInstance(addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "c", }.Instance(addrs.NoKey)) if c.Current == nil { t.Fatal("test_instance.c has no current instance, but it should") } if c.Current.Status != states.ObjectTainted { t.Fatal("test_instance.c should be tainted") } if len(c.Deposed) != 0 { t.Fatal("test_instance.c should have no deposed instances") } if string(c.Current.AttrsJSON) != `{"foo":"old","id":"c"}` { t.Fatalf("unexpected attrs for c: %q\n", c.Current.AttrsJSON) } } func TestContext2Apply_plannedConnectionRefs(t *testing.T) { m := testModule(t, "apply-plan-connection-refs") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { s := req.PlannedState.AsValueMap() // delay "a" slightly, so if the reference edge is missing the "b" // provisioner will see an unknown value. if s["foo"].AsString() == "a" { time.Sleep(500 * time.Millisecond) } s["id"] = cty.StringVal("ID") if ty, ok := s["type"]; ok && !ty.IsKnown() { s["type"] = cty.StringVal(req.TypeName) } resp.NewState = cty.ObjectVal(s) return resp } provisionerFactory := func() (provisioners.Interface, error) { pr := testProvisioner() pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { host := req.Connection.GetAttr("host") if host.IsNull() || !host.IsKnown() { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("invalid host value: %#v", host)) } return resp } return pr, nil } Providers := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), } provisioners := map[string]provisioners.Factory{ "shell": provisionerFactory, } hook := &testHook{} ctx := testContext2(t, &ContextOpts{ Config: m, Providers: Providers, Provisioners: provisioners, Hooks: []Hook{hook}, }) _, diags := ctx.Plan() diags.HasErrors() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } } func TestContext2Apply_cbdCycle(t *testing.T) { m, snap := testModuleWithSnapshot(t, "apply-cbd-cycle") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "a", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"a","require_new":"old","foo":"b"}`), Dependencies: []addrs.ConfigResource{ addrs.ConfigResource{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "b", }, Module: addrs.RootModule, }, addrs.ConfigResource{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "c", }, Module: addrs.RootModule, }, }, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "b", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"b","require_new":"old","foo":"c"}`), Dependencies: []addrs.ConfigResource{ addrs.ConfigResource{ Resource: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "c", }, Module: addrs.RootModule, }, }, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) root.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", Name: "c", }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"c","require_new":"old"}`), }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) Providers := map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), } hook := &testHook{} ctx := testContext2(t, &ContextOpts{ Config: m, Providers: Providers, State: state, Hooks: []Hook{hook}, }) plan, diags := ctx.Plan() diags.HasErrors() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } // We'll marshal and unmarshal the plan here, to ensure that we have // a clean new context as would be created if we separately ran // terraform plan -out=tfplan && terraform apply tfplan ctxOpts, err := contextOptsForPlanViaFile(snap, plan) if err != nil { t.Fatal(err) } ctxOpts.Providers = Providers ctx, diags = NewContext(ctxOpts) if diags.HasErrors() { t.Fatalf("failed to create context for plan: %s", diags.Err()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } } func TestContext2Apply_ProviderMeta_apply_set(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { Type: cty.String, Required: true, }, }, } var pmMu sync.Mutex arcPMs := map[string]cty.Value{} p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { pmMu.Lock() defer pmMu.Unlock() arcPMs[req.TypeName] = req.ProviderMeta s := req.PlannedState.AsValueMap() s["id"] = cty.StringVal("ID") if ty, ok := s["type"]; ok && !ty.IsKnown() { s["type"] = cty.StringVal(req.TypeName) } return providers.ApplyResourceChangeResponse{ NewState: cty.ObjectVal(s), } } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() assertNoErrors(t, diags) _, diags = ctx.Apply() assertNoErrors(t, diags) if !p.ApplyResourceChangeCalled { t.Fatalf("ApplyResourceChange not called") } expectations := map[string]cty.Value{} if pm, ok := arcPMs["test_resource"]; !ok { t.Fatalf("sub-module ApplyResourceChange not called") } else if pm.IsNull() { t.Fatalf("null ProviderMeta in sub-module ApplyResourceChange") } else { expectations["quux-submodule"] = pm } if pm, ok := arcPMs["test_instance"]; !ok { t.Fatalf("root module ApplyResourceChange not called") } else if pm.IsNull() { t.Fatalf("null ProviderMeta in root module ApplyResourceChange") } else { expectations["quux"] = pm } type metaStruct struct { Baz string `cty:"baz"` } for expected, v := range expectations { var meta metaStruct err := gocty.FromCtyValue(v, &meta) if err != nil { t.Fatalf("Error parsing cty value: %s", err) } if meta.Baz != expected { t.Fatalf("Expected meta.Baz to be %q, got %q", expected, meta.Baz) } } } func TestContext2Apply_ProviderMeta_apply_unset(t *testing.T) { m := testModule(t, "provider-meta-unset") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { Type: cty.String, Required: true, }, }, } var pmMu sync.Mutex arcPMs := map[string]cty.Value{} p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { pmMu.Lock() defer pmMu.Unlock() arcPMs[req.TypeName] = req.ProviderMeta s := req.PlannedState.AsValueMap() s["id"] = cty.StringVal("ID") if ty, ok := s["type"]; ok && !ty.IsKnown() { s["type"] = cty.StringVal(req.TypeName) } return providers.ApplyResourceChangeResponse{ NewState: cty.ObjectVal(s), } } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() assertNoErrors(t, diags) _, diags = ctx.Apply() assertNoErrors(t, diags) if !p.ApplyResourceChangeCalled { t.Fatalf("ApplyResourceChange not called") } if pm, ok := arcPMs["test_resource"]; !ok { t.Fatalf("sub-module ApplyResourceChange not called") } else if !pm.IsNull() { t.Fatalf("non-null ProviderMeta in sub-module ApplyResourceChange: %+v", pm) } if pm, ok := arcPMs["test_instance"]; !ok { t.Fatalf("root module ApplyResourceChange not called") } else if !pm.IsNull() { t.Fatalf("non-null ProviderMeta in root module ApplyResourceChange: %+v", pm) } } func TestContext2Apply_ProviderMeta_plan_set(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { Type: cty.String, Required: true, }, }, } prcPMs := map[string]cty.Value{} p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { prcPMs[req.TypeName] = req.ProviderMeta return providers.PlanResourceChangeResponse{ PlannedState: req.ProposedNewState, } } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() assertNoErrors(t, diags) if !p.PlanResourceChangeCalled { t.Fatalf("PlanResourceChange not called") } expectations := map[string]cty.Value{} if pm, ok := prcPMs["test_resource"]; !ok { t.Fatalf("sub-module PlanResourceChange not called") } else if pm.IsNull() { t.Fatalf("null ProviderMeta in sub-module PlanResourceChange") } else { expectations["quux-submodule"] = pm } if pm, ok := prcPMs["test_instance"]; !ok { t.Fatalf("root module PlanResourceChange not called") } else if pm.IsNull() { t.Fatalf("null ProviderMeta in root module PlanResourceChange") } else { expectations["quux"] = pm } type metaStruct struct { Baz string `cty:"baz"` } for expected, v := range expectations { var meta metaStruct err := gocty.FromCtyValue(v, &meta) if err != nil { t.Fatalf("Error parsing cty value: %s", err) } if meta.Baz != expected { t.Fatalf("Expected meta.Baz to be %q, got %q", expected, meta.Baz) } } } func TestContext2Apply_ProviderMeta_plan_unset(t *testing.T) { m := testModule(t, "provider-meta-unset") p := testProvider("test") schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { Type: cty.String, Required: true, }, }, } prcPMs := map[string]cty.Value{} p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { prcPMs[req.TypeName] = req.ProviderMeta return providers.PlanResourceChangeResponse{ PlannedState: req.ProposedNewState, } } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() assertNoErrors(t, diags) if !p.PlanResourceChangeCalled { t.Fatalf("PlanResourceChange not called") } if pm, ok := prcPMs["test_resource"]; !ok { t.Fatalf("sub-module PlanResourceChange not called") } else if !pm.IsNull() { t.Fatalf("non-null ProviderMeta in sub-module PlanResourceChange: %+v", pm) } if pm, ok := prcPMs["test_instance"]; !ok { t.Fatalf("root module PlanResourceChange not called") } else if !pm.IsNull() { t.Fatalf("non-null ProviderMeta in root module PlanResourceChange: %+v", pm) } } func TestContext2Apply_ProviderMeta_plan_setNoSchema(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if !diags.HasErrors() { t.Fatalf("plan supposed to error, has no errors") } var rootErr, subErr bool errorSummary := "The resource test_%s.bar belongs to a provider that doesn't support provider_meta blocks" for _, diag := range diags { if diag.Description().Summary != "Provider registry.terraform.io/hashicorp/test doesn't support provider_meta" { t.Errorf("Unexpected error: %+v", diag.Description()) } switch diag.Description().Detail { case fmt.Sprintf(errorSummary, "instance"): rootErr = true case fmt.Sprintf(errorSummary, "resource"): subErr = true default: t.Errorf("Unexpected error: %s", diag.Description()) } } if !rootErr { t.Errorf("Expected unsupported provider_meta block error for root module, none received") } if !subErr { t.Errorf("Expected unsupported provider_meta block error for sub-module, none received") } } func TestContext2Apply_ProviderMeta_plan_setInvalid(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "quux": { Type: cty.String, Required: true, }, }, } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if !diags.HasErrors() { t.Fatalf("plan supposed to error, has no errors") } var reqErr, invalidErr bool for _, diag := range diags { switch diag.Description().Summary { case "Missing required argument": if diag.Description().Detail == `The argument "quux" is required, but no definition was found.` { reqErr = true } else { t.Errorf("Unexpected error %+v", diag.Description()) } case "Unsupported argument": if diag.Description().Detail == `An argument named "baz" is not expected here.` { invalidErr = true } else { t.Errorf("Unexpected error %+v", diag.Description()) } default: t.Errorf("Unexpected error %+v", diag.Description()) } } if !reqErr { t.Errorf("Expected missing required argument error, none received") } if !invalidErr { t.Errorf("Expected unsupported argument error, none received") } } func TestContext2Apply_ProviderMeta_refresh_set(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { Type: cty.String, Required: true, }, }, } rrcPMs := map[string]cty.Value{} p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { rrcPMs[req.TypeName] = req.ProviderMeta newState, err := p.GetProviderSchemaResponse.ResourceTypes[req.TypeName].Block.CoerceValue(req.PriorState) if err != nil { panic(err) } resp.NewState = newState return resp } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() assertNoErrors(t, diags) _, diags = ctx.Apply() assertNoErrors(t, diags) _, diags = ctx.Refresh() assertNoErrors(t, diags) if !p.ReadResourceCalled { t.Fatalf("ReadResource not called") } expectations := map[string]cty.Value{} if pm, ok := rrcPMs["test_resource"]; !ok { t.Fatalf("sub-module ReadResource not called") } else if pm.IsNull() { t.Fatalf("null ProviderMeta in sub-module ReadResource") } else { expectations["quux-submodule"] = pm } if pm, ok := rrcPMs["test_instance"]; !ok { t.Fatalf("root module ReadResource not called") } else if pm.IsNull() { t.Fatalf("null ProviderMeta in root module ReadResource") } else { expectations["quux"] = pm } type metaStruct struct { Baz string `cty:"baz"` } for expected, v := range expectations { var meta metaStruct err := gocty.FromCtyValue(v, &meta) if err != nil { t.Fatalf("Error parsing cty value: %s", err) } if meta.Baz != expected { t.Fatalf("Expected meta.Baz to be %q, got %q", expected, meta.Baz) } } } func TestContext2Apply_ProviderMeta_refresh_setNoSchema(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn // we need a schema for plan/apply so they don't error schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { Type: cty.String, Required: true, }, }, } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() assertNoErrors(t, diags) _, diags = ctx.Apply() assertNoErrors(t, diags) // drop the schema before refresh, to test that it errors schema.ProviderMeta = nil p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, State: ctx.State(), }) _, diags = ctx.Refresh() if !diags.HasErrors() { t.Fatalf("refresh supposed to error, has no errors") } var rootErr, subErr bool errorSummary := "The resource test_%s.bar belongs to a provider that doesn't support provider_meta blocks" for _, diag := range diags { if diag.Description().Summary != "Provider registry.terraform.io/hashicorp/test doesn't support provider_meta" { t.Errorf("Unexpected error: %+v", diag.Description()) } switch diag.Description().Detail { case fmt.Sprintf(errorSummary, "instance"): rootErr = true case fmt.Sprintf(errorSummary, "resource"): subErr = true default: t.Errorf("Unexpected error: %s", diag.Description()) } } if !rootErr { t.Errorf("Expected unsupported provider_meta block error for root module, none received") } if !subErr { t.Errorf("Expected unsupported provider_meta block error for sub-module, none received") } } func TestContext2Apply_ProviderMeta_refresh_setInvalid(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn // we need a matching schema for plan/apply so they don't error schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { Type: cty.String, Required: true, }, }, } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() assertNoErrors(t, diags) _, diags = ctx.Apply() assertNoErrors(t, diags) // change the schema before refresh, to test that it errors schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "quux": { Type: cty.String, Required: true, }, }, } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, State: ctx.State(), }) _, diags = ctx.Refresh() if !diags.HasErrors() { t.Fatalf("refresh supposed to error, has no errors") } var reqErr, invalidErr bool for _, diag := range diags { switch diag.Description().Summary { case "Missing required argument": if diag.Description().Detail == `The argument "quux" is required, but no definition was found.` { reqErr = true } else { t.Errorf("Unexpected error %+v", diag.Description()) } case "Unsupported argument": if diag.Description().Detail == `An argument named "baz" is not expected here.` { invalidErr = true } else { t.Errorf("Unexpected error %+v", diag.Description()) } default: t.Errorf("Unexpected error %+v", diag.Description()) } } if !reqErr { t.Errorf("Expected missing required argument error, none received") } if !invalidErr { t.Errorf("Expected unsupported argument error, none received") } } func TestContext2Apply_ProviderMeta_refreshdata_set(t *testing.T) { m := testModule(t, "provider-meta-data-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { Type: cty.String, Required: true, }, }, } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) rdsPMs := map[string]cty.Value{} p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { rdsPMs[req.TypeName] = req.ProviderMeta switch req.TypeName { case "test_data_source": log.Printf("[TRACE] test_data_source RDSR returning") return providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("yo"), "foo": cty.StringVal("bar"), }), } case "test_file": log.Printf("[TRACE] test_file RDSR returning") return providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("bar"), "rendered": cty.StringVal("baz"), "template": cty.StringVal(""), }), } default: // config drift, oops log.Printf("[TRACE] unknown request TypeName: %q", req.TypeName) return providers.ReadDataSourceResponse{} } } _, diags := ctx.Plan() assertNoErrors(t, diags) _, diags = ctx.Apply() assertNoErrors(t, diags) _, diags = ctx.Refresh() assertNoErrors(t, diags) if !p.ReadDataSourceCalled { t.Fatalf("ReadDataSource not called") } expectations := map[string]cty.Value{} if pm, ok := rdsPMs["test_file"]; !ok { t.Fatalf("sub-module ReadDataSource not called") } else if pm.IsNull() { t.Fatalf("null ProviderMeta in sub-module ReadDataSource") } else { expectations["quux-submodule"] = pm } if pm, ok := rdsPMs["test_data_source"]; !ok { t.Fatalf("root module ReadDataSource not called") } else if pm.IsNull() { t.Fatalf("null ProviderMeta in root module ReadDataSource") } else { expectations["quux"] = pm } type metaStruct struct { Baz string `cty:"baz"` } for expected, v := range expectations { var meta metaStruct err := gocty.FromCtyValue(v, &meta) if err != nil { t.Fatalf("Error parsing cty value: %s", err) } if meta.Baz != expected { t.Fatalf("Expected meta.Baz to be %q, got %q", expected, meta.Baz) } } } func TestContext2Apply_ProviderMeta_refreshdata_unset(t *testing.T) { m := testModule(t, "provider-meta-data-unset") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { Type: cty.String, Required: true, }, }, } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) rdsPMs := map[string]cty.Value{} p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { rdsPMs[req.TypeName] = req.ProviderMeta switch req.TypeName { case "test_data_source": return providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("yo"), "foo": cty.StringVal("bar"), }), } case "test_file": return providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("bar"), "rendered": cty.StringVal("baz"), "template": cty.StringVal(""), }), } default: // config drift, oops return providers.ReadDataSourceResponse{} } } _, diags := ctx.Plan() assertNoErrors(t, diags) _, diags = ctx.Apply() assertNoErrors(t, diags) if !p.ReadDataSourceCalled { t.Fatalf("ReadDataSource not called") } if pm, ok := rdsPMs["test_file"]; !ok { t.Fatalf("sub-module ReadDataSource not called") } else if !pm.IsNull() { t.Fatalf("non-null ProviderMeta in sub-module ReadDataSource") } if pm, ok := rdsPMs["test_data_source"]; !ok { t.Fatalf("root module ReadDataSource not called") } else if !pm.IsNull() { t.Fatalf("non-null ProviderMeta in root module ReadDataSource") } } func TestContext2Apply_ProviderMeta_refreshdata_setNoSchema(t *testing.T) { m := testModule(t, "provider-meta-data-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("yo"), "foo": cty.StringVal("bar"), }), } _, diags := ctx.Refresh() if !diags.HasErrors() { t.Fatalf("refresh supposed to error, has no errors") } var rootErr, subErr bool errorSummary := "The resource data.test_%s.foo belongs to a provider that doesn't support provider_meta blocks" for _, diag := range diags { if diag.Description().Summary != "Provider registry.terraform.io/hashicorp/test doesn't support provider_meta" { t.Errorf("Unexpected error: %+v", diag.Description()) } switch diag.Description().Detail { case fmt.Sprintf(errorSummary, "data_source"): rootErr = true case fmt.Sprintf(errorSummary, "file"): subErr = true default: t.Errorf("Unexpected error: %s", diag.Description()) } } if !rootErr { t.Errorf("Expected unsupported provider_meta block error for root module, none received") } if !subErr { t.Errorf("Expected unsupported provider_meta block error for sub-module, none received") } } func TestContext2Apply_ProviderMeta_refreshdata_setInvalid(t *testing.T) { m := testModule(t, "provider-meta-data-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn schema := p.ProviderSchema() schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "quux": { Type: cty.String, Required: true, }, }, } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("yo"), "foo": cty.StringVal("bar"), }), } _, diags := ctx.Refresh() if !diags.HasErrors() { t.Fatalf("refresh supposed to error, has no errors") } var reqErr, invalidErr bool for _, diag := range diags { switch diag.Description().Summary { case "Missing required argument": if diag.Description().Detail == `The argument "quux" is required, but no definition was found.` { reqErr = true } else { t.Errorf("Unexpected error %+v", diag.Description()) } case "Unsupported argument": if diag.Description().Detail == `An argument named "baz" is not expected here.` { invalidErr = true } else { t.Errorf("Unexpected error %+v", diag.Description()) } default: t.Errorf("Unexpected error %+v", diag.Description()) } } if !reqErr { t.Errorf("Expected missing required argument error, none received") } if !invalidErr { t.Errorf("Expected unsupported argument error, none received") } } func TestContext2Apply_expandModuleVariables(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` module "mod1" { for_each = toset(["a"]) source = "./mod" } module "mod2" { source = "./mod" in = module.mod1["a"].out } `, "mod/main.tf": ` resource "aws_instance" "foo" { foo = var.in } variable "in" { type = string default = "default" } output "out" { value = aws_instance.foo.id } `, }) p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } expected := ` module.mod1["a"]: aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = default type = aws_instance Outputs: out = foo module.mod2: aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = foo type = aws_instance Dependencies: module.mod1.aws_instance.foo` if state.String() != expected { t.Fatalf("expected:\n%s\ngot:\n%s\n", expected, state) } } func TestContext2Apply_inheritAndStoreCBD(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` resource "aws_instance" "foo" { } resource "aws_instance" "cbd" { foo = aws_instance.foo.id lifecycle { create_before_destroy = true } } `, }) p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } foo := state.ResourceInstance(mustResourceInstanceAddr("aws_instance.foo")) if !foo.Current.CreateBeforeDestroy { t.Fatal("aws_instance.foo should also be create_before_destroy") } } func TestContext2Apply_moduleDependsOn(t *testing.T) { m := testModule(t, "apply-module-depends-on") p := testProvider("test") // each instance being applied should happen in sequential order applied := int64(0) p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { cfg := req.Config.AsValueMap() foo := cfg["foo"].AsString() ord := atomic.LoadInt64(&applied) resp := providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("data"), "foo": cfg["foo"], }), } if foo == "a" && ord < 4 { // due to data source "a"'s module depending on instance 4, this // should not be less than 4 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("data source a read too early")) } if foo == "b" && ord < 1 { // due to data source "b"'s module depending on instance 1, this // should not be less than 1 resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("data source b read too early")) } return resp } p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { state := req.PlannedState.AsValueMap() num, _ := state["num"].AsBigFloat().Float64() ord := int64(num) if !atomic.CompareAndSwapInt64(&applied, ord-1, ord) { actual := atomic.LoadInt64(&applied) resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("instance %d was applied after %d", ord, actual)) } state["id"] = cty.StringVal(fmt.Sprintf("test_%d", ord)) state["type"] = cty.StringVal("test_instance") resp.NewState = cty.ObjectVal(state) return resp } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } for _, res := range plan.Changes.Resources { if res.Action != plans.NoOp { t.Fatalf("expected NoOp, got %s for %s", res.Action, res.Addr) } } } func TestContext2Apply_moduleSelfReference(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` module "test" { source = "./test" a = module.test.b } output "c" { value = module.test.c } `, "test/main.tf": ` variable "a" {} resource "test_instance" "test" { } output "b" { value = test_instance.test.id } output "c" { value = var.a }`}) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, PlanMode: plans.DestroyMode, }) _, diags = ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } if !state.Empty() { t.Fatal("expected empty state, got:", state) } } func TestContext2Apply_moduleExpandDependsOn(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` module "child" { count = 1 source = "./child" depends_on = [test_instance.a, test_instance.b] } resource "test_instance" "a" { } resource "test_instance" "b" { } `, "child/main.tf": ` resource "test_instance" "foo" { } output "myoutput" { value = "literal string" } `}) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, PlanMode: plans.DestroyMode, State: state, }) _, diags = ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } if !state.Empty() { t.Fatal("expected empty state, got:", state) } } func TestContext2Apply_scaleInCBD(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` variable "ct" { type = number } resource "test_instance" "a" { count = var.ct } resource "test_instance" "b" { require_new = local.removable lifecycle { create_before_destroy = true } } resource "test_instance" "c" { require_new = test_instance.b.id lifecycle { create_before_destroy = true } } output "out" { value = join(".", test_instance.a[*].id) } locals { removable = join(".", test_instance.a[*].id) } `}) state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("test_instance.a[0]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"a0"}`), Dependencies: []addrs.ConfigResource{}, CreateBeforeDestroy: true, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("test_instance.a[1]").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"a1"}`), Dependencies: []addrs.ConfigResource{}, CreateBeforeDestroy: true, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("test_instance.b").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"b", "require_new":"old.old"}`), Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_instance.a")}, CreateBeforeDestroy: true, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), ) root.SetResourceInstanceCurrent( mustResourceInstanceAddr("test_instance.c").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"c", "require_new":"b"}`), Dependencies: []addrs.ConfigResource{ mustConfigResourceAddr("test_instance.a"), mustConfigResourceAddr("test_instance.b"), }, CreateBeforeDestroy: true, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), ) p := testProvider("test") p.PlanResourceChangeFn = func(r providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { n := r.ProposedNewState.AsValueMap() if r.PriorState.IsNull() { n["id"] = cty.UnknownVal(cty.String) resp.PlannedState = cty.ObjectVal(n) return resp } p := r.PriorState.AsValueMap() priorRN := p["require_new"] newRN := n["require_new"] if eq := priorRN.Equals(newRN); !eq.IsKnown() || eq.False() { resp.RequiresReplace = []cty.Path{{cty.GetAttrStep{Name: "require_new"}}} n["id"] = cty.UnknownVal(cty.String) } resp.PlannedState = cty.ObjectVal(n) return resp } // reduce the count to 1 ctx := testContext2(t, &ContextOpts{ Variables: InputValues{ "ct": &InputValue{ Value: cty.NumberIntVal(1), SourceType: ValueFromCaller, }, }, Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, State: state, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } state, diags = ctx.Apply() if diags.HasErrors() { log.Fatal(diags.ErrWithWarnings()) } // check the output, as those can't cause an error planning the value out := state.RootModule().OutputValues["out"].Value.AsString() if out != "a0" { t.Fatalf(`expected output "a0", got: %q`, out) } // reduce the count to 0 ctx = testContext2(t, &ContextOpts{ Variables: InputValues{ "ct": &InputValue{ Value: cty.NumberIntVal(0), SourceType: ValueFromCaller, }, }, Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, State: state, }) _, diags = ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } // check the output, as those can't cause an error planning the value out = state.RootModule().OutputValues["out"].Value.AsString() if out != "" { t.Fatalf(`expected output "", got: %q`, out) } } // Ensure that we can destroy when a provider references a resource that will // also be destroyed func TestContext2Apply_destroyProviderReference(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` provider "null" { value = "" } module "mod" { source = "./mod" } provider "test" { value = module.mod.output } resource "test_instance" "bar" { } `, "mod/main.tf": ` data "null_data_source" "foo" { count = 1 } output "output" { value = data.null_data_source.foo[0].output } `}) schemaFn := func(name string) *ProviderSchema { return &ProviderSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": { Type: cty.String, Required: true, }, }, }, ResourceTypes: map[string]*configschema.Block{ name + "_instance": { Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, Computed: true, }, "foo": { Type: cty.String, Optional: true, }, }, }, }, DataSources: map[string]*configschema.Block{ name + "_data_source": { Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, Computed: true, }, "output": { Type: cty.String, Computed: true, }, }, }, }, } } testP := new(MockProvider) testP.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { return providers.ReadResourceResponse{NewState: req.PriorState} } testP.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schemaFn("test")) providerConfig := "" testP.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { value := req.Config.GetAttr("value") if value.IsKnown() && !value.IsNull() { providerConfig = value.AsString() } else { providerConfig = "" } return resp } testP.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { if providerConfig != "valid" { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("provider config is %q", providerConfig)) return } return testApplyFn(req) } testP.PlanResourceChangeFn = testDiffFn nullP := new(MockProvider) nullP.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { return providers.ReadResourceResponse{NewState: req.PriorState} } nullP.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schemaFn("null")) nullP.ApplyResourceChangeFn = testApplyFn nullP.PlanResourceChangeFn = testDiffFn nullP.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("ID"), "output": cty.StringVal("valid"), }), } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(testP), addrs.NewDefaultProvider("null"): testProviderFuncFixed(nullP), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(testP), addrs.NewDefaultProvider("null"): testProviderFuncFixed(nullP), }, State: state, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("destroy plan errors: %s", diags.Err()) } if _, diags := ctx.Apply(); diags.HasErrors() { t.Fatalf("destroy apply errors: %s", diags.Err()) } } // Destroying properly requires pruning out all unneeded config nodes to // prevent incorrect expansion evaluation. func TestContext2Apply_destroyInterModuleExpansion(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` data "test_data_source" "a" { for_each = { one = "thing" } } locals { module_input = { for k, v in data.test_data_source.a : k => v.id } } module "mod1" { source = "./mod" input = local.module_input } module "mod2" { source = "./mod" input = module.mod1.outputs } resource "test_instance" "bar" { for_each = module.mod2.outputs } output "module_output" { value = module.mod2.outputs } output "test_instances" { value = test_instance.bar } `, "mod/main.tf": ` variable "input" { } data "test_data_source" "foo" { for_each = var.input } output "outputs" { value = data.test_data_source.foo } `}) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { return providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("data_source"), "foo": cty.StringVal("output"), }), } } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } destroy := func() { ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, State: state, PlanMode: plans.DestroyMode, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("destroy plan errors: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("destroy apply errors: %s", diags.Err()) } } destroy() // Destroying again from the empty state should not cause any errors either destroy() } func TestContext2Apply_createBeforeDestroyWithModule(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` variable "v" {} module "mod" { source = "./mod" in = var.v } resource "test_resource" "a" { value = var.v depends_on = [module.mod] lifecycle { create_before_destroy = true } } `, "mod/main.tf": ` variable "in" {} resource "test_resource" "a" { value = var.in } `}) p := testProvider("test") p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { proposed := req.ProposedNewState.AsValueMap() proposed["id"] = cty.UnknownVal(cty.String) return providers.PlanResourceChangeResponse{ PlannedState: cty.ObjectVal(proposed), RequiresReplace: []cty.Path{cty.Path{cty.GetAttrStep{Name: "value"}}}, } } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, Variables: InputValues{ "v": &InputValue{ Value: cty.StringVal("A"), }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, Variables: InputValues{ "v": &InputValue{ Value: cty.StringVal("B"), }, }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } func TestContext2Apply_forcedCBD(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` variable "v" {} resource "test_instance" "a" { require_new = var.v } resource "test_instance" "b" { depends_on = [test_instance.a] lifecycle { create_before_destroy = true } } `}) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, Variables: InputValues{ "v": &InputValue{ Value: cty.StringVal("A"), }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, Variables: InputValues{ "v": &InputValue{ Value: cty.StringVal("B"), }, }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } func TestContext2Apply_removeReferencedResource(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` variable "ct" { } resource "test_resource" "to_remove" { count = var.ct } resource "test_resource" "c" { value = join("", test_resource.to_remove[*].id) } `}) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, Variables: InputValues{ "ct": &InputValue{ Value: cty.NumberIntVal(1), }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, Variables: InputValues{ "ct": &InputValue{ Value: cty.NumberIntVal(0), }, }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } func TestContext2Apply_variableSensitivity(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` variable "sensitive_var" { default = "foo" sensitive = true } variable "sensitive_id" { default = "secret id" sensitive = true } resource "test_resource" "foo" { value = var.sensitive_var network_interface { network_interface_id = var.sensitive_id } }`, }) p := new(MockProvider) p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { return providers.ReadResourceResponse{NewState: req.PriorState} } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "test_resource": { Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, Computed: true, }, "value": { Type: cty.String, Optional: true, Computed: true, }, }, BlockTypes: map[string]*configschema.NestedBlock{ "network_interface": { Block: configschema.Block{ Attributes: map[string]*configschema.Attribute{ "network_interface_id": {Type: cty.String, Optional: true}, "device_index": {Type: cty.Number, Optional: true}, }, }, Nesting: configschema.NestingSet, }, }, }, }, }) p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } // Run a second apply with no changes ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } // Now change the variable value for sensitive_var ctx = testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, Variables: InputValues{ "sensitive_var": &InputValue{ Value: cty.StringVal("bar"), }, }, State: state, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } func TestContext2Apply_variableSensitivityPropagation(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` variable "sensitive_map" { type = map(string) default = { "x" = "foo" } sensitive = true } resource "test_resource" "foo" { value = var.sensitive_map.x } `, }) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } verifySensitiveValue := func(pvms []cty.PathValueMarks) { if len(pvms) != 1 { t.Fatalf("expected 1 sensitive path, got %d", len(pvms)) } pvm := pvms[0] if gotPath, wantPath := pvm.Path, cty.GetAttrPath("value"); !gotPath.Equals(wantPath) { t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath) } if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks("sensitive"); !gotMarks.Equal(wantMarks) { t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks) } } addr := mustResourceInstanceAddr("test_resource.foo") fooChangeSrc := plan.Changes.ResourceInstance(addr) verifySensitiveValue(fooChangeSrc.AfterValMarks) state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } fooState := state.ResourceInstance(addr) verifySensitiveValue(fooState.Current.AttrSensitivePaths) } func TestContext2Apply_variableSensitivityProviders(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` resource "test_resource" "foo" { sensitive_value = "should get marked" } resource "test_resource" "bar" { value = test_resource.foo.sensitive_value random = test_resource.foo.id # not sensitive nesting_single { value = "abc" sensitive_value = "xyz" } } resource "test_resource" "baz" { value = test_resource.bar.nesting_single.sensitive_value } `, }) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) plan, diags := ctx.Plan() if diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } verifySensitiveValue := func(pvms []cty.PathValueMarks) { if len(pvms) != 1 { t.Fatalf("expected 1 sensitive path, got %d", len(pvms)) } pvm := pvms[0] if gotPath, wantPath := pvm.Path, cty.GetAttrPath("value"); !gotPath.Equals(wantPath) { t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath) } if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks("sensitive"); !gotMarks.Equal(wantMarks) { t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks) } } // Sensitive attributes (defined by the provider) are marked // as sensitive when referenced from another resource // "bar" references sensitive resources in "foo" barAddr := mustResourceInstanceAddr("test_resource.bar") barChangeSrc := plan.Changes.ResourceInstance(barAddr) verifySensitiveValue(barChangeSrc.AfterValMarks) bazAddr := mustResourceInstanceAddr("test_resource.baz") bazChangeSrc := plan.Changes.ResourceInstance(bazAddr) verifySensitiveValue(bazChangeSrc.AfterValMarks) state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } barState := state.ResourceInstance(barAddr) verifySensitiveValue(barState.Current.AttrSensitivePaths) bazState := state.ResourceInstance(bazAddr) verifySensitiveValue(bazState.Current.AttrSensitivePaths) } func TestContext2Apply_variableSensitivityChange(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` variable "sensitive_var" { default = "hello" sensitive = true } resource "test_resource" "foo" { value = var.sensitive_var }`, }) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo", "value":"hello"}`), // No AttrSensitivePaths present }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, ) }), }) _, diags := ctx.Plan() assertNoErrors(t, diags) addr := mustResourceInstanceAddr("test_resource.foo") state, diags := ctx.Apply() assertNoErrors(t, diags) fooState := state.ResourceInstance(addr) if len(fooState.Current.AttrSensitivePaths) != 1 { t.Fatalf("wrong number of sensitive paths, expected 1, got, %v", len(fooState.Current.AttrSensitivePaths)) } got := fooState.Current.AttrSensitivePaths[0] want := cty.PathValueMarks{ Path: cty.GetAttrPath("value"), Marks: cty.NewValueMarks("sensitive"), } if !got.Equal(want) { t.Fatalf("wrong value marks; got:\n%#v\n\nwant:\n%#v\n", got, want) } m2 := testModuleInline(t, map[string]string{ "main.tf": ` variable "sensitive_var" { default = "hello" sensitive = false } resource "test_resource" "foo" { value = var.sensitive_var }`, }) ctx2 := testContext2(t, &ContextOpts{ Config: m2, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, State: state, }) _, diags = ctx2.Plan() assertNoErrors(t, diags) stateWithoutSensitive, diags := ctx.Apply() assertNoErrors(t, diags) fooState2 := stateWithoutSensitive.ResourceInstance(addr) if len(fooState2.Current.AttrSensitivePaths) > 0 { t.Fatalf("wrong number of sensitive paths, expected 0, got, %v", len(fooState2.Current.AttrSensitivePaths)) } } func TestContext2Apply_moduleVariableOptionalAttributes(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` terraform { experiments = [module_variable_optional_attrs] } variable "in" { type = object({ required = string optional = optional(string) }) } output "out" { value = var.in } `}) ctx := testContext2(t, &ContextOpts{ Variables: InputValues{ "in": &InputValue{ Value: cty.MapVal(map[string]cty.Value{ "required": cty.StringVal("boop"), }), SourceType: ValueFromCaller, }, }, Config: m, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } got := state.RootModule().OutputValues["out"].Value want := cty.ObjectVal(map[string]cty.Value{ "required": cty.StringVal("boop"), // Because "optional" was marked as optional, it got silently filled // in as a null value of string type rather than returning an error. "optional": cty.NullVal(cty.String), }) if !want.RawEquals(got) { t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) } } func TestContext2Apply_provisionerSensitive(t *testing.T) { m := testModule(t, "apply-provisioner-sensitive") p := testProvider("aws") pr := testProvisioner() pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { if req.Config.ContainsMarked() { t.Fatalf("unexpectedly marked config value: %#v", req.Config) } command := req.Config.GetAttr("command") if command.IsMarked() { t.Fatalf("unexpectedly marked command argument: %#v", command.Marks()) } req.UIOutput.Output(fmt.Sprintf("Executing: %q", command.AsString())) return } p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn h := new(MockHook) ctx := testContext2(t, &ContextOpts{ Config: m, Hooks: []Hook{h}, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, Provisioners: map[string]provisioners.Factory{ "shell": testProvisionerFuncFixed(pr), }, Variables: InputValues{ "password": &InputValue{ Value: cty.StringVal("secret"), SourceType: ValueFromCaller, }, }, }) if _, diags := ctx.Plan(); diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("plan failed") } // "restart" provisioner pr.CloseCalled = false state, diags := ctx.Apply() if diags.HasErrors() { logDiagnostics(t, diags) t.Fatal("apply failed") } actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyProvisionerSensitiveStr) if actual != expected { t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) } // Verify apply was invoked if !pr.ProvisionResourceCalled { t.Fatalf("provisioner was not called on apply") } // Verify output was suppressed if !h.ProvisionOutputCalled { t.Fatalf("ProvisionOutput hook not called") } if got, doNotWant := h.ProvisionOutputMessage, "secret"; strings.Contains(got, doNotWant) { t.Errorf("sensitive value %q included in output:\n%s", doNotWant, got) } if got, want := h.ProvisionOutputMessage, "output suppressed"; !strings.Contains(got, want) { t.Errorf("expected hook to be called with %q, but was:\n%s", want, got) } } func TestContext2Apply_warnings(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` resource "test_resource" "foo" { }`, }) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { resp := testApplyFn(req) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.SimpleWarning("warning")) return resp } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) if _, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("plan errors: %s", diags.Err()) } state, diags := ctx.Apply() if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } inst := state.ResourceInstance(mustResourceInstanceAddr("test_resource.foo")) if inst == nil { t.Fatal("missing 'test_resource.foo' in state:", state) } } func TestContext2Apply_rpcDiagnostics(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` resource "test_instance" "a" { } `, }) p := testProvider("test") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { resp = testApplyFn(req) resp.Diagnostics = resp.Diagnostics.Append(tfdiags.SimpleWarning("don't frobble")) return resp } p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, }, }, }) ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.Err()) } _, diags = ctx.Apply() if diags.HasErrors() { t.Fatal(diags.Err()) } if len(diags) == 0 { t.Fatal("expected warnings") } for _, d := range diags { des := d.Description().Summary if !strings.Contains(des, "frobble") { t.Fatalf(`expected frobble, got %q`, des) } } } func TestContext2Apply_dataSensitive(t *testing.T) { m := testModule(t, "apply-data-sensitive") p := testProvider("null") p.PlanResourceChangeFn = testDiffFn p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { // add the required id m := req.Config.AsValueMap() m["id"] = cty.StringVal("foo") return providers.ReadDataSourceResponse{ State: cty.ObjectVal(m), } } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("null"): testProviderFuncFixed(p), }, }) if p, diags := ctx.Plan(); diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { t.Logf(legacyDiffComparisonString(p.Changes)) } state, diags := ctx.Apply() assertNoErrors(t, diags) addr := mustResourceInstanceAddr("data.null_data_source.testing") dataSourceState := state.ResourceInstance(addr) pvms := dataSourceState.Current.AttrSensitivePaths if len(pvms) != 1 { t.Fatalf("expected 1 sensitive path, got %d", len(pvms)) } pvm := pvms[0] if gotPath, wantPath := pvm.Path, cty.GetAttrPath("foo"); !gotPath.Equals(wantPath) { t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath) } if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks("sensitive"); !gotMarks.Equal(wantMarks) { t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks) } } func TestContext2Apply_errorRestorePrivateData(t *testing.T) { // empty config to remove our resource m := testModuleInline(t, map[string]string{ "main.tf": "", }) p := simpleMockProvider() p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{ // we error during apply, which will trigger core to preserve the last // known state, including private data Diagnostics: tfdiags.Diagnostics(nil).Append(errors.New("oops")), } addr := mustResourceInstanceAddr("test_object.a") state := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo"}`), Private: []byte("private"), }, 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), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.Err()) } state, _ = ctx.Apply() if string(state.ResourceInstance(addr).Current.Private) != "private" { t.Fatal("missing private data in state") } } func TestContext2Apply_errorRestoreStatus(t *testing.T) { // empty config to remove our resource m := testModuleInline(t, map[string]string{ "main.tf": "", }) p := simpleMockProvider() p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { // We error during apply, but return the current object state. resp.Diagnostics = resp.Diagnostics.Append(errors.New("oops")) // return a warning too to make sure it isn't dropped resp.Diagnostics = resp.Diagnostics.Append(tfdiags.SimpleWarning("warned")) resp.NewState = req.PriorState resp.Private = req.PlannedPrivate return resp } addr := mustResourceInstanceAddr("test_object.a") state := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent(addr, &states.ResourceInstanceObjectSrc{ Status: states.ObjectTainted, AttrsJSON: []byte(`{"test_string":"foo"}`), Private: []byte("private"), Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.b")}, }, 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), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.Err()) } state, diags = ctx.Apply() errString := diags.ErrWithWarnings().Error() if !strings.Contains(errString, "oops") || !strings.Contains(errString, "warned") { t.Fatalf("error missing expected info: %q", errString) } if len(diags) != 2 { t.Fatalf("expected 1 error and 1 warning, got: %q", errString) } res := state.ResourceInstance(addr) if res == nil { t.Fatal("resource was removed from state") } if res.Current.Status != states.ObjectTainted { t.Fatal("resource should still be tainted in the state") } if len(res.Current.Dependencies) != 1 || !res.Current.Dependencies[0].Equal(mustConfigResourceAddr("test_object.b")) { t.Fatalf("incorrect dependencies, got %q", res.Current.Dependencies) } if string(res.Current.Private) != "private" { t.Fatalf("incorrect private data, got %q", res.Current.Private) } } func TestContext2Apply_nonConformingResponse(t *testing.T) { // empty config to remove our resource m := testModuleInline(t, map[string]string{ "main.tf": ` resource "test_object" "a" { test_string = "x" } `, }) p := simpleMockProvider() respDiags := tfdiags.Diagnostics(nil).Append(tfdiags.SimpleWarning("warned")) respDiags = respDiags.Append(errors.New("oops")) p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{ // Don't lose these diagnostics Diagnostics: respDiags, // This state is missing required attributes, and should produce an error NewState: cty.ObjectVal(map[string]cty.Value{ "test_string": cty.StringVal("x"), }), } ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.Err()) } _, diags = ctx.Apply() errString := diags.ErrWithWarnings().Error() if !strings.Contains(errString, "oops") || !strings.Contains(errString, "warned") { t.Fatalf("error missing expected info: %q", errString) } // we should have more than the ones returned from the provider, and they // should not be coalesced into a single value if len(diags) < 3 { t.Fatalf("incorrect diagnostics, got %d values with %s", len(diags), diags.ErrWithWarnings()) } } func TestContext2Apply_nilResponse(t *testing.T) { // empty config to remove our resource m := testModuleInline(t, map[string]string{ "main.tf": ` resource "test_object" "a" { } `, }) p := simpleMockProvider() p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{} ctx := testContext2(t, &ContextOpts{ Config: m, Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) _, diags := ctx.Plan() if diags.HasErrors() { t.Fatal(diags.Err()) } _, diags = ctx.Apply() if !diags.HasErrors() { t.Fatal("expected and error") } errString := diags.ErrWithWarnings().Error() if !strings.Contains(errString, "invalid nil value") { t.Fatalf("error missing expected info: %q", errString) } } //////////////////////////////////////////////////////////////////////////////// // NOTE: Due to the size of this file, new tests should be added to // context_apply2_test.go. ////////////////////////////////////////////////////////////////////////////////