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 `