| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package command | |
| import ( | |
| "bytes" | |
| "path/filepath" | |
| "strings" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/backend" | |
| backendInit "github.com/hashicorp/terraform/internal/backend/init" | |
| "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" | |
| ) | |
| func TestStateReplaceProvider(t *testing.T) { | |
| state := states.BuildState(func(s *states.SyncState) { | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "aws_instance", | |
| Name: "alpha", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"alpha","foo":"value","bar":"value"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("aws"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "aws_instance", | |
| Name: "beta", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"beta","foo":"value","bar":"value"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("aws"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "azurerm_virtual_machine", | |
| Name: "gamma", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"gamma","baz":"value"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewLegacyProvider("azurerm"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| }) | |
| t.Run("happy path", func(t *testing.T) { | |
| statePath := testStateFile(t, state) | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| Ui: ui, | |
| View: view, | |
| }, | |
| }, | |
| } | |
| inputBuf := &bytes.Buffer{} | |
| ui.InputReader = inputBuf | |
| inputBuf.WriteString("yes\n") | |
| args := []string{ | |
| "-state", statePath, | |
| "hashicorp/aws", | |
| "acmecorp/aws", | |
| } | |
| if code := c.Run(args); code != 0 { | |
| t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String()) | |
| } | |
| testStateOutput(t, statePath, testStateReplaceProviderOutput) | |
| backups := testStateBackups(t, filepath.Dir(statePath)) | |
| if len(backups) != 1 { | |
| t.Fatalf("unexpected backups: %#v", backups) | |
| } | |
| testStateOutput(t, backups[0], testStateReplaceProviderOutputOriginal) | |
| }) | |
| t.Run("auto approve", func(t *testing.T) { | |
| statePath := testStateFile(t, state) | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| Ui: ui, | |
| View: view, | |
| }, | |
| }, | |
| } | |
| inputBuf := &bytes.Buffer{} | |
| ui.InputReader = inputBuf | |
| args := []string{ | |
| "-state", statePath, | |
| "-auto-approve", | |
| "hashicorp/aws", | |
| "acmecorp/aws", | |
| } | |
| if code := c.Run(args); code != 0 { | |
| t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String()) | |
| } | |
| testStateOutput(t, statePath, testStateReplaceProviderOutput) | |
| backups := testStateBackups(t, filepath.Dir(statePath)) | |
| if len(backups) != 1 { | |
| t.Fatalf("unexpected backups: %#v", backups) | |
| } | |
| testStateOutput(t, backups[0], testStateReplaceProviderOutputOriginal) | |
| }) | |
| t.Run("cancel at approval step", func(t *testing.T) { | |
| statePath := testStateFile(t, state) | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| Ui: ui, | |
| View: view, | |
| }, | |
| }, | |
| } | |
| inputBuf := &bytes.Buffer{} | |
| ui.InputReader = inputBuf | |
| inputBuf.WriteString("no\n") | |
| args := []string{ | |
| "-state", statePath, | |
| "hashicorp/aws", | |
| "acmecorp/aws", | |
| } | |
| if code := c.Run(args); code != 0 { | |
| t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String()) | |
| } | |
| testStateOutput(t, statePath, testStateReplaceProviderOutputOriginal) | |
| backups := testStateBackups(t, filepath.Dir(statePath)) | |
| if len(backups) != 0 { | |
| t.Fatalf("unexpected backups: %#v", backups) | |
| } | |
| }) | |
| t.Run("no matching provider found", func(t *testing.T) { | |
| statePath := testStateFile(t, state) | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| Ui: ui, | |
| View: view, | |
| }, | |
| }, | |
| } | |
| args := []string{ | |
| "-state", statePath, | |
| "hashicorp/google", | |
| "acmecorp/google", | |
| } | |
| if code := c.Run(args); code != 0 { | |
| t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String()) | |
| } | |
| testStateOutput(t, statePath, testStateReplaceProviderOutputOriginal) | |
| backups := testStateBackups(t, filepath.Dir(statePath)) | |
| if len(backups) != 0 { | |
| t.Fatalf("unexpected backups: %#v", backups) | |
| } | |
| }) | |
| t.Run("invalid flags", func(t *testing.T) { | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| Ui: ui, | |
| View: view, | |
| }, | |
| }, | |
| } | |
| args := []string{ | |
| "-invalid", | |
| "hashicorp/google", | |
| "acmecorp/google", | |
| } | |
| if code := c.Run(args); code == 0 { | |
| t.Fatalf("successful exit; want error") | |
| } | |
| if got, want := ui.ErrorWriter.String(), "Failed to parse command-line flags"; !strings.Contains(got, want) { | |
| t.Fatalf("missing expected error message\nwant: %s\nfull output:\n%s", want, got) | |
| } | |
| }) | |
| t.Run("wrong number of arguments", func(t *testing.T) { | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| Ui: ui, | |
| View: view, | |
| }, | |
| }, | |
| } | |
| args := []string{"a", "b", "c", "d"} | |
| if code := c.Run(args); code == 0 { | |
| t.Fatalf("successful exit; want error") | |
| } | |
| if got, want := ui.ErrorWriter.String(), "Exactly two arguments expected"; !strings.Contains(got, want) { | |
| t.Fatalf("missing expected error message\nwant: %s\nfull output:\n%s", want, got) | |
| } | |
| }) | |
| t.Run("invalid provider strings", func(t *testing.T) { | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| Ui: ui, | |
| View: view, | |
| }, | |
| }, | |
| } | |
| args := []string{ | |
| "hashicorp/google_cloud", | |
| "-/-/google", | |
| } | |
| if code := c.Run(args); code == 0 { | |
| t.Fatalf("successful exit; want error") | |
| } | |
| got := ui.ErrorWriter.String() | |
| msgs := []string{ | |
| `Invalid "from" provider "hashicorp/google_cloud"`, | |
| "Invalid provider type", | |
| `Invalid "to" provider "-/-/google"`, | |
| "Invalid provider source hostname", | |
| } | |
| for _, msg := range msgs { | |
| if !strings.Contains(got, msg) { | |
| t.Errorf("missing expected error message\nwant: %s\nfull output:\n%s", msg, got) | |
| } | |
| } | |
| }) | |
| } | |
| func TestStateReplaceProvider_stateStore(t *testing.T) { | |
| // Create a temporary working directory | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("state-store-unchanged/provider-managed-by-terraform"), td) | |
| t.Chdir(td) | |
| // Get bytes describing a state containing resources | |
| state := 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":"foo","foo":"value","bar":"value"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("test"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "test_instance", | |
| Name: "baz", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"baz","foo":"value","bar":"value"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("test"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| }) | |
| var stateBuf bytes.Buffer | |
| if err := statefile.Write(statefile.New(state, "", 1), &stateBuf); err != nil { | |
| t.Fatalf("error during test setup: %s", err) | |
| } | |
| // Create a mock that contains a persisted "default" state that uses the bytes from above. | |
| mockProvider := mockPluggableStateStorageProvider(mockSingleStateStoreSchema("test_store")) | |
| mockProvider.MockStates = testing_provider.NewMockStateBytesWithSingleState( | |
| "test_store", | |
| "default", | |
| stateBuf.Bytes(), | |
| ) | |
| mockProviderAddress := addrs.NewDefaultProvider("test") | |
| ui := testUiWrapped(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| AllowExperimentalFeatures: true, | |
| testingOverrides: &testingOverrides{ | |
| Providers: map[addrs.Provider]providers.Factory{ | |
| mockProviderAddress: providers.FactoryFixed(mockProvider), | |
| }, | |
| }, | |
| Ui: ui, | |
| }, | |
| }, | |
| } | |
| inputBuf := &bytes.Buffer{} | |
| ui.InputReader = inputBuf | |
| inputBuf.WriteString("yes\n") | |
| args := []string{ | |
| "hashicorp/test", | |
| "testing-corp/test", | |
| } | |
| if code := c.Run(args); code != 0 { | |
| t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String()) | |
| } | |
| // For the two resources in the mocked state, we expect them both to be changed. | |
| expectedOutputMsgs := []string{ | |
| "- registry.terraform.io/hashicorp/test\n + registry.terraform.io/testing-corp/test\n", | |
| "Successfully replaced provider for 2 resources.", | |
| } | |
| for _, msg := range expectedOutputMsgs { | |
| if !strings.Contains(ui.OutputWriter.String(), msg) { | |
| t.Fatalf("expected command output to include %q but it's not present in the output:\nOutput = %s", | |
| msg, ui.OutputWriter.String()) | |
| } | |
| } | |
| } | |
| func TestStateReplaceProvider_constVariable(t *testing.T) { | |
| t.Run("missing value", func(t *testing.T) { | |
| wd := tempWorkingDirFixture(t, "dynamic-module-sources/command-with-const-var") | |
| t.Chdir(wd.RootModuleDir()) | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| Ui: ui, | |
| View: view, | |
| WorkingDir: wd, | |
| }, | |
| }, | |
| } | |
| args := []string{ | |
| "-auto-approve", | |
| "hashicorp/test", | |
| "testing-corp/test", | |
| } | |
| if code := c.Run(args); code == 0 { | |
| t.Fatalf("expected error, got 0") | |
| } | |
| errStr := ui.ErrorWriter.String() | |
| if !strings.Contains(errStr, "No value for required variable") { | |
| t.Fatalf("expected missing variable error, got: %s", errStr) | |
| } | |
| }) | |
| t.Run("value via cli", func(t *testing.T) { | |
| wd := tempWorkingDirFixture(t, "dynamic-module-sources/command-with-const-var") | |
| t.Chdir(wd.RootModuleDir()) | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| Ui: ui, | |
| View: view, | |
| WorkingDir: wd, | |
| }, | |
| }, | |
| } | |
| args := []string{ | |
| "-auto-approve", | |
| "-var", "module_name=child", | |
| "hashicorp/test", | |
| "testing-corp/test", | |
| } | |
| if code := c.Run(args); code != 0 { | |
| t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String()) | |
| } | |
| actual := strings.TrimSpace(testStateRead(t, "terraform.tfstate").String()) | |
| expected := strings.TrimSpace(` | |
| <no state> | |
| module.child: | |
| test_instance.test: | |
| ID = | |
| provider = provider["registry.terraform.io/testing-corp/test"] | |
| `) | |
| if diff := cmp.Diff(expected, actual); diff != "" { | |
| t.Fatalf("unexpected state output\n%s", diff) | |
| } | |
| }) | |
| t.Run("value via backend", func(t *testing.T) { | |
| mockBackend := TestNewVariableBackend(map[string]string{ | |
| "module_name": "child", | |
| }) | |
| backendInit.Set("local-vars", func() backend.Backend { return mockBackend }) | |
| defer backendInit.Set("local-vars", nil) | |
| wd := tempWorkingDirFixture(t, "dynamic-module-sources/command-with-const-var-backend") | |
| t.Chdir(wd.RootModuleDir()) | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| testingOverrides: metaOverridesForProvider(testProvider()), | |
| Ui: ui, | |
| View: view, | |
| WorkingDir: wd, | |
| }, | |
| }, | |
| } | |
| args := []string{ | |
| "-auto-approve", | |
| "hashicorp/test", | |
| "testing-corp/test", | |
| } | |
| if code := c.Run(args); code != 0 { | |
| t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String()) | |
| } | |
| actual := strings.TrimSpace(testStateRead(t, "terraform.tfstate").String()) | |
| expected := strings.TrimSpace(` | |
| <no state> | |
| module.child: | |
| test_instance.test: | |
| ID = | |
| provider = provider["registry.terraform.io/testing-corp/test"] | |
| `) | |
| if diff := cmp.Diff(expected, actual); diff != "" { | |
| t.Fatalf("unexpected state output\n%s", diff) | |
| } | |
| }) | |
| } | |
| func TestStateReplaceProvider_docs(t *testing.T) { | |
| c := &StateReplaceProviderCommand{} | |
| if got, want := c.Help(), "Usage: terraform [global options] state replace-provider"; !strings.Contains(got, want) { | |
| t.Fatalf("unexpected help text\nwant: %s\nfull output:\n%s", want, got) | |
| } | |
| if got, want := c.Synopsis(), "Replace provider in the state"; got != want { | |
| t.Fatalf("unexpected synopsis\nwant: %s\nfull output:\n%s", want, got) | |
| } | |
| } | |
| func TestStateReplaceProvider_checkRequiredVersion(t *testing.T) { | |
| // Create a temporary working directory that is empty | |
| td := t.TempDir() | |
| testCopyDir(t, testFixturePath("command-check-required-version"), td) | |
| t.Chdir(td) | |
| state := states.BuildState(func(s *states.SyncState) { | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "aws_instance", | |
| Name: "alpha", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"alpha","foo":"value","bar":"value"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("aws"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "aws_instance", | |
| Name: "beta", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"beta","foo":"value","bar":"value"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewDefaultProvider("aws"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| s.SetResourceInstanceCurrent( | |
| addrs.Resource{ | |
| Mode: addrs.ManagedResourceMode, | |
| Type: "azurerm_virtual_machine", | |
| Name: "gamma", | |
| }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), | |
| &states.ResourceInstanceObjectSrc{ | |
| AttrsJSON: []byte(`{"id":"gamma","baz":"value"}`), | |
| Status: states.ObjectReady, | |
| }, | |
| addrs.AbsProviderConfig{ | |
| Provider: addrs.NewLegacyProvider("azurerm"), | |
| Module: addrs.RootModule, | |
| }, | |
| ) | |
| }) | |
| statePath := testStateFile(t, state) | |
| ui := testUiWrapped(t) | |
| view, _ := testView(t) | |
| c := &StateReplaceProviderCommand{ | |
| StateMeta{ | |
| Meta: Meta{ | |
| Ui: ui, | |
| View: view, | |
| }, | |
| }, | |
| } | |
| inputBuf := &bytes.Buffer{} | |
| ui.InputReader = inputBuf | |
| inputBuf.WriteString("yes\n") | |
| args := []string{ | |
| "-state", statePath, | |
| "hashicorp/aws", | |
| "acmecorp/aws", | |
| } | |
| if code := c.Run(args); code != 1 { | |
| t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) | |
| } | |
| // State is unchanged | |
| testStateOutput(t, statePath, testStateReplaceProviderOutputOriginal) | |
| // Required version diags are correct | |
| errStr := ui.ErrorWriter.String() | |
| if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { | |
| t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) | |
| } | |
| if strings.Contains(errStr, `required_version = ">= 0.13.0"`) { | |
| t.Fatalf("output should not point to met version constraint, but is:\n\n%s", errStr) | |
| } | |
| } | |
| const testStateReplaceProviderOutputOriginal = ` | |
| aws_instance.alpha: | |
| ID = alpha | |
| provider = provider["registry.terraform.io/hashicorp/aws"] | |
| bar = value | |
| foo = value | |
| aws_instance.beta: | |
| ID = beta | |
| provider = provider["registry.terraform.io/hashicorp/aws"] | |
| bar = value | |
| foo = value | |
| azurerm_virtual_machine.gamma: | |
| ID = gamma | |
| provider = provider["registry.terraform.io/-/azurerm"] | |
| baz = value | |
| ` | |
| const testStateReplaceProviderOutput = ` | |
| aws_instance.alpha: | |
| ID = alpha | |
| provider = provider["registry.terraform.io/acmecorp/aws"] | |
| bar = value | |
| foo = value | |
| aws_instance.beta: | |
| ID = beta | |
| provider = provider["registry.terraform.io/acmecorp/aws"] | |
| bar = value | |
| foo = value | |
| azurerm_virtual_machine.gamma: | |
| ID = gamma | |
| provider = provider["registry.terraform.io/-/azurerm"] | |
| baz = value | |
| ` |