| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package command | |
| import ( | |
| "bytes" | |
| "context" | |
| "fmt" | |
| "io/ioutil" | |
| "os" | |
| "path" | |
| "path/filepath" | |
| "strings" | |
| "sync" | |
| "testing" | |
| "time" | |
| "github.com/davecgh/go-spew/spew" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| backendinit "github.com/hashicorp/terraform/internal/backend/init" | |
| "github.com/hashicorp/terraform/internal/checks" | |
| "github.com/hashicorp/terraform/internal/command/clistate" | |
| "github.com/hashicorp/terraform/internal/configs/configschema" | |
| "github.com/hashicorp/terraform/internal/plans" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| testing_provider "github.com/hashicorp/terraform/internal/providers/testing" | |
| "github.com/hashicorp/terraform/internal/states" | |
| "github.com/hashicorp/terraform/internal/states/statefile" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| func TestPlan(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| } | |
| func TestPlan_lockedState(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| unlock, err := testLockState(t, testDataDir, filepath.Join(td, DefaultStateFilename)) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer unlock() | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| if code == 0 { | |
| t.Fatal("expected error", done(t).Stdout()) | |
| } | |
| output := done(t).Stderr() | |
| if !strings.Contains(output, "lock") { | |
| t.Fatal("command output does not look like a lock error:", output) | |
| } | |
| } | |
| func TestPlan_plan(t *testing.T) { | |
| tmp := t.TempDir() | |
| t.Chdir(tmp) | |
| planPath := testPlanFileNoop(t) | |
| p := testProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{planPath} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("wrong exit status %d; want 1\nstderr: %s", code, output.Stderr()) | |
| } | |
| } | |
| func TestPlan_destroy(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"bar"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("test"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| }) | |
| outPath := testTempFile(t) | |
| statePath := testStateFile(t, originalState) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-destroy", | |
| "-out", outPath, | |
| "-state", statePath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| plan := testReadPlan(t, outPath) | |
| for _, rc := range plan.Changes.Resources { | |
| if got, want := rc.Action, plans.Delete; got != want { | |
| t.Fatalf("wrong action %s for %s; want %s\nplanned change: %s", got, rc.Addr, want, spew.Sdump(rc)) | |
| } | |
| } | |
| } | |
| func TestPlan_noState(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| // Verify that refresh was called | |
| if p.ReadResourceCalled { | |
| t.Fatal("ReadResource should not be called") | |
| } | |
| // Verify that the provider was called with the existing state | |
| actual := p.PlanResourceChangeRequest.PriorState | |
| expected := cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_instance"].Body.ImpliedType()) | |
| if !expected.RawEquals(actual) { | |
| t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) | |
| } | |
| } | |
| func TestPlan_generatedConfigPath(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-import-config-gen"), td) | |
| t.Chdir(td) | |
| genPath := filepath.Join(td, "generated.tf") | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ | |
| ImportedResources: []providers.ImportedResource{ | |
| { | |
| TypeName: "test_instance", | |
| State: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("bar"), | |
| }), | |
| Private: nil, | |
| }, | |
| }, | |
| } | |
| args := []string{ | |
| "-generate-config-out", genPath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| testFileEquals(t, genPath, filepath.Join(td, "generated.tf.expected")) | |
| } | |
| func TestPlan_outPath(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| outPath := filepath.Join(td, "test.plan") | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{ | |
| PlannedState: cty.NullVal(cty.EmptyObject), | |
| } | |
| args := []string{ | |
| "-out", outPath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| testReadPlan(t, outPath) // will call t.Fatal itself if the file cannot be read | |
| } | |
| func TestPlan_outPathNoChange(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| // Aside from "id" (which is computed) the values here must | |
| // exactly match the values in the "plan" test fixture in order | |
| // to produce the empty plan we need for this test. | |
| AttrsJSON: []byte(`{"id":"bar","ami":"bar","network_interface":[{"description":"Main network interface","device_index":"0"}]}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("test"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| }) | |
| statePath := testStateFile(t, originalState) | |
| outPath := filepath.Join(td, "test.plan") | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-out", outPath, | |
| "-state", statePath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| plan := testReadPlan(t, outPath) | |
| if !plan.Changes.Empty() { | |
| t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan)) | |
| } | |
| } | |
| func TestPlan_outPathWithError(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-fail-condition"), td) | |
| t.Chdir(td) | |
| outPath := filepath.Join(td, "test.plan") | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{ | |
| PlannedState: cty.NullVal(cty.EmptyObject), | |
| } | |
| args := []string{ | |
| "-out", outPath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code == 0 { | |
| t.Fatal("expected non-zero exit status", output) | |
| } | |
| plan := testReadPlan(t, outPath) // will call t.Fatal itself if the file cannot be read | |
| if !plan.Errored { | |
| t.Fatal("plan should be marked with Errored") | |
| } | |
| if plan.Checks == nil { | |
| t.Fatal("plan contains no checks") | |
| } | |
| // the checks should only contain one failure | |
| results := plan.Checks.ConfigResults.Elements() | |
| if len(results) != 1 { | |
| t.Fatal("incorrect number of check results", len(results)) | |
| } | |
| if results[0].Value.Status != checks.StatusFail { | |
| t.Errorf("incorrect status, got %s", results[0].Value.Status) | |
| } | |
| } | |
| // When using "-out" with a backend, the plan should encode the backend config | |
| func TestPlan_outBackend(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-out-backend"), td) | |
| t.Chdir(td) | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"bar","ami":"bar"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("test"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| }) | |
| // Set up our backend state | |
| dataState, srv := testBackendState(t, originalState, 200) | |
| defer srv.Close() | |
| testStateFileRemote(t, dataState) | |
| outPath := "foo" | |
| p := testProvider() | |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": { | |
| Type: cty.String, | |
| Computed: true, | |
| }, | |
| "ami": { | |
| Type: cty.String, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { | |
| return providers.PlanResourceChangeResponse{ | |
| PlannedState: req.ProposedNewState, | |
| } | |
| } | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-out", outPath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Logf("stdout: %s", output.Stdout()) | |
| t.Fatalf("plan command failed with exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| plan := testReadPlan(t, outPath) | |
| if !plan.Changes.Empty() { | |
| t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan)) | |
| } | |
| if plan.Backend == nil { | |
| t.Fatal("unexpected nil Backend") | |
| } | |
| if got, want := plan.Backend.Type, "http"; got != want { | |
| t.Errorf("wrong backend type %q; want %q", got, want) | |
| } | |
| if got, want := plan.Backend.Workspace, "default"; got != want { | |
| t.Errorf("wrong backend workspace %q; want %q", got, want) | |
| } | |
| { | |
| httpBackend := backendinit.Backend("http")() | |
| schema := httpBackend.ConfigSchema() | |
| got, err := plan.Backend.Config.Decode(schema.ImpliedType()) | |
| if err != nil { | |
| t.Fatalf("failed to decode backend config in plan: %s", err) | |
| } | |
| want, err := dataState.Backend.Config(schema) | |
| if err != nil { | |
| t.Fatalf("failed to decode cached config: %s", err) | |
| } | |
| if !want.RawEquals(got) { | |
| t.Errorf("wrong backend config\ngot: %#v\nwant: %#v", got, want) | |
| } | |
| } | |
| } | |
| // When using "-out" with a backend, the plan should encode the backend config | |
| // and also the selected workspace, if workspaces are supported by the backend. | |
| // | |
| // This test demonstrates that setting the workspace in the backend plan | |
| // responds to the selected workspace, versus other tests that show the same process | |
| // when defaulting to the default workspace when there's a lack of information about | |
| // the selected workspace. | |
| // | |
| // To test planning with a non-default workspace we need to use a backend that supports | |
| // workspaces. In this test the `inmem` backend is used. | |
| func TestPlan_outBackend_withWorkspace(t *testing.T) { | |
| // Create a temporary working directory | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-out-backend-workspace"), td) | |
| t.Chdir(td) | |
| // These values are coupled with the test fixture used above. | |
| expectedBackendType := "inmem" | |
| expectedWorkspace := "custom-workspace" | |
| outPath := "foo" | |
| p := testProvider() | |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": { | |
| Type: cty.String, | |
| Computed: true, | |
| }, | |
| "ami": { | |
| Type: cty.String, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { | |
| return providers.PlanResourceChangeResponse{ | |
| PlannedState: req.ProposedNewState, | |
| } | |
| } | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-out", outPath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Logf("stdout: %s", output.Stdout()) | |
| t.Fatalf("plan command failed with exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| plan := testReadPlan(t, outPath) | |
| if got, want := plan.Backend.Type, expectedBackendType; got != want { | |
| t.Errorf("wrong backend type %q; want %q", got, want) | |
| } | |
| if got, want := plan.Backend.Workspace, expectedWorkspace; got != want { | |
| t.Errorf("wrong backend workspace %q; want %q", got, want) | |
| } | |
| } | |
| // When using "-out" with a state store, the plan should encode the state store config | |
| func TestPlan_outStateStore(t *testing.T) { | |
| // Create a temporary working directory with state_store config | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-out-state-store"), td) | |
| t.Chdir(td) | |
| // Make state that resembles the resource defined in the test fixture | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "foo", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"bar","ami":"bar"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("test"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| }) | |
| var stateBuf bytes.Buffer | |
| if err := statefile.Write(statefile.New(originalState, "", 1), &stateBuf); err != nil { | |
| t.Fatalf("error during test setup: %s", err) | |
| } | |
| stateBytes := stateBuf.Bytes() | |
| // Make a mock provider that: | |
| // 1) will return the state defined above. | |
| // 2) has a schema for the resource being managed in this test. | |
| mock := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store")) | |
| mock.MockStates = testing_provider.NewMockStateBytesWithSingleState( | |
| "test_store", | |
| "default", | |
| stateBytes, | |
| ) | |
| mock.GetProviderSchemaResponse.ResourceTypes = map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": { | |
| Type: cty.String, | |
| Computed: true, | |
| }, | |
| "ami": { | |
| Type: cty.String, | |
| Optional: true, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| mock.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { | |
| return providers.PlanResourceChangeResponse{ | |
| PlannedState: req.ProposedNewState, | |
| } | |
| } | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| AllowExperimentalFeatures: true, | |
| testingOverrides: metaOverridesForProvider(mock), | |
| View: view, | |
| }, | |
| } | |
| outPath := "foo" | |
| args := []string{ | |
| "-out", outPath, | |
| "-no-color", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Logf("stdout: %s", output.Stdout()) | |
| t.Fatalf("plan command failed with exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| plan := testReadPlan(t, outPath) | |
| if !plan.Changes.Empty() { | |
| t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan)) | |
| } | |
| if plan.Backend != nil { | |
| t.Errorf("expected the plan file to not describe a backend, but got %#v", plan.Backend) | |
| } | |
| if plan.StateStore == nil { | |
| t.Errorf("expected the plan file to describe a state store, but it's empty: %#v", plan.StateStore) | |
| } | |
| if got, want := plan.StateStore.Workspace, "default"; got != want { | |
| t.Errorf("wrong workspace %q; want %q", got, want) | |
| } | |
| { | |
| // Comparing the plan's description of the state store | |
| // to the backend state file's description of the state store: | |
| statePath := ".terraform/terraform.tfstate" | |
| sMgr := &clistate.LocalState{Path: statePath} | |
| if err := sMgr.RefreshState(); err != nil { | |
| t.Fatal(err) | |
| } | |
| s := sMgr.State() // The plan should resemble this. | |
| if !plan.StateStore.Provider.Version.Equal(s.StateStore.Provider.Version) { | |
| t.Fatalf("wrong provider version, got %q; want %q", | |
| plan.StateStore.Provider.Version, | |
| s.StateStore.Provider.Version, | |
| ) | |
| } | |
| if !plan.StateStore.Provider.Source.Equals(*s.StateStore.Provider.Source) { | |
| t.Fatalf("wrong provider source, got %q; want %q", | |
| plan.StateStore.Provider.Source, | |
| s.StateStore.Provider.Source, | |
| ) | |
| } | |
| // Is the provider config data correct? | |
| providerSchema := mock.GetProviderSchemaResponse.Provider | |
| providerTy := providerSchema.Body.ImpliedType() | |
| pGot, err := plan.StateStore.Provider.Config.Decode(providerTy) | |
| if err != nil { | |
| t.Fatalf("failed to decode provider config in plan: %s", err) | |
| } | |
| pWant, err := s.StateStore.Provider.Config(providerSchema.Body) | |
| if err != nil { | |
| t.Fatalf("failed to decode cached provider config: %s", err) | |
| } | |
| if !pWant.RawEquals(pGot) { | |
| t.Errorf("wrong provider config\ngot: %#v\nwant: %#v", pGot, pWant) | |
| } | |
| // Is the store config data correct? | |
| storeSchema := mock.GetProviderSchemaResponse.StateStores["test_store"] | |
| ty := storeSchema.Body.ImpliedType() | |
| sGot, err := plan.StateStore.Config.Decode(ty) | |
| if err != nil { | |
| t.Fatalf("failed to decode state store config in plan: %s", err) | |
| } | |
| sWant, err := s.StateStore.Config(storeSchema.Body) | |
| if err != nil { | |
| t.Fatalf("failed to decode cached state store config: %s", err) | |
| } | |
| if !sWant.RawEquals(sGot) { | |
| t.Errorf("wrong state store config\ngot: %#v\nwant: %#v", sGot, sWant) | |
| } | |
| } | |
| } | |
| func TestPlan_refreshFalse(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-existing-state"), td) | |
| t.Chdir(td) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-refresh=false", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| if p.ReadResourceCalled { | |
| t.Fatal("ReadResource should not have been called") | |
| } | |
| } | |
| func TestPlan_refreshTrue(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-existing-state"), td) | |
| t.Chdir(td) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-refresh=true", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| if !p.ReadResourceCalled { | |
| t.Fatalf("ReadResource should have been called") | |
| } | |
| } | |
| // A consumer relies on the fact that running | |
| // terraform plan -refresh=false -refresh=true gives the same result as | |
| // terraform plan -refresh=true. | |
| // While the flag logic itself is handled by the stdlib flags package (and code | |
| // in main() that is tested elsewhere), we verify the overall plan command | |
| // behaviour here in case we accidentally break this with additional logic. | |
| func TestPlan_refreshFalseRefreshTrue(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-existing-state"), td) | |
| t.Chdir(td) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-refresh=false", | |
| "-refresh=true", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| if !p.ReadResourceCalled { | |
| t.Fatal("ReadResource should have been called") | |
| } | |
| } | |
| func TestPlan_state(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| originalState := testState() | |
| statePath := testStateFile(t, originalState) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-state", statePath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| // Verify that the provider was called with the existing state | |
| actual := p.PlanResourceChangeRequest.PriorState | |
| expected := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("bar"), | |
| "ami": cty.NullVal(cty.String), | |
| "network_interface": cty.ListValEmpty(cty.Object(map[string]cty.Type{ | |
| "device_index": cty.String, | |
| "description": cty.String, | |
| })), | |
| }) | |
| if !expected.RawEquals(actual) { | |
| t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) | |
| } | |
| } | |
| func TestPlan_stateDefault(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| // Generate state and move it to the default path | |
| originalState := testState() | |
| statePath := testStateFile(t, originalState) | |
| os.Rename(statePath, path.Join(td, "terraform.tfstate")) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| // Verify that the provider was called with the existing state | |
| actual := p.PlanResourceChangeRequest.PriorState | |
| expected := cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("bar"), | |
| "ami": cty.NullVal(cty.String), | |
| "network_interface": cty.ListValEmpty(cty.Object(map[string]cty.Type{ | |
| "device_index": cty.String, | |
| "description": cty.String, | |
| })), | |
| }) | |
| if !expected.RawEquals(actual) { | |
| t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) | |
| } | |
| } | |
| func TestPlan_validate(t *testing.T) { | |
| // This is triggered by not asking for input so we have to set this to false | |
| test = false | |
| defer func() { test = true }() | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-invalid"), td) | |
| t.Chdir(td) | |
| p := testProvider() | |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { | |
| return providers.PlanResourceChangeResponse{ | |
| PlannedState: req.ProposedNewState, | |
| } | |
| } | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{"-no-color"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| actual := output.Stderr() | |
| if want := "Error: Invalid count argument"; !strings.Contains(actual, want) { | |
| t.Fatalf("unexpected error output\ngot:\n%s\n\nshould contain: %s", actual, want) | |
| } | |
| if want := "9: count = timestamp()"; !strings.Contains(actual, want) { | |
| t.Fatalf("unexpected error output\ngot:\n%s\n\nshould contain: %s", actual, want) | |
| } | |
| } | |
| func TestPlan_vars(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-vars"), td) | |
| t.Chdir(td) | |
| p := planVarsFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| actual := "" | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { | |
| actual = req.ProposedNewState.GetAttr("value").AsString() | |
| resp.PlannedState = req.ProposedNewState | |
| return | |
| } | |
| args := []string{ | |
| "-var", "foo=bar", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| if actual != "bar" { | |
| t.Fatal("didn't work") | |
| } | |
| } | |
| func TestPlan_varsInvalid(t *testing.T) { | |
| testCases := []struct { | |
| args []string | |
| wantErr string | |
| }{ | |
| { | |
| []string{"-var", "foo"}, | |
| `The given -var option "foo" is not correctly specified.`, | |
| }, | |
| { | |
| []string{"-var", "foo = bar"}, | |
| `Variable name "foo " is invalid due to trailing space.`, | |
| }, | |
| } | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-vars"), td) | |
| t.Chdir(td) | |
| for _, tc := range testCases { | |
| t.Run(strings.Join(tc.args, " "), func(t *testing.T) { | |
| p := planVarsFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| code := c.Run(tc.args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) | |
| } | |
| got := output.Stderr() | |
| if !strings.Contains(got, tc.wantErr) { | |
| t.Fatalf("bad error output, want %q, got:\n%s", tc.wantErr, got) | |
| } | |
| }) | |
| } | |
| } | |
| func TestPlan_varsUnset(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-vars"), td) | |
| t.Chdir(td) | |
| // The plan command will prompt for interactive input of var.foo. | |
| // We'll answer "bar" to that prompt, which should then allow this | |
| // configuration to apply even though var.foo doesn't have a | |
| // default value and there are no -var arguments on our command line. | |
| // This will (helpfully) panic if more than one variable is requested during plan: | |
| // https://github.com/hashicorp/terraform/issues/26027 | |
| close := testInteractiveInput(t, []string{"bar"}) | |
| defer close() | |
| p := planVarsFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| } | |
| // This test adds a required argument to the test provider to validate | |
| // processing of user input: | |
| // https://github.com/hashicorp/terraform/issues/26035 | |
| func TestPlan_providerArgumentUnset(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| // Disable test mode so input would be asked | |
| test = false | |
| defer func() { test = true }() | |
| // The plan command will prompt for interactive input of provider.test.region | |
| defaultInputReader = bytes.NewBufferString("us-east-1\n") | |
| p := planFixtureProvider() | |
| // override the planFixtureProvider schema to include a required provider argument | |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| Provider: providers.Schema{ | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "region": {Type: cty.String, Required: true}, | |
| }, | |
| }, | |
| }, | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "ami": {Type: cty.String, Optional: true, Computed: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "network_interface": { | |
| Nesting: configschema.NestingList, | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "device_index": {Type: cty.String, Optional: true}, | |
| "description": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| DataSources: map[string]providers.Schema{ | |
| "test_data_source": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": { | |
| Type: cty.String, | |
| Required: true, | |
| }, | |
| "valid": { | |
| Type: cty.Bool, | |
| Computed: true, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| } | |
| // Test that terraform properly merges provider configuration that's split | |
| // between config files and interactive input variables. | |
| // https://github.com/hashicorp/terraform/issues/28956 | |
| func TestPlan_providerConfigMerge(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-provider-input"), td) | |
| t.Chdir(td) | |
| // Disable test mode so input would be asked | |
| test = false | |
| defer func() { test = true }() | |
| // The plan command will prompt for interactive input of provider.test.region | |
| defaultInputReader = bytes.NewBufferString("us-east-1\n") | |
| p := planFixtureProvider() | |
| // override the planFixtureProvider schema to include a required provider argument and a nested block | |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| Provider: providers.Schema{ | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "region": {Type: cty.String, Required: true}, | |
| "url": {Type: cty.String, Required: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "auth": { | |
| Nesting: configschema.NestingList, | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "user": {Type: cty.String, Required: true}, | |
| "password": {Type: cty.String, Required: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| if !p.ConfigureProviderCalled { | |
| t.Fatal("configure provider not called") | |
| } | |
| // For this test, we want to confirm that we've sent the expected config | |
| // value *to* the provider. | |
| got := p.ConfigureProviderRequest.Config | |
| want := cty.ObjectVal(map[string]cty.Value{ | |
| "auth": cty.ListVal([]cty.Value{ | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("one"), | |
| "password": cty.StringVal("onepw"), | |
| }), | |
| cty.ObjectVal(map[string]cty.Value{ | |
| "user": cty.StringVal("two"), | |
| "password": cty.StringVal("twopw"), | |
| }), | |
| }), | |
| "region": cty.StringVal("us-east-1"), | |
| "url": cty.StringVal("example.com"), | |
| }) | |
| if !got.RawEquals(want) { | |
| t.Fatal("wrong provider config") | |
| } | |
| } | |
| func TestPlan_varFile(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-vars"), td) | |
| t.Chdir(td) | |
| varFilePath := testTempFile(t) | |
| if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| p := planVarsFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| actual := "" | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { | |
| actual = req.ProposedNewState.GetAttr("value").AsString() | |
| resp.PlannedState = req.ProposedNewState | |
| return | |
| } | |
| args := []string{ | |
| "-var-file", varFilePath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| if actual != "bar" { | |
| t.Fatal("didn't work") | |
| } | |
| } | |
| func TestPlan_varFileDefault(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-vars"), td) | |
| t.Chdir(td) | |
| varFilePath := filepath.Join(td, "terraform.tfvars") | |
| if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| p := planVarsFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| actual := "" | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { | |
| actual = req.ProposedNewState.GetAttr("value").AsString() | |
| resp.PlannedState = req.ProposedNewState | |
| return | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| if actual != "bar" { | |
| t.Fatal("didn't work") | |
| } | |
| } | |
| func TestPlan_varFileWithDecls(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-vars"), td) | |
| t.Chdir(td) | |
| varFilePath := testTempFile(t) | |
| if err := ioutil.WriteFile(varFilePath, []byte(planVarFileWithDecl), 0644); err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| p := planVarsFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-var-file", varFilePath, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code == 0 { | |
| t.Fatalf("succeeded; want failure\n\n%s", output.Stdout()) | |
| } | |
| msg := output.Stderr() | |
| if got, want := msg, "Variable declaration in .tfvars file"; !strings.Contains(got, want) { | |
| t.Fatalf("missing expected error message\nwant message containing %q\ngot:\n%s", want, got) | |
| } | |
| } | |
| // TestPlan_varFileDuplicateAttr is a regression test for a bug where a | |
| // -var-file containing a duplicated attribute would print an error diagnostic | |
| // but still exit 0, silently discarding the file and falling back to other | |
| // variable sources (here, the variable's default value). A malformed var file | |
| // must cause the plan to fail. | |
| func TestPlan_varFileDuplicateAttr(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-duplicate-var-file"), td) | |
| t.Chdir(td) | |
| p := planVarsFixtureProvider() | |
| planned := false | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { | |
| planned = true | |
| resp.PlannedState = req.ProposedNewState | |
| return | |
| } | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-var-file", "duplicate.tfvars", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code == 0 { | |
| t.Fatalf("succeeded; want failure with a non-zero exit code\n\n%s", output.Stdout()) | |
| } | |
| if got, want := output.Stderr(), "Attribute redefined"; !strings.Contains(got, want) { | |
| t.Fatalf("missing expected error message\nwant message containing %q\ngot:\n%s", want, got) | |
| } | |
| if planned { | |
| t.Fatal("plan proceeded using the variable's default value; the malformed var file should have aborted the operation") | |
| } | |
| } | |
| func TestPlan_detailedExitcode(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| t.Run("return 1", func(t *testing.T) { | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| // Running plan without setting testingOverrides is similar to plan without init | |
| View: view, | |
| }, | |
| } | |
| code := c.Run([]string{"-detailed-exitcode"}) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| }) | |
| t.Run("return 2", func(t *testing.T) { | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| code := c.Run([]string{"-detailed-exitcode"}) | |
| output := done(t) | |
| if code != 2 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| }) | |
| } | |
| func TestPlan_detailedExitcode_emptyDiff(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-emptydiff"), td) | |
| t.Chdir(td) | |
| p := testProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{"-detailed-exitcode"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| } | |
| func TestPlan_shutdown(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("apply-shutdown"), td) | |
| t.Chdir(td) | |
| cancelled := make(chan struct{}) | |
| shutdownCh := make(chan struct{}) | |
| p := testProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| ShutdownCh: shutdownCh, | |
| }, | |
| } | |
| p.StopFn = func() error { | |
| close(cancelled) | |
| return nil | |
| } | |
| var once sync.Once | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { | |
| once.Do(func() { | |
| shutdownCh <- struct{}{} | |
| }) | |
| // Because of the internal lock in the MockProvider, we can't | |
| // coordinate directly with the calling of Stop, and making the | |
| // MockProvider concurrent is disruptive to a lot of existing tests. | |
| // Wait here a moment to help make sure the main goroutine gets to the | |
| // Stop call before we exit, or the plan may finish before it can be | |
| // canceled. | |
| time.Sleep(200 * time.Millisecond) | |
| s := req.ProposedNewState.AsValueMap() | |
| s["ami"] = cty.StringVal("bar") | |
| resp.PlannedState = cty.ObjectVal(s) | |
| return | |
| } | |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "ami": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| code := c.Run([]string{}) | |
| output := done(t) | |
| if code != 1 { | |
| t.Errorf("wrong exit code %d; want 1\noutput:\n%s", code, output.Stdout()) | |
| } | |
| select { | |
| case <-cancelled: | |
| default: | |
| t.Error("command not cancelled") | |
| } | |
| } | |
| func TestPlan_init_required(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| // Running plan without setting testingOverrides is similar to plan without init | |
| View: view, | |
| }, | |
| } | |
| args := []string{"-no-color"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("expected error, got success") | |
| } | |
| got := output.Stderr() | |
| if !(strings.Contains(got, "terraform init") && strings.Contains(got, "provider registry.terraform.io/hashicorp/test: required by this configuration but no version is selected")) { | |
| t.Fatal("wrong error message in output:", got) | |
| } | |
| } | |
| // Config with multiple resources, targeting plan of a subset | |
| func TestPlan_targeted(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("apply-targeted"), td) | |
| t.Chdir(td) | |
| p := testProvider() | |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Computed: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { | |
| return providers.PlanResourceChangeResponse{ | |
| PlannedState: req.ProposedNewState, | |
| } | |
| } | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-target", "test_instance.foo", | |
| "-target", "test_instance.baz", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| if got, want := output.Stdout(), "3 to add, 0 to change, 0 to destroy"; !strings.Contains(got, want) { | |
| t.Fatalf("bad change summary, want %q, got:\n%s", want, got) | |
| } | |
| } | |
| // Diagnostics for invalid -target flags | |
| func TestPlan_targetFlagsDiags(t *testing.T) { | |
| testCases := map[string]string{ | |
| "test_instance.": "Dot must be followed by attribute name.", | |
| "test_instance": "Resource specification must include a resource type and name.", | |
| } | |
| for target, wantDiag := range testCases { | |
| t.Run(target, func(t *testing.T) { | |
| td := testTempDir(t) | |
| defer os.RemoveAll(td) | |
| t.Chdir(td) | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-target", target, | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 1 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stdout()) | |
| } | |
| got := output.Stderr() | |
| if !strings.Contains(got, target) { | |
| t.Fatalf("bad error output, want %q, got:\n%s", target, got) | |
| } | |
| if !strings.Contains(got, wantDiag) { | |
| t.Fatalf("bad error output, want %q, got:\n%s", wantDiag, got) | |
| } | |
| }) | |
| } | |
| } | |
| func TestPlan_replace(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan-replace"), td) | |
| t.Chdir(td) | |
| originalState := states.BuildState(func(s *states.SyncState) { | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "a", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"hello"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("test"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| }) | |
| statePath := testStateFile(t, originalState) | |
| p := testProvider() | |
| p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Computed: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { | |
| return providers.PlanResourceChangeResponse{ | |
| PlannedState: req.ProposedNewState, | |
| } | |
| } | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-state", statePath, | |
| "-no-color", | |
| "-replace", "test_instance.a", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("wrong exit code %d\n\n%s", code, output.Stderr()) | |
| } | |
| stdout := output.Stdout() | |
| if got, want := stdout, "1 to add, 0 to change, 1 to destroy"; !strings.Contains(got, want) { | |
| t.Errorf("wrong plan summary\ngot output:\n%s\n\nwant substring: %s", got, want) | |
| } | |
| if got, want := stdout, "test_instance.a will be replaced, as requested"; !strings.Contains(got, want) { | |
| t.Errorf("missing replace explanation\ngot output:\n%s\n\nwant substring: %s", got, want) | |
| } | |
| } | |
| // Verify that the parallelism flag allows no more than the desired number of | |
| // concurrent calls to PlanResourceChange. | |
| func TestPlan_parallelism(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("parallelism"), td) | |
| t.Chdir(td) | |
| par := 4 | |
| // started is a semaphore that we use to ensure that we never have more | |
| // than "par" plan operations happening concurrently | |
| started := make(chan struct{}, par) | |
| // beginCtx is used as a starting gate to hold back PlanResourceChange | |
| // calls until we reach the desired concurrency. The cancel func "begin" is | |
| // called once we reach the desired concurrency, allowing all apply calls | |
| // to proceed in unison. | |
| beginCtx, begin := context.WithCancel(context.Background()) | |
| // This just makes go vet happy, in reality the function will never exit if | |
| // begin() isn't called inside ApplyResourceChangeFn. | |
| defer begin() | |
| // Since our mock provider has its own mutex preventing concurrent calls | |
| // to ApplyResourceChange, we need to use a number of separate providers | |
| // here. They will all have the same mock implementation function assigned | |
| // but crucially they will each have their own mutex. | |
| providerFactories := map[addrs.Provider]providers.Factory{} | |
| for i := 0; i < 10; i++ { | |
| name := fmt.Sprintf("test%d", i) | |
| provider := &testing_provider.MockProvider{} | |
| provider.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| name + "_instance": {Body: &configschema.Block{}}, | |
| }, | |
| } | |
| provider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { | |
| // If we ever have more than our intended parallelism number of | |
| // plan operations running concurrently, the semaphore will fail. | |
| select { | |
| case started <- struct{}{}: | |
| defer func() { | |
| <-started | |
| }() | |
| default: | |
| t.Fatal("too many concurrent apply operations") | |
| } | |
| // If we never reach our intended parallelism, the context will | |
| // never be canceled and the test will time out. | |
| if len(started) >= par { | |
| begin() | |
| } | |
| <-beginCtx.Done() | |
| // do some "work" | |
| // Not required for correctness, but makes it easier to spot a | |
| // failure when there is more overlap. | |
| time.Sleep(10 * time.Millisecond) | |
| return providers.PlanResourceChangeResponse{ | |
| PlannedState: req.ProposedNewState, | |
| } | |
| } | |
| providerFactories[addrs.NewDefaultProvider(name)] = providers.FactoryFixed(provider) | |
| } | |
| testingOverrides := &testingOverrides{ | |
| Providers: providerFactories, | |
| } | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: testingOverrides, | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| fmt.Sprintf("-parallelism=%d", par), | |
| } | |
| res := c.Run(args) | |
| output := done(t) | |
| if res != 0 { | |
| t.Fatal(output.Stdout()) | |
| } | |
| } | |
| func TestPlan_warnings(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| t.Run("full warnings", func(t *testing.T) { | |
| p := planWarningsFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| code := c.Run([]string{}) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| // the output should contain 3 warnings (returned by planWarningsFixtureProvider()) | |
| wantWarnings := []string{ | |
| "warning 1", | |
| "warning 2", | |
| "warning 3", | |
| } | |
| for _, want := range wantWarnings { | |
| if !strings.Contains(output.Stdout(), want) { | |
| t.Errorf("missing warning %s", want) | |
| } | |
| } | |
| }) | |
| t.Run("compact warnings", func(t *testing.T) { | |
| p := planWarningsFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| code := c.Run([]string{"-compact-warnings"}) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| // the output should contain 3 warnings (returned by planWarningsFixtureProvider()) | |
| // and the message that plan was run with -compact-warnings | |
| wantWarnings := []string{ | |
| "warning 1", | |
| "warning 2", | |
| "warning 3", | |
| "To see the full warning notes, run Terraform without -compact-warnings.", | |
| } | |
| for _, want := range wantWarnings { | |
| if !strings.Contains(output.Stdout(), want) { | |
| t.Errorf("missing warning %s", want) | |
| } | |
| } | |
| }) | |
| } | |
| func TestPlan_jsonGoldenReference(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("plan"), td) | |
| t.Chdir(td) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{ | |
| "-json", | |
| } | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| checkGoldenReference(t, output, "plan") | |
| } | |
| // Tests related to how plan command behaves when there are query files in the configuration path | |
| func TestPlan_QueryFiles(t *testing.T) { | |
| // a plan succeeds regardless of valid or invalid | |
| // tfquery files in the configuration path | |
| t.Run("with invalid query files in the config path", func(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("query/invalid-syntax"), td) | |
| t.Chdir(td) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| }) | |
| // the duplicate in the query should not matter because query files are not processed | |
| t.Run("with duplicate variables across query and plan file", func(t *testing.T) { | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("query/duplicate-variables"), td) | |
| t.Chdir(td) | |
| p := planFixtureProvider() | |
| view, done := testView(t) | |
| c := &PlanCommand{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(p), | |
| View: view, | |
| }, | |
| } | |
| args := []string{"-var", "instance_name=foo"} | |
| code := c.Run(args) | |
| output := done(t) | |
| if code != 0 { | |
| t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) | |
| } | |
| }) | |
| } | |
| // planFixtureSchema returns a schema suitable for processing the | |
| // configuration in testdata/plan . This schema should be | |
| // assigned to a mock provider named "test". | |
| func planFixtureSchema() *providers.GetProviderSchemaResponse { | |
| return &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "ami": {Type: cty.String, Optional: true}, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "network_interface": { | |
| Nesting: configschema.NestingList, | |
| Block: configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "device_index": {Type: cty.String, Optional: true}, | |
| "description": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| DataSources: map[string]providers.Schema{ | |
| "test_data_source": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": { | |
| Type: cty.String, | |
| Required: true, | |
| }, | |
| "valid": { | |
| Type: cty.Bool, | |
| Computed: true, | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| } | |
| // planFixtureProvider returns a mock provider that is configured for basic | |
| // operation with the configuration in testdata/plan. This mock has | |
| // GetSchemaResponse and PlanResourceChangeFn populated, with the plan | |
| // step just passing through the new object proposed by Terraform Core. | |
| func planFixtureProvider() *testing_provider.MockProvider { | |
| p := testProvider() | |
| p.GetProviderSchemaResponse = planFixtureSchema() | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { | |
| return providers.PlanResourceChangeResponse{ | |
| PlannedState: req.ProposedNewState, | |
| } | |
| } | |
| p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { | |
| return providers.ReadDataSourceResponse{ | |
| State: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("zzzzz"), | |
| "valid": cty.BoolVal(true), | |
| }), | |
| } | |
| } | |
| return p | |
| } | |
| // planVarsFixtureSchema returns a schema suitable for processing the | |
| // configuration in testdata/plan-vars . This schema should be | |
| // assigned to a mock provider named "test". | |
| func planVarsFixtureSchema() *providers.GetProviderSchemaResponse { | |
| return &providers.GetProviderSchemaResponse{ | |
| ResourceTypes: map[string]providers.Schema{ | |
| "test_instance": { | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "id": {Type: cty.String, Optional: true, Computed: true}, | |
| "value": {Type: cty.String, Optional: true}, | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| } | |
| // planVarsFixtureProvider returns a mock provider that is configured for basic | |
| // operation with the configuration in testdata/plan-vars. This mock has | |
| // GetSchemaResponse and PlanResourceChangeFn populated, with the plan | |
| // step just passing through the new object proposed by Terraform Core. | |
| func planVarsFixtureProvider() *testing_provider.MockProvider { | |
| p := testProvider() | |
| p.GetProviderSchemaResponse = planVarsFixtureSchema() | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { | |
| return providers.PlanResourceChangeResponse{ | |
| PlannedState: req.ProposedNewState, | |
| } | |
| } | |
| p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { | |
| return providers.ReadDataSourceResponse{ | |
| State: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("zzzzz"), | |
| "valid": cty.BoolVal(true), | |
| }), | |
| } | |
| } | |
| return p | |
| } | |
| // planFixtureProvider returns a mock provider that is configured for basic | |
| // operation with the configuration in testdata/plan. This mock has | |
| // GetSchemaResponse and PlanResourceChangeFn populated, returning 3 warnings. | |
| func planWarningsFixtureProvider() *testing_provider.MockProvider { | |
| p := testProvider() | |
| p.GetProviderSchemaResponse = planFixtureSchema() | |
| p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { | |
| return providers.PlanResourceChangeResponse{ | |
| Diagnostics: tfdiags.Diagnostics{ | |
| tfdiags.SimpleWarning("warning 1"), | |
| tfdiags.SimpleWarning("warning 2"), | |
| tfdiags.SimpleWarning("warning 3"), | |
| }, | |
| PlannedState: req.ProposedNewState, | |
| } | |
| } | |
| p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { | |
| return providers.ReadDataSourceResponse{ | |
| State: cty.ObjectVal(map[string]cty.Value{ | |
| "id": cty.StringVal("zzzzz"), | |
| "valid": cty.BoolVal(true), | |
| }), | |
| } | |
| } | |
| return p | |
| } | |
| const planVarFile = ` | |
| foo = "bar" | |
| ` | |
| const planVarFileWithDecl = ` | |
| foo = "bar" | |
| variable "nope" { | |
| } | |
| ` |