github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/state_replace_provider_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"bytes"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/mitchellh/cli"
    13  
    14  	"github.com/terramate-io/tf/addrs"
    15  	"github.com/terramate-io/tf/states"
    16  )
    17  
    18  func TestStateReplaceProvider(t *testing.T) {
    19  	state := states.BuildState(func(s *states.SyncState) {
    20  		s.SetResourceInstanceCurrent(
    21  			addrs.Resource{
    22  				Mode: addrs.ManagedResourceMode,
    23  				Type: "aws_instance",
    24  				Name: "alpha",
    25  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
    26  			&states.ResourceInstanceObjectSrc{
    27  				AttrsJSON: []byte(`{"id":"alpha","foo":"value","bar":"value"}`),
    28  				Status:    states.ObjectReady,
    29  			},
    30  			addrs.AbsProviderConfig{
    31  				Provider: addrs.NewDefaultProvider("aws"),
    32  				Module:   addrs.RootModule,
    33  			},
    34  		)
    35  		s.SetResourceInstanceCurrent(
    36  			addrs.Resource{
    37  				Mode: addrs.ManagedResourceMode,
    38  				Type: "aws_instance",
    39  				Name: "beta",
    40  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
    41  			&states.ResourceInstanceObjectSrc{
    42  				AttrsJSON: []byte(`{"id":"beta","foo":"value","bar":"value"}`),
    43  				Status:    states.ObjectReady,
    44  			},
    45  			addrs.AbsProviderConfig{
    46  				Provider: addrs.NewDefaultProvider("aws"),
    47  				Module:   addrs.RootModule,
    48  			},
    49  		)
    50  		s.SetResourceInstanceCurrent(
    51  			addrs.Resource{
    52  				Mode: addrs.ManagedResourceMode,
    53  				Type: "azurerm_virtual_machine",
    54  				Name: "gamma",
    55  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
    56  			&states.ResourceInstanceObjectSrc{
    57  				AttrsJSON: []byte(`{"id":"gamma","baz":"value"}`),
    58  				Status:    states.ObjectReady,
    59  			},
    60  			addrs.AbsProviderConfig{
    61  				Provider: addrs.NewLegacyProvider("azurerm"),
    62  				Module:   addrs.RootModule,
    63  			},
    64  		)
    65  	})
    66  
    67  	t.Run("happy path", func(t *testing.T) {
    68  		statePath := testStateFile(t, state)
    69  
    70  		ui := new(cli.MockUi)
    71  		view, _ := testView(t)
    72  		c := &StateReplaceProviderCommand{
    73  			StateMeta{
    74  				Meta: Meta{
    75  					Ui:   ui,
    76  					View: view,
    77  				},
    78  			},
    79  		}
    80  
    81  		inputBuf := &bytes.Buffer{}
    82  		ui.InputReader = inputBuf
    83  		inputBuf.WriteString("yes\n")
    84  
    85  		args := []string{
    86  			"-state", statePath,
    87  			"hashicorp/aws",
    88  			"acmecorp/aws",
    89  		}
    90  		if code := c.Run(args); code != 0 {
    91  			t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String())
    92  		}
    93  
    94  		testStateOutput(t, statePath, testStateReplaceProviderOutput)
    95  
    96  		backups := testStateBackups(t, filepath.Dir(statePath))
    97  		if len(backups) != 1 {
    98  			t.Fatalf("unexpected backups: %#v", backups)
    99  		}
   100  		testStateOutput(t, backups[0], testStateReplaceProviderOutputOriginal)
   101  	})
   102  
   103  	t.Run("auto approve", func(t *testing.T) {
   104  		statePath := testStateFile(t, state)
   105  
   106  		ui := new(cli.MockUi)
   107  		view, _ := testView(t)
   108  		c := &StateReplaceProviderCommand{
   109  			StateMeta{
   110  				Meta: Meta{
   111  					Ui:   ui,
   112  					View: view,
   113  				},
   114  			},
   115  		}
   116  
   117  		inputBuf := &bytes.Buffer{}
   118  		ui.InputReader = inputBuf
   119  
   120  		args := []string{
   121  			"-state", statePath,
   122  			"-auto-approve",
   123  			"hashicorp/aws",
   124  			"acmecorp/aws",
   125  		}
   126  		if code := c.Run(args); code != 0 {
   127  			t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String())
   128  		}
   129  
   130  		testStateOutput(t, statePath, testStateReplaceProviderOutput)
   131  
   132  		backups := testStateBackups(t, filepath.Dir(statePath))
   133  		if len(backups) != 1 {
   134  			t.Fatalf("unexpected backups: %#v", backups)
   135  		}
   136  		testStateOutput(t, backups[0], testStateReplaceProviderOutputOriginal)
   137  	})
   138  
   139  	t.Run("cancel at approval step", func(t *testing.T) {
   140  		statePath := testStateFile(t, state)
   141  
   142  		ui := new(cli.MockUi)
   143  		view, _ := testView(t)
   144  		c := &StateReplaceProviderCommand{
   145  			StateMeta{
   146  				Meta: Meta{
   147  					Ui:   ui,
   148  					View: view,
   149  				},
   150  			},
   151  		}
   152  
   153  		inputBuf := &bytes.Buffer{}
   154  		ui.InputReader = inputBuf
   155  		inputBuf.WriteString("no\n")
   156  
   157  		args := []string{
   158  			"-state", statePath,
   159  			"hashicorp/aws",
   160  			"acmecorp/aws",
   161  		}
   162  		if code := c.Run(args); code != 0 {
   163  			t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String())
   164  		}
   165  
   166  		testStateOutput(t, statePath, testStateReplaceProviderOutputOriginal)
   167  
   168  		backups := testStateBackups(t, filepath.Dir(statePath))
   169  		if len(backups) != 0 {
   170  			t.Fatalf("unexpected backups: %#v", backups)
   171  		}
   172  	})
   173  
   174  	t.Run("no matching provider found", func(t *testing.T) {
   175  		statePath := testStateFile(t, state)
   176  
   177  		ui := new(cli.MockUi)
   178  		view, _ := testView(t)
   179  		c := &StateReplaceProviderCommand{
   180  			StateMeta{
   181  				Meta: Meta{
   182  					Ui:   ui,
   183  					View: view,
   184  				},
   185  			},
   186  		}
   187  
   188  		args := []string{
   189  			"-state", statePath,
   190  			"hashicorp/google",
   191  			"acmecorp/google",
   192  		}
   193  		if code := c.Run(args); code != 0 {
   194  			t.Fatalf("return code: %d\n\n%s", code, ui.ErrorWriter.String())
   195  		}
   196  
   197  		testStateOutput(t, statePath, testStateReplaceProviderOutputOriginal)
   198  
   199  		backups := testStateBackups(t, filepath.Dir(statePath))
   200  		if len(backups) != 0 {
   201  			t.Fatalf("unexpected backups: %#v", backups)
   202  		}
   203  	})
   204  
   205  	t.Run("invalid flags", func(t *testing.T) {
   206  		ui := new(cli.MockUi)
   207  		view, _ := testView(t)
   208  		c := &StateReplaceProviderCommand{
   209  			StateMeta{
   210  				Meta: Meta{
   211  					Ui:   ui,
   212  					View: view,
   213  				},
   214  			},
   215  		}
   216  
   217  		args := []string{
   218  			"-invalid",
   219  			"hashicorp/google",
   220  			"acmecorp/google",
   221  		}
   222  		if code := c.Run(args); code == 0 {
   223  			t.Fatalf("successful exit; want error")
   224  		}
   225  
   226  		if got, want := ui.ErrorWriter.String(), "Error parsing command-line flags"; !strings.Contains(got, want) {
   227  			t.Fatalf("missing expected error message\nwant: %s\nfull output:\n%s", want, got)
   228  		}
   229  	})
   230  
   231  	t.Run("wrong number of arguments", func(t *testing.T) {
   232  		ui := new(cli.MockUi)
   233  		view, _ := testView(t)
   234  		c := &StateReplaceProviderCommand{
   235  			StateMeta{
   236  				Meta: Meta{
   237  					Ui:   ui,
   238  					View: view,
   239  				},
   240  			},
   241  		}
   242  
   243  		args := []string{"a", "b", "c", "d"}
   244  		if code := c.Run(args); code == 0 {
   245  			t.Fatalf("successful exit; want error")
   246  		}
   247  
   248  		if got, want := ui.ErrorWriter.String(), "Exactly two arguments expected"; !strings.Contains(got, want) {
   249  			t.Fatalf("missing expected error message\nwant: %s\nfull output:\n%s", want, got)
   250  		}
   251  	})
   252  
   253  	t.Run("invalid provider strings", func(t *testing.T) {
   254  		ui := new(cli.MockUi)
   255  		view, _ := testView(t)
   256  		c := &StateReplaceProviderCommand{
   257  			StateMeta{
   258  				Meta: Meta{
   259  					Ui:   ui,
   260  					View: view,
   261  				},
   262  			},
   263  		}
   264  
   265  		args := []string{
   266  			"hashicorp/google_cloud",
   267  			"-/-/google",
   268  		}
   269  		if code := c.Run(args); code == 0 {
   270  			t.Fatalf("successful exit; want error")
   271  		}
   272  
   273  		got := ui.ErrorWriter.String()
   274  		msgs := []string{
   275  			`Invalid "from" provider "hashicorp/google_cloud"`,
   276  			"Invalid provider type",
   277  			`Invalid "to" provider "-/-/google"`,
   278  			"Invalid provider source hostname",
   279  		}
   280  		for _, msg := range msgs {
   281  			if !strings.Contains(got, msg) {
   282  				t.Errorf("missing expected error message\nwant: %s\nfull output:\n%s", msg, got)
   283  			}
   284  		}
   285  	})
   286  }
   287  
   288  func TestStateReplaceProvider_docs(t *testing.T) {
   289  	c := &StateReplaceProviderCommand{}
   290  
   291  	if got, want := c.Help(), "Usage: terraform [global options] state replace-provider"; !strings.Contains(got, want) {
   292  		t.Fatalf("unexpected help text\nwant: %s\nfull output:\n%s", want, got)
   293  	}
   294  
   295  	if got, want := c.Synopsis(), "Replace provider in the state"; got != want {
   296  		t.Fatalf("unexpected synopsis\nwant: %s\nfull output:\n%s", want, got)
   297  	}
   298  }
   299  
   300  func TestStateReplaceProvider_checkRequiredVersion(t *testing.T) {
   301  	// Create a temporary working directory that is empty
   302  	td := t.TempDir()
   303  	testCopyDir(t, testFixturePath("command-check-required-version"), td)
   304  	defer testChdir(t, td)()
   305  
   306  	state := states.BuildState(func(s *states.SyncState) {
   307  		s.SetResourceInstanceCurrent(
   308  			addrs.Resource{
   309  				Mode: addrs.ManagedResourceMode,
   310  				Type: "aws_instance",
   311  				Name: "alpha",
   312  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   313  			&states.ResourceInstanceObjectSrc{
   314  				AttrsJSON: []byte(`{"id":"alpha","foo":"value","bar":"value"}`),
   315  				Status:    states.ObjectReady,
   316  			},
   317  			addrs.AbsProviderConfig{
   318  				Provider: addrs.NewDefaultProvider("aws"),
   319  				Module:   addrs.RootModule,
   320  			},
   321  		)
   322  		s.SetResourceInstanceCurrent(
   323  			addrs.Resource{
   324  				Mode: addrs.ManagedResourceMode,
   325  				Type: "aws_instance",
   326  				Name: "beta",
   327  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   328  			&states.ResourceInstanceObjectSrc{
   329  				AttrsJSON: []byte(`{"id":"beta","foo":"value","bar":"value"}`),
   330  				Status:    states.ObjectReady,
   331  			},
   332  			addrs.AbsProviderConfig{
   333  				Provider: addrs.NewDefaultProvider("aws"),
   334  				Module:   addrs.RootModule,
   335  			},
   336  		)
   337  		s.SetResourceInstanceCurrent(
   338  			addrs.Resource{
   339  				Mode: addrs.ManagedResourceMode,
   340  				Type: "azurerm_virtual_machine",
   341  				Name: "gamma",
   342  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   343  			&states.ResourceInstanceObjectSrc{
   344  				AttrsJSON: []byte(`{"id":"gamma","baz":"value"}`),
   345  				Status:    states.ObjectReady,
   346  			},
   347  			addrs.AbsProviderConfig{
   348  				Provider: addrs.NewLegacyProvider("azurerm"),
   349  				Module:   addrs.RootModule,
   350  			},
   351  		)
   352  	})
   353  
   354  	statePath := testStateFile(t, state)
   355  
   356  	ui := new(cli.MockUi)
   357  	view, _ := testView(t)
   358  	c := &StateReplaceProviderCommand{
   359  		StateMeta{
   360  			Meta: Meta{
   361  				Ui:   ui,
   362  				View: view,
   363  			},
   364  		},
   365  	}
   366  
   367  	inputBuf := &bytes.Buffer{}
   368  	ui.InputReader = inputBuf
   369  	inputBuf.WriteString("yes\n")
   370  
   371  	args := []string{
   372  		"-state", statePath,
   373  		"hashicorp/aws",
   374  		"acmecorp/aws",
   375  	}
   376  	if code := c.Run(args); code != 1 {
   377  		t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String())
   378  	}
   379  
   380  	// State is unchanged
   381  	testStateOutput(t, statePath, testStateReplaceProviderOutputOriginal)
   382  
   383  	// Required version diags are correct
   384  	errStr := ui.ErrorWriter.String()
   385  	if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) {
   386  		t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr)
   387  	}
   388  	if strings.Contains(errStr, `required_version = ">= 0.13.0"`) {
   389  		t.Fatalf("output should not point to met version constraint, but is:\n\n%s", errStr)
   390  	}
   391  }
   392  
   393  const testStateReplaceProviderOutputOriginal = `
   394  aws_instance.alpha:
   395    ID = alpha
   396    provider = provider["registry.terraform.io/hashicorp/aws"]
   397    bar = value
   398    foo = value
   399  aws_instance.beta:
   400    ID = beta
   401    provider = provider["registry.terraform.io/hashicorp/aws"]
   402    bar = value
   403    foo = value
   404  azurerm_virtual_machine.gamma:
   405    ID = gamma
   406    provider = provider["registry.terraform.io/-/azurerm"]
   407    baz = value
   408  `
   409  
   410  const testStateReplaceProviderOutput = `
   411  aws_instance.alpha:
   412    ID = alpha
   413    provider = provider["registry.terraform.io/acmecorp/aws"]
   414    bar = value
   415    foo = value
   416  aws_instance.beta:
   417    ID = beta
   418    provider = provider["registry.terraform.io/acmecorp/aws"]
   419    bar = value
   420    foo = value
   421  azurerm_virtual_machine.gamma:
   422    ID = gamma
   423    provider = provider["registry.terraform.io/-/azurerm"]
   424    baz = value
   425  `