| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package command |
| |
| import ( |
| "bytes" |
| "path/filepath" |
| "strings" |
| "testing" |
| |
| "github.com/mitchellh/cli" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/states" |
| ) |
| |
| 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 := new(cli.MockUi) |
| 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 := new(cli.MockUi) |
| 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 := new(cli.MockUi) |
| 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 := new(cli.MockUi) |
| 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 := new(cli.MockUi) |
| 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(), "Error parsing 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 := new(cli.MockUi) |
| 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 := new(cli.MockUi) |
| 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_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) |
| defer testChdir(t, 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 := new(cli.MockUi) |
| 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 |
| ` |