github.com/opentofu/opentofu@v1.7.1/internal/command/init_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package command 7 8 import ( 9 "bytes" 10 "context" 11 "encoding/json" 12 "fmt" 13 "log" 14 "os" 15 "path/filepath" 16 "strings" 17 "testing" 18 19 "github.com/davecgh/go-spew/spew" 20 "github.com/google/go-cmp/cmp" 21 "github.com/mitchellh/cli" 22 "github.com/zclconf/go-cty/cty" 23 24 "github.com/hashicorp/go-version" 25 26 "github.com/opentofu/opentofu/internal/addrs" 27 "github.com/opentofu/opentofu/internal/configs" 28 "github.com/opentofu/opentofu/internal/configs/configschema" 29 "github.com/opentofu/opentofu/internal/depsfile" 30 "github.com/opentofu/opentofu/internal/encryption" 31 "github.com/opentofu/opentofu/internal/getproviders" 32 "github.com/opentofu/opentofu/internal/providercache" 33 "github.com/opentofu/opentofu/internal/states" 34 "github.com/opentofu/opentofu/internal/states/statefile" 35 "github.com/opentofu/opentofu/internal/states/statemgr" 36 ) 37 38 func TestInit_empty(t *testing.T) { 39 // Create a temporary working directory that is empty 40 td := t.TempDir() 41 os.MkdirAll(td, 0755) 42 defer testChdir(t, td)() 43 44 ui := new(cli.MockUi) 45 view, _ := testView(t) 46 c := &InitCommand{ 47 Meta: Meta{ 48 testingOverrides: metaOverridesForProvider(testProvider()), 49 Ui: ui, 50 View: view, 51 }, 52 } 53 54 args := []string{} 55 if code := c.Run(args); code != 0 { 56 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 57 } 58 } 59 60 func TestInit_multipleArgs(t *testing.T) { 61 // Create a temporary working directory that is empty 62 td := t.TempDir() 63 os.MkdirAll(td, 0755) 64 defer testChdir(t, td)() 65 66 ui := new(cli.MockUi) 67 view, _ := testView(t) 68 c := &InitCommand{ 69 Meta: Meta{ 70 testingOverrides: metaOverridesForProvider(testProvider()), 71 Ui: ui, 72 View: view, 73 }, 74 } 75 76 args := []string{ 77 "bad", 78 "bad", 79 } 80 if code := c.Run(args); code != 1 { 81 t.Fatalf("bad: \n%s", ui.OutputWriter.String()) 82 } 83 } 84 85 func TestInit_fromModule_cwdDest(t *testing.T) { 86 // Create a temporary working directory that is empty 87 td := t.TempDir() 88 os.MkdirAll(td, os.ModePerm) 89 defer testChdir(t, td)() 90 91 ui := new(cli.MockUi) 92 view, _ := testView(t) 93 c := &InitCommand{ 94 Meta: Meta{ 95 testingOverrides: metaOverridesForProvider(testProvider()), 96 Ui: ui, 97 View: view, 98 }, 99 } 100 101 args := []string{ 102 "-from-module=" + testFixturePath("init"), 103 } 104 if code := c.Run(args); code != 0 { 105 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 106 } 107 108 if _, err := os.Stat(filepath.Join(td, "hello.tf")); err != nil { 109 t.Fatalf("err: %s", err) 110 } 111 } 112 113 // https://github.com/hashicorp/terraform/issues/518 114 func TestInit_fromModule_dstInSrc(t *testing.T) { 115 dir := t.TempDir() 116 if err := os.MkdirAll(dir, 0755); err != nil { 117 t.Fatalf("err: %s", err) 118 } 119 120 // Change to the temporary directory 121 cwd, err := os.Getwd() 122 if err != nil { 123 t.Fatalf("err: %s", err) 124 } 125 if err := os.Chdir(dir); err != nil { 126 t.Fatalf("err: %s", err) 127 } 128 defer os.Chdir(cwd) 129 130 if err := os.Mkdir("foo", os.ModePerm); err != nil { 131 t.Fatal(err) 132 } 133 134 if _, err := os.Create("issue518.tf"); err != nil { 135 t.Fatalf("err: %s", err) 136 } 137 138 if err := os.Chdir("foo"); err != nil { 139 t.Fatalf("err: %s", err) 140 } 141 142 ui := new(cli.MockUi) 143 view, _ := testView(t) 144 c := &InitCommand{ 145 Meta: Meta{ 146 testingOverrides: metaOverridesForProvider(testProvider()), 147 Ui: ui, 148 View: view, 149 }, 150 } 151 152 args := []string{ 153 "-from-module=./..", 154 } 155 if code := c.Run(args); code != 0 { 156 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 157 } 158 159 if _, err := os.Stat(filepath.Join(dir, "foo", "issue518.tf")); err != nil { 160 t.Fatalf("err: %s", err) 161 } 162 } 163 164 func TestInit_get(t *testing.T) { 165 // Create a temporary working directory that is empty 166 td := t.TempDir() 167 testCopyDir(t, testFixturePath("init-get"), td) 168 defer testChdir(t, td)() 169 170 ui := new(cli.MockUi) 171 view, _ := testView(t) 172 c := &InitCommand{ 173 Meta: Meta{ 174 testingOverrides: metaOverridesForProvider(testProvider()), 175 Ui: ui, 176 View: view, 177 }, 178 } 179 180 args := []string{} 181 if code := c.Run(args); code != 0 { 182 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 183 } 184 185 // Check output 186 output := ui.OutputWriter.String() 187 if !strings.Contains(output, "foo in foo") { 188 t.Fatalf("doesn't look like we installed module 'foo': %s", output) 189 } 190 } 191 192 func TestInit_getUpgradeModules(t *testing.T) { 193 // Create a temporary working directory that is empty 194 td := t.TempDir() 195 testCopyDir(t, testFixturePath("init-get"), td) 196 defer testChdir(t, td)() 197 198 ui := new(cli.MockUi) 199 view, _ := testView(t) 200 c := &InitCommand{ 201 Meta: Meta{ 202 testingOverrides: metaOverridesForProvider(testProvider()), 203 Ui: ui, 204 View: view, 205 }, 206 } 207 208 args := []string{ 209 "-get=true", 210 "-upgrade", 211 } 212 if code := c.Run(args); code != 0 { 213 t.Fatalf("command did not complete successfully:\n%s", ui.ErrorWriter.String()) 214 } 215 216 // Check output 217 output := ui.OutputWriter.String() 218 if !strings.Contains(output, "Upgrading modules...") { 219 t.Fatalf("doesn't look like get upgrade: %s", output) 220 } 221 } 222 223 func TestInit_backend(t *testing.T) { 224 // Create a temporary working directory that is empty 225 td := t.TempDir() 226 testCopyDir(t, testFixturePath("init-backend"), td) 227 defer testChdir(t, td)() 228 229 ui := new(cli.MockUi) 230 view, _ := testView(t) 231 c := &InitCommand{ 232 Meta: Meta{ 233 testingOverrides: metaOverridesForProvider(testProvider()), 234 Ui: ui, 235 View: view, 236 }, 237 } 238 239 args := []string{} 240 if code := c.Run(args); code != 0 { 241 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 242 } 243 244 if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { 245 t.Fatalf("err: %s", err) 246 } 247 } 248 249 func TestInit_backendUnset(t *testing.T) { 250 // Create a temporary working directory that is empty 251 td := t.TempDir() 252 testCopyDir(t, testFixturePath("init-backend"), td) 253 defer testChdir(t, td)() 254 255 { 256 log.Printf("[TRACE] TestInit_backendUnset: beginning first init") 257 258 ui := cli.NewMockUi() 259 view, _ := testView(t) 260 c := &InitCommand{ 261 Meta: Meta{ 262 testingOverrides: metaOverridesForProvider(testProvider()), 263 Ui: ui, 264 View: view, 265 }, 266 } 267 268 // Init 269 args := []string{} 270 if code := c.Run(args); code != 0 { 271 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 272 } 273 log.Printf("[TRACE] TestInit_backendUnset: first init complete") 274 t.Logf("First run output:\n%s", ui.OutputWriter.String()) 275 t.Logf("First run errors:\n%s", ui.ErrorWriter.String()) 276 277 if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { 278 t.Fatalf("err: %s", err) 279 } 280 } 281 282 { 283 log.Printf("[TRACE] TestInit_backendUnset: beginning second init") 284 285 // Unset 286 if err := os.WriteFile("main.tf", []byte(""), 0644); err != nil { 287 t.Fatalf("err: %s", err) 288 } 289 290 ui := cli.NewMockUi() 291 view, _ := testView(t) 292 c := &InitCommand{ 293 Meta: Meta{ 294 testingOverrides: metaOverridesForProvider(testProvider()), 295 Ui: ui, 296 View: view, 297 }, 298 } 299 300 args := []string{"-force-copy"} 301 if code := c.Run(args); code != 0 { 302 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 303 } 304 log.Printf("[TRACE] TestInit_backendUnset: second init complete") 305 t.Logf("Second run output:\n%s", ui.OutputWriter.String()) 306 t.Logf("Second run errors:\n%s", ui.ErrorWriter.String()) 307 308 s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 309 if !s.Backend.Empty() { 310 t.Fatal("should not have backend config") 311 } 312 } 313 } 314 315 func TestInit_backendConfigFile(t *testing.T) { 316 // Create a temporary working directory that is empty 317 td := t.TempDir() 318 testCopyDir(t, testFixturePath("init-backend-config-file"), td) 319 defer testChdir(t, td)() 320 321 t.Run("good-config-file", func(t *testing.T) { 322 ui := new(cli.MockUi) 323 view, _ := testView(t) 324 c := &InitCommand{ 325 Meta: Meta{ 326 testingOverrides: metaOverridesForProvider(testProvider()), 327 Ui: ui, 328 View: view, 329 }, 330 } 331 args := []string{"-backend-config", "input.config"} 332 if code := c.Run(args); code != 0 { 333 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 334 } 335 336 // Read our saved backend config and verify we have our settings 337 state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 338 if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"hello","workspace_dir":null}`; got != want { 339 t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) 340 } 341 }) 342 343 // the backend config file must not be a full tofu block 344 t.Run("full-backend-config-file", func(t *testing.T) { 345 ui := new(cli.MockUi) 346 view, _ := testView(t) 347 c := &InitCommand{ 348 Meta: Meta{ 349 testingOverrides: metaOverridesForProvider(testProvider()), 350 Ui: ui, 351 View: view, 352 }, 353 } 354 args := []string{"-backend-config", "backend.config"} 355 if code := c.Run(args); code != 1 { 356 t.Fatalf("expected error, got success\n") 357 } 358 if !strings.Contains(ui.ErrorWriter.String(), "Unsupported block type") { 359 t.Fatalf("wrong error: %s", ui.ErrorWriter) 360 } 361 }) 362 363 // the backend config file must match the schema for the backend 364 t.Run("invalid-config-file", func(t *testing.T) { 365 ui := new(cli.MockUi) 366 view, _ := testView(t) 367 c := &InitCommand{ 368 Meta: Meta{ 369 testingOverrides: metaOverridesForProvider(testProvider()), 370 Ui: ui, 371 View: view, 372 }, 373 } 374 args := []string{"-backend-config", "invalid.config"} 375 if code := c.Run(args); code != 1 { 376 t.Fatalf("expected error, got success\n") 377 } 378 if !strings.Contains(ui.ErrorWriter.String(), "Unsupported argument") { 379 t.Fatalf("wrong error: %s", ui.ErrorWriter) 380 } 381 }) 382 383 // missing file is an error 384 t.Run("missing-config-file", func(t *testing.T) { 385 ui := new(cli.MockUi) 386 view, _ := testView(t) 387 c := &InitCommand{ 388 Meta: Meta{ 389 testingOverrides: metaOverridesForProvider(testProvider()), 390 Ui: ui, 391 View: view, 392 }, 393 } 394 args := []string{"-backend-config", "missing.config"} 395 if code := c.Run(args); code != 1 { 396 t.Fatalf("expected error, got success\n") 397 } 398 if !strings.Contains(ui.ErrorWriter.String(), "Failed to read file") { 399 t.Fatalf("wrong error: %s", ui.ErrorWriter) 400 } 401 }) 402 403 // blank filename clears the backend config 404 t.Run("blank-config-file", func(t *testing.T) { 405 ui := new(cli.MockUi) 406 view, _ := testView(t) 407 c := &InitCommand{ 408 Meta: Meta{ 409 testingOverrides: metaOverridesForProvider(testProvider()), 410 Ui: ui, 411 View: view, 412 }, 413 } 414 args := []string{"-backend-config=", "-migrate-state"} 415 if code := c.Run(args); code != 0 { 416 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 417 } 418 419 // Read our saved backend config and verify the backend config is empty 420 state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 421 if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":null,"workspace_dir":null}`; got != want { 422 t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) 423 } 424 }) 425 426 // simulate the local backend having a required field which is not 427 // specified in the override file 428 t.Run("required-argument", func(t *testing.T) { 429 c := &InitCommand{} 430 schema := &configschema.Block{ 431 Attributes: map[string]*configschema.Attribute{ 432 "path": { 433 Type: cty.String, 434 Optional: true, 435 }, 436 "workspace_dir": { 437 Type: cty.String, 438 Required: true, 439 }, 440 }, 441 } 442 flagConfigExtra := newRawFlags("-backend-config") 443 flagConfigExtra.Set("input.config") 444 _, diags := c.backendConfigOverrideBody(flagConfigExtra, schema) 445 if len(diags) != 0 { 446 t.Errorf("expected no diags, got: %s", diags.Err()) 447 } 448 }) 449 } 450 451 func TestInit_backendConfigFilePowershellConfusion(t *testing.T) { 452 // Create a temporary working directory that is empty 453 td := t.TempDir() 454 testCopyDir(t, testFixturePath("init-backend-config-file"), td) 455 defer testChdir(t, td)() 456 457 ui := new(cli.MockUi) 458 view, _ := testView(t) 459 c := &InitCommand{ 460 Meta: Meta{ 461 testingOverrides: metaOverridesForProvider(testProvider()), 462 Ui: ui, 463 View: view, 464 }, 465 } 466 467 // SUBTLE: when using -flag=value with Powershell, unquoted values are 468 // broken into separate arguments. This results in the init command 469 // interpreting the flags as an empty backend-config setting (which is 470 // semantically valid!) followed by a custom configuration path. 471 // 472 // Adding the "=" here forces this codepath to be checked, and it should 473 // result in an early exit with a diagnostic that the provided 474 // configuration file is not a diretory. 475 args := []string{"-backend-config=", "./input.config"} 476 if code := c.Run(args); code != 1 { 477 t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) 478 } 479 480 output := ui.ErrorWriter.String() 481 if got, want := output, `Too many command line arguments`; !strings.Contains(got, want) { 482 t.Fatalf("wrong output\ngot:\n%s\n\nwant: message containing %q", got, want) 483 } 484 } 485 486 func TestInit_backendReconfigure(t *testing.T) { 487 // Create a temporary working directory that is empty 488 td := t.TempDir() 489 testCopyDir(t, testFixturePath("init-backend"), td) 490 defer testChdir(t, td)() 491 492 providerSource, close := newMockProviderSource(t, map[string][]string{ 493 "hashicorp/test": {"1.2.3"}, 494 }) 495 defer close() 496 497 ui := new(cli.MockUi) 498 view, _ := testView(t) 499 c := &InitCommand{ 500 Meta: Meta{ 501 testingOverrides: metaOverridesForProvider(testProvider()), 502 ProviderSource: providerSource, 503 Ui: ui, 504 View: view, 505 }, 506 } 507 508 // create some state, so the backend has something to migrate. 509 f, err := os.Create("foo") // this is the path" in the backend config 510 if err != nil { 511 t.Fatalf("err: %s", err) 512 } 513 err = writeStateForTesting(testState(), f) 514 f.Close() 515 if err != nil { 516 t.Fatalf("err: %s", err) 517 } 518 519 args := []string{} 520 if code := c.Run(args); code != 0 { 521 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 522 } 523 524 // now run init again, changing the path. 525 // The -reconfigure flag prevents init from migrating 526 // Without -reconfigure, the test fails since the backend asks for input on migrating state 527 args = []string{"-reconfigure", "-backend-config", "path=changed"} 528 if code := c.Run(args); code != 0 { 529 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 530 } 531 } 532 533 func TestInit_backendConfigFileChange(t *testing.T) { 534 // Create a temporary working directory that is empty 535 td := t.TempDir() 536 testCopyDir(t, testFixturePath("init-backend-config-file-change"), td) 537 defer testChdir(t, td)() 538 539 ui := new(cli.MockUi) 540 view, _ := testView(t) 541 c := &InitCommand{ 542 Meta: Meta{ 543 testingOverrides: metaOverridesForProvider(testProvider()), 544 Ui: ui, 545 View: view, 546 }, 547 } 548 549 args := []string{"-backend-config", "input.config", "-migrate-state"} 550 if code := c.Run(args); code != 0 { 551 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 552 } 553 554 // Read our saved backend config and verify we have our settings 555 state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 556 if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"hello","workspace_dir":null}`; got != want { 557 t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) 558 } 559 } 560 561 func TestInit_backendMigrateWhileLocked(t *testing.T) { 562 // Create a temporary working directory that is empty 563 td := t.TempDir() 564 testCopyDir(t, testFixturePath("init-backend-migrate-while-locked"), td) 565 defer testChdir(t, td)() 566 567 providerSource, close := newMockProviderSource(t, map[string][]string{ 568 "hashicorp/test": {"1.2.3"}, 569 }) 570 defer close() 571 572 ui := new(cli.MockUi) 573 view, _ := testView(t) 574 c := &InitCommand{ 575 Meta: Meta{ 576 testingOverrides: metaOverridesForProvider(testProvider()), 577 ProviderSource: providerSource, 578 Ui: ui, 579 View: view, 580 }, 581 } 582 583 // Create some state, so the backend has something to migrate from 584 f, err := os.Create("local-state.tfstate") 585 if err != nil { 586 t.Fatalf("err: %s", err) 587 } 588 err = writeStateForTesting(testState(), f) 589 f.Close() 590 if err != nil { 591 t.Fatalf("err: %s", err) 592 } 593 594 // Lock the source state 595 unlock, err := testLockState(t, testDataDir, "local-state.tfstate") 596 if err != nil { 597 t.Fatal(err) 598 } 599 defer unlock() 600 601 // Attempt to migrate 602 args := []string{"-backend-config", "input.config", "-migrate-state", "-force-copy"} 603 if code := c.Run(args); code == 0 { 604 t.Fatalf("expected nonzero exit code: %s", ui.OutputWriter.String()) 605 } 606 607 // Disabling locking should work 608 args = []string{"-backend-config", "input.config", "-migrate-state", "-force-copy", "-lock=false"} 609 if code := c.Run(args); code != 0 { 610 t.Fatalf("expected zero exit code, got %d: %s", code, ui.ErrorWriter.String()) 611 } 612 } 613 614 func TestInit_backendConfigFileChangeWithExistingState(t *testing.T) { 615 // Create a temporary working directory that is empty 616 td := t.TempDir() 617 testCopyDir(t, testFixturePath("init-backend-config-file-change-migrate-existing"), td) 618 defer testChdir(t, td)() 619 620 ui := new(cli.MockUi) 621 c := &InitCommand{ 622 Meta: Meta{ 623 testingOverrides: metaOverridesForProvider(testProvider()), 624 Ui: ui, 625 }, 626 } 627 628 oldState := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 629 630 // we deliberately do not provide the answer for backend-migrate-copy-to-empty to trigger error 631 args := []string{"-migrate-state", "-backend-config", "input.config", "-input=true"} 632 if code := c.Run(args); code == 0 { 633 t.Fatal("expected error") 634 } 635 636 // Read our backend config and verify new settings are not saved 637 state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 638 if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"local-state.tfstate"}`; got != want { 639 t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) 640 } 641 642 // without changing config, hash should not change 643 if oldState.Backend.Hash != state.Backend.Hash { 644 t.Errorf("backend hash should not have changed\ngot: %d\nwant: %d", state.Backend.Hash, oldState.Backend.Hash) 645 } 646 } 647 648 func TestInit_backendConfigKV(t *testing.T) { 649 // Create a temporary working directory that is empty 650 td := t.TempDir() 651 testCopyDir(t, testFixturePath("init-backend-config-kv"), td) 652 defer testChdir(t, td)() 653 654 ui := new(cli.MockUi) 655 view, _ := testView(t) 656 c := &InitCommand{ 657 Meta: Meta{ 658 testingOverrides: metaOverridesForProvider(testProvider()), 659 Ui: ui, 660 View: view, 661 }, 662 } 663 664 args := []string{"-backend-config", "path=hello"} 665 if code := c.Run(args); code != 0 { 666 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 667 } 668 669 // Read our saved backend config and verify we have our settings 670 state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 671 if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"hello","workspace_dir":null}`; got != want { 672 t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) 673 } 674 } 675 676 func TestInit_backendConfigKVReInit(t *testing.T) { 677 // Create a temporary working directory that is empty 678 td := t.TempDir() 679 testCopyDir(t, testFixturePath("init-backend-config-kv"), td) 680 defer testChdir(t, td)() 681 682 ui := new(cli.MockUi) 683 view, _ := testView(t) 684 c := &InitCommand{ 685 Meta: Meta{ 686 testingOverrides: metaOverridesForProvider(testProvider()), 687 Ui: ui, 688 View: view, 689 }, 690 } 691 692 args := []string{"-backend-config", "path=test"} 693 if code := c.Run(args); code != 0 { 694 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 695 } 696 697 ui = new(cli.MockUi) 698 c = &InitCommand{ 699 Meta: Meta{ 700 testingOverrides: metaOverridesForProvider(testProvider()), 701 Ui: ui, 702 View: view, 703 }, 704 } 705 706 // a second init should require no changes, nor should it change the backend. 707 args = []string{"-input=false"} 708 if code := c.Run(args); code != 0 { 709 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 710 } 711 712 // make sure the backend is configured how we expect 713 configState := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 714 cfg := map[string]interface{}{} 715 if err := json.Unmarshal(configState.Backend.ConfigRaw, &cfg); err != nil { 716 t.Fatal(err) 717 } 718 if cfg["path"] != "test" { 719 t.Fatalf(`expected backend path="test", got path="%v"`, cfg["path"]) 720 } 721 722 // override the -backend-config options by settings 723 args = []string{"-input=false", "-backend-config", "", "-migrate-state"} 724 if code := c.Run(args); code != 0 { 725 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 726 } 727 728 // make sure the backend is configured how we expect 729 configState = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 730 cfg = map[string]interface{}{} 731 if err := json.Unmarshal(configState.Backend.ConfigRaw, &cfg); err != nil { 732 t.Fatal(err) 733 } 734 if cfg["path"] != nil { 735 t.Fatalf(`expected backend path="<nil>", got path="%v"`, cfg["path"]) 736 } 737 } 738 739 func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { 740 // Create a temporary working directory that is empty 741 td := t.TempDir() 742 testCopyDir(t, testFixturePath("init-backend"), td) 743 defer testChdir(t, td)() 744 745 ui := new(cli.MockUi) 746 view, _ := testView(t) 747 c := &InitCommand{ 748 Meta: Meta{ 749 testingOverrides: metaOverridesForProvider(testProvider()), 750 Ui: ui, 751 View: view, 752 }, 753 } 754 755 args := []string{"-input=false"} 756 if code := c.Run(args); code != 0 { 757 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 758 } 759 760 ui = new(cli.MockUi) 761 c = &InitCommand{ 762 Meta: Meta{ 763 testingOverrides: metaOverridesForProvider(testProvider()), 764 Ui: ui, 765 View: view, 766 }, 767 } 768 769 // a second init with identical config should require no changes, nor 770 // should it change the backend. 771 args = []string{"-input=false", "-backend-config", "path=foo"} 772 if code := c.Run(args); code != 0 { 773 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 774 } 775 776 // make sure the backend is configured how we expect 777 configState := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 778 cfg := map[string]interface{}{} 779 if err := json.Unmarshal(configState.Backend.ConfigRaw, &cfg); err != nil { 780 t.Fatal(err) 781 } 782 if cfg["path"] != "foo" { 783 t.Fatalf(`expected backend path="foo", got path="%v"`, cfg["foo"]) 784 } 785 } 786 787 func TestInit_backendCli_no_config_block(t *testing.T) { 788 // Create a temporary working directory that is empty 789 td := t.TempDir() 790 testCopyDir(t, testFixturePath("init"), td) 791 defer testChdir(t, td)() 792 793 ui := new(cli.MockUi) 794 view, _ := testView(t) 795 c := &InitCommand{ 796 Meta: Meta{ 797 testingOverrides: metaOverridesForProvider(testProvider()), 798 Ui: ui, 799 View: view, 800 }, 801 } 802 803 args := []string{"-backend-config", "path=test"} 804 if code := c.Run(args); code != 0 { 805 t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) 806 } 807 808 errMsg := ui.ErrorWriter.String() 809 if !strings.Contains(errMsg, "Warning: Missing backend configuration") { 810 t.Fatal("expected missing backend block warning, got", errMsg) 811 } 812 } 813 814 func TestInit_backendReinitWithExtra(t *testing.T) { 815 td := t.TempDir() 816 testCopyDir(t, testFixturePath("init-backend-empty"), td) 817 defer testChdir(t, td)() 818 819 m := testMetaBackend(t, nil) 820 opts := &BackendOpts{ 821 ConfigOverride: configs.SynthBody("synth", map[string]cty.Value{ 822 "path": cty.StringVal("hello"), 823 }), 824 Init: true, 825 } 826 827 _, cHash, err := m.backendConfig(opts) 828 if err != nil { 829 t.Fatal(err) 830 } 831 832 ui := new(cli.MockUi) 833 view, _ := testView(t) 834 c := &InitCommand{ 835 Meta: Meta{ 836 testingOverrides: metaOverridesForProvider(testProvider()), 837 Ui: ui, 838 View: view, 839 }, 840 } 841 842 args := []string{"-backend-config", "path=hello"} 843 if code := c.Run(args); code != 0 { 844 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 845 } 846 847 // Read our saved backend config and verify we have our settings 848 state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 849 if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"hello","workspace_dir":null}`; got != want { 850 t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) 851 } 852 853 if state.Backend.Hash != uint64(cHash) { 854 t.Fatal("mismatched state and config backend hashes") 855 } 856 857 // init again and make sure nothing changes 858 if code := c.Run(args); code != 0 { 859 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 860 } 861 state = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 862 if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"hello","workspace_dir":null}`; got != want { 863 t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) 864 } 865 if state.Backend.Hash != uint64(cHash) { 866 t.Fatal("mismatched state and config backend hashes") 867 } 868 } 869 870 // move option from config to -backend-config args 871 func TestInit_backendReinitConfigToExtra(t *testing.T) { 872 td := t.TempDir() 873 testCopyDir(t, testFixturePath("init-backend"), td) 874 defer testChdir(t, td)() 875 876 ui := new(cli.MockUi) 877 view, _ := testView(t) 878 c := &InitCommand{ 879 Meta: Meta{ 880 testingOverrides: metaOverridesForProvider(testProvider()), 881 Ui: ui, 882 View: view, 883 }, 884 } 885 886 if code := c.Run([]string{"-input=false"}); code != 0 { 887 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 888 } 889 890 // Read our saved backend config and verify we have our settings 891 state := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 892 if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"foo","workspace_dir":null}`; got != want { 893 t.Errorf("wrong config\ngot: %s\nwant: %s", got, want) 894 } 895 896 backendHash := state.Backend.Hash 897 898 // init again but remove the path option from the config 899 cfg := "terraform {\n backend \"local\" {}\n}\n" 900 if err := os.WriteFile("main.tf", []byte(cfg), 0644); err != nil { 901 t.Fatal(err) 902 } 903 904 // We need a fresh InitCommand here because the old one now has our configuration 905 // file cached inside it, so it won't re-read the modification we just made. 906 c = &InitCommand{ 907 Meta: Meta{ 908 testingOverrides: metaOverridesForProvider(testProvider()), 909 Ui: ui, 910 View: view, 911 }, 912 } 913 914 args := []string{"-input=false", "-backend-config=path=foo"} 915 if code := c.Run(args); code != 0 { 916 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 917 } 918 state = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) 919 if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"foo","workspace_dir":null}`; got != want { 920 t.Errorf("wrong config after moving to arg\ngot: %s\nwant: %s", got, want) 921 } 922 923 if state.Backend.Hash == backendHash { 924 t.Fatal("state.Backend.Hash was not updated") 925 } 926 } 927 928 func TestInit_backendCloudInvalidOptions(t *testing.T) { 929 // There are various "tofu init" options that are only for 930 // traditional backends and not applicable to Terraform Cloud mode. 931 // For those, we want to return an explicit error rather than 932 // just silently ignoring them, so that users will be aware that 933 // Cloud mode has more of an expected "happy path" than the 934 // less-vertically-integrated backends do, and to avoid these 935 // unapplicable options becoming compatibility constraints for 936 // future evolution of Cloud mode. 937 938 // We use the same starting fixture for all of these tests, but some 939 // of them will customize it a bit as part of their work. 940 setupTempDir := func(t *testing.T) func() { 941 t.Helper() 942 td := t.TempDir() 943 testCopyDir(t, testFixturePath("init-cloud-simple"), td) 944 unChdir := testChdir(t, td) 945 return unChdir 946 } 947 948 // Some of the tests need a non-empty placeholder state file to work 949 // with. 950 fakeState := states.BuildState(func(cb *states.SyncState) { 951 // Having a root module output value should be enough for this 952 // state file to be considered "non-empty" and thus a candidate 953 // for migration. 954 cb.SetOutputValue( 955 addrs.OutputValue{Name: "a"}.Absolute(addrs.RootModuleInstance), 956 cty.True, 957 false, 958 ) 959 }) 960 fakeStateFile := &statefile.File{ 961 Lineage: "boop", 962 Serial: 4, 963 TerraformVersion: version.Must(version.NewVersion("1.0.0")), 964 State: fakeState, 965 } 966 var fakeStateBuf bytes.Buffer 967 err := statefile.WriteForTest(fakeStateFile, &fakeStateBuf) 968 if err != nil { 969 t.Error(err) 970 } 971 fakeStateBytes := fakeStateBuf.Bytes() 972 973 t.Run("-backend-config", func(t *testing.T) { 974 defer setupTempDir(t)() 975 976 // We have -backend-config as a pragmatic way to dynamically set 977 // certain settings of backends that tend to vary depending on 978 // where OpenTofu is running, such as AWS authentication profiles 979 // that are naturally local only to the machine where OpenTofu is 980 // running. Those needs don't apply to Terraform Cloud, because 981 // the remote workspace encapsulates all of the details of how 982 // operations and state work in that case, and so the Cloud 983 // configuration is only about which workspaces we'll be working 984 // with. 985 ui := cli.NewMockUi() 986 view, _ := testView(t) 987 c := &InitCommand{ 988 Meta: Meta{ 989 Ui: ui, 990 View: view, 991 }, 992 } 993 args := []string{"-backend-config=anything"} 994 if code := c.Run(args); code == 0 { 995 t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) 996 } 997 998 gotStderr := ui.ErrorWriter.String() 999 wantStderr := ` 1000 Error: Invalid command-line option 1001 1002 The -backend-config=... command line option is only for state backends, and 1003 is not applicable to cloud backend-based configurations. 1004 1005 To change the set of workspaces associated with this configuration, edit the 1006 Cloud configuration block in the root module. 1007 1008 ` 1009 if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { 1010 t.Errorf("wrong error output\n%s", diff) 1011 } 1012 }) 1013 t.Run("-reconfigure", func(t *testing.T) { 1014 defer setupTempDir(t)() 1015 1016 // The -reconfigure option was originally imagined as a way to force 1017 // skipping state migration when migrating between backends, but it 1018 // has a historical flaw that it doesn't work properly when the 1019 // initial situation is the implicit local backend with a state file 1020 // present. The Terraform Cloud migration path has some additional 1021 // steps to take care of more details automatically, and so 1022 // -reconfigure doesn't really make sense in that context, particularly 1023 // with its design bug with the handling of the implicit local backend. 1024 ui := cli.NewMockUi() 1025 view, _ := testView(t) 1026 c := &InitCommand{ 1027 Meta: Meta{ 1028 Ui: ui, 1029 View: view, 1030 }, 1031 } 1032 args := []string{"-reconfigure"} 1033 if code := c.Run(args); code == 0 { 1034 t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) 1035 } 1036 1037 gotStderr := ui.ErrorWriter.String() 1038 wantStderr := ` 1039 Error: Invalid command-line option 1040 1041 The -reconfigure option is for in-place reconfiguration of state backends 1042 only, and is not needed when changing cloud backend settings. 1043 1044 When using cloud backend, initialization automatically activates any new 1045 Cloud configuration settings. 1046 1047 ` 1048 if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { 1049 t.Errorf("wrong error output\n%s", diff) 1050 } 1051 }) 1052 t.Run("-reconfigure when migrating in", func(t *testing.T) { 1053 defer setupTempDir(t)() 1054 1055 // We have a slightly different error message for the case where we 1056 // seem to be trying to migrate to Terraform Cloud with existing 1057 // state or explicit backend already present. 1058 1059 if err := os.WriteFile("terraform.tfstate", fakeStateBytes, 0644); err != nil { 1060 t.Fatal(err) 1061 } 1062 1063 ui := cli.NewMockUi() 1064 view, _ := testView(t) 1065 c := &InitCommand{ 1066 Meta: Meta{ 1067 Ui: ui, 1068 View: view, 1069 }, 1070 } 1071 args := []string{"-reconfigure"} 1072 if code := c.Run(args); code == 0 { 1073 t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) 1074 } 1075 1076 gotStderr := ui.ErrorWriter.String() 1077 wantStderr := ` 1078 Error: Invalid command-line option 1079 1080 The -reconfigure option is unsupported when migrating to cloud backend, 1081 because activating cloud backend involves some additional steps. 1082 1083 ` 1084 if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { 1085 t.Errorf("wrong error output\n%s", diff) 1086 } 1087 }) 1088 t.Run("-migrate-state", func(t *testing.T) { 1089 defer setupTempDir(t)() 1090 1091 // In Cloud mode, migrating in or out always proposes migrating state 1092 // and changing configuration while staying in cloud mode never migrates 1093 // state, so this special option isn't relevant. 1094 ui := cli.NewMockUi() 1095 view, _ := testView(t) 1096 c := &InitCommand{ 1097 Meta: Meta{ 1098 Ui: ui, 1099 View: view, 1100 }, 1101 } 1102 args := []string{"-migrate-state"} 1103 if code := c.Run(args); code == 0 { 1104 t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) 1105 } 1106 1107 gotStderr := ui.ErrorWriter.String() 1108 wantStderr := ` 1109 Error: Invalid command-line option 1110 1111 The -migrate-state option is for migration between state backends only, and 1112 is not applicable when using cloud backend. 1113 1114 State storage is handled automatically by cloud backend and so the state 1115 storage location is not configurable. 1116 1117 ` 1118 if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { 1119 t.Errorf("wrong error output\n%s", diff) 1120 } 1121 }) 1122 t.Run("-migrate-state when migrating in", func(t *testing.T) { 1123 defer setupTempDir(t)() 1124 1125 // We have a slightly different error message for the case where we 1126 // seem to be trying to migrate to Terraform Cloud with existing 1127 // state or explicit backend already present. 1128 1129 if err := os.WriteFile("terraform.tfstate", fakeStateBytes, 0644); err != nil { 1130 t.Fatal(err) 1131 } 1132 1133 ui := cli.NewMockUi() 1134 view, _ := testView(t) 1135 c := &InitCommand{ 1136 Meta: Meta{ 1137 Ui: ui, 1138 View: view, 1139 }, 1140 } 1141 args := []string{"-migrate-state"} 1142 if code := c.Run(args); code == 0 { 1143 t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) 1144 } 1145 1146 gotStderr := ui.ErrorWriter.String() 1147 wantStderr := ` 1148 Error: Invalid command-line option 1149 1150 The -migrate-state option is for migration between state backends only, and 1151 is not applicable when using cloud backend. 1152 1153 Cloud backend migration has additional steps, configured by interactive 1154 prompts. 1155 1156 ` 1157 if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { 1158 t.Errorf("wrong error output\n%s", diff) 1159 } 1160 }) 1161 t.Run("-force-copy", func(t *testing.T) { 1162 defer setupTempDir(t)() 1163 1164 // In Cloud mode, migrating in or out always proposes migrating state 1165 // and changing configuration while staying in cloud mode never migrates 1166 // state, so this special option isn't relevant. 1167 ui := cli.NewMockUi() 1168 view, _ := testView(t) 1169 c := &InitCommand{ 1170 Meta: Meta{ 1171 Ui: ui, 1172 View: view, 1173 }, 1174 } 1175 args := []string{"-force-copy"} 1176 if code := c.Run(args); code == 0 { 1177 t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) 1178 } 1179 1180 gotStderr := ui.ErrorWriter.String() 1181 wantStderr := ` 1182 Error: Invalid command-line option 1183 1184 The -force-copy option is for migration between state backends only, and is 1185 not applicable when using cloud backend. 1186 1187 State storage is handled automatically by cloud backend and so the state 1188 storage location is not configurable. 1189 1190 ` 1191 if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { 1192 t.Errorf("wrong error output\n%s", diff) 1193 } 1194 }) 1195 t.Run("-force-copy when migrating in", func(t *testing.T) { 1196 defer setupTempDir(t)() 1197 1198 // We have a slightly different error message for the case where we 1199 // seem to be trying to migrate to Terraform Cloud with existing 1200 // state or explicit backend already present. 1201 1202 if err := os.WriteFile("terraform.tfstate", fakeStateBytes, 0644); err != nil { 1203 t.Fatal(err) 1204 } 1205 1206 ui := cli.NewMockUi() 1207 view, _ := testView(t) 1208 c := &InitCommand{ 1209 Meta: Meta{ 1210 Ui: ui, 1211 View: view, 1212 }, 1213 } 1214 args := []string{"-force-copy"} 1215 if code := c.Run(args); code == 0 { 1216 t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) 1217 } 1218 1219 gotStderr := ui.ErrorWriter.String() 1220 wantStderr := ` 1221 Error: Invalid command-line option 1222 1223 The -force-copy option is for migration between state backends only, and is 1224 not applicable when using cloud backend. 1225 1226 Cloud backend migration has additional steps, configured by interactive 1227 prompts. 1228 1229 ` 1230 if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { 1231 t.Errorf("wrong error output\n%s", diff) 1232 } 1233 }) 1234 1235 } 1236 1237 // make sure inputFalse stops execution on migrate 1238 func TestInit_inputFalse(t *testing.T) { 1239 td := t.TempDir() 1240 testCopyDir(t, testFixturePath("init-backend"), td) 1241 defer testChdir(t, td)() 1242 1243 ui := new(cli.MockUi) 1244 view, _ := testView(t) 1245 c := &InitCommand{ 1246 Meta: Meta{ 1247 testingOverrides: metaOverridesForProvider(testProvider()), 1248 Ui: ui, 1249 View: view, 1250 }, 1251 } 1252 1253 args := []string{"-input=false", "-backend-config=path=foo"} 1254 if code := c.Run(args); code != 0 { 1255 t.Fatalf("bad: \n%s", ui.ErrorWriter) 1256 } 1257 1258 // write different states for foo and bar 1259 fooState := states.BuildState(func(s *states.SyncState) { 1260 s.SetOutputValue( 1261 addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), 1262 cty.StringVal("foo"), 1263 false, // not sensitive 1264 ) 1265 }) 1266 if err := statemgr.WriteAndPersist(statemgr.NewFilesystem("foo", encryption.StateEncryptionDisabled()), fooState, nil); err != nil { 1267 t.Fatal(err) 1268 } 1269 barState := states.BuildState(func(s *states.SyncState) { 1270 s.SetOutputValue( 1271 addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), 1272 cty.StringVal("bar"), 1273 false, // not sensitive 1274 ) 1275 }) 1276 if err := statemgr.WriteAndPersist(statemgr.NewFilesystem("bar", encryption.StateEncryptionDisabled()), barState, nil); err != nil { 1277 t.Fatal(err) 1278 } 1279 1280 ui = new(cli.MockUi) 1281 c = &InitCommand{ 1282 Meta: Meta{ 1283 testingOverrides: metaOverridesForProvider(testProvider()), 1284 Ui: ui, 1285 View: view, 1286 }, 1287 } 1288 1289 args = []string{"-input=false", "-backend-config=path=bar", "-migrate-state"} 1290 if code := c.Run(args); code == 0 { 1291 t.Fatal("init should have failed", ui.OutputWriter) 1292 } 1293 1294 errMsg := ui.ErrorWriter.String() 1295 if !strings.Contains(errMsg, "interactive input is disabled") { 1296 t.Fatal("expected input disabled error, got", errMsg) 1297 } 1298 1299 ui = new(cli.MockUi) 1300 c = &InitCommand{ 1301 Meta: Meta{ 1302 testingOverrides: metaOverridesForProvider(testProvider()), 1303 Ui: ui, 1304 View: view, 1305 }, 1306 } 1307 1308 // A missing input=false should abort rather than loop infinitely 1309 args = []string{"-backend-config=path=baz"} 1310 if code := c.Run(args); code == 0 { 1311 t.Fatal("init should have failed", ui.OutputWriter) 1312 } 1313 } 1314 1315 func TestInit_getProvider(t *testing.T) { 1316 // Create a temporary working directory that is empty 1317 td := t.TempDir() 1318 testCopyDir(t, testFixturePath("init-get-providers"), td) 1319 defer testChdir(t, td)() 1320 1321 overrides := metaOverridesForProvider(testProvider()) 1322 ui := new(cli.MockUi) 1323 view, _ := testView(t) 1324 providerSource, close := newMockProviderSource(t, map[string][]string{ 1325 // looking for an exact version 1326 "exact": {"1.2.3"}, 1327 // config requires >= 2.3.3 1328 "greater-than": {"2.3.4", "2.3.3", "2.3.0"}, 1329 // config specifies 1330 "between": {"3.4.5", "2.3.4", "1.2.3"}, 1331 }) 1332 defer close() 1333 m := Meta{ 1334 testingOverrides: overrides, 1335 Ui: ui, 1336 View: view, 1337 ProviderSource: providerSource, 1338 } 1339 1340 c := &InitCommand{ 1341 Meta: m, 1342 } 1343 1344 args := []string{ 1345 "-backend=false", // should be possible to install plugins without backend init 1346 } 1347 if code := c.Run(args); code != 0 { 1348 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 1349 } 1350 1351 // check that we got the providers for our config 1352 exactPath := fmt.Sprintf(".terraform/providers/registry.opentofu.org/hashicorp/exact/1.2.3/%s", getproviders.CurrentPlatform) 1353 if _, err := os.Stat(exactPath); os.IsNotExist(err) { 1354 t.Fatal("provider 'exact' not downloaded") 1355 } 1356 greaterThanPath := fmt.Sprintf(".terraform/providers/registry.opentofu.org/hashicorp/greater-than/2.3.4/%s", getproviders.CurrentPlatform) 1357 if _, err := os.Stat(greaterThanPath); os.IsNotExist(err) { 1358 t.Fatal("provider 'greater-than' not downloaded") 1359 } 1360 betweenPath := fmt.Sprintf(".terraform/providers/registry.opentofu.org/hashicorp/between/2.3.4/%s", getproviders.CurrentPlatform) 1361 if _, err := os.Stat(betweenPath); os.IsNotExist(err) { 1362 t.Fatal("provider 'between' not downloaded") 1363 } 1364 1365 t.Run("future-state", func(t *testing.T) { 1366 // getting providers should fail if a state from a newer version of 1367 // tofu exists, since InitCommand.getProviders needs to inspect that 1368 // state. 1369 1370 f, err := os.Create(DefaultStateFilename) 1371 if err != nil { 1372 t.Fatalf("err: %s", err) 1373 } 1374 defer f.Close() 1375 1376 // Construct a mock state file from the far future 1377 type FutureState struct { 1378 Version uint `json:"version"` 1379 Lineage string `json:"lineage"` 1380 TerraformVersion string `json:"terraform_version"` 1381 Outputs map[string]interface{} `json:"outputs"` 1382 Resources []map[string]interface{} `json:"resources"` 1383 } 1384 fs := &FutureState{ 1385 Version: 999, 1386 Lineage: "123-456-789", 1387 TerraformVersion: "999.0.0", 1388 Outputs: make(map[string]interface{}), 1389 Resources: make([]map[string]interface{}, 0), 1390 } 1391 src, err := json.MarshalIndent(fs, "", " ") 1392 if err != nil { 1393 t.Fatalf("failed to marshal future state: %s", err) 1394 } 1395 src = append(src, '\n') 1396 _, err = f.Write(src) 1397 if err != nil { 1398 t.Fatal(err) 1399 } 1400 1401 ui := new(cli.MockUi) 1402 view, _ := testView(t) 1403 m.Ui = ui 1404 m.View = view 1405 c := &InitCommand{ 1406 Meta: m, 1407 } 1408 1409 if code := c.Run(nil); code == 0 { 1410 t.Fatal("expected error, got:", ui.OutputWriter) 1411 } 1412 1413 errMsg := ui.ErrorWriter.String() 1414 if !strings.Contains(errMsg, "Unsupported state file format") { 1415 t.Fatal("unexpected error:", errMsg) 1416 } 1417 }) 1418 } 1419 1420 func TestInit_getProviderSource(t *testing.T) { 1421 // Create a temporary working directory that is empty 1422 td := t.TempDir() 1423 testCopyDir(t, testFixturePath("init-get-provider-source"), td) 1424 defer testChdir(t, td)() 1425 1426 overrides := metaOverridesForProvider(testProvider()) 1427 ui := new(cli.MockUi) 1428 view, _ := testView(t) 1429 providerSource, close := newMockProviderSource(t, map[string][]string{ 1430 // looking for an exact version 1431 "acme/alpha": {"1.2.3"}, 1432 // config doesn't specify versions for other providers 1433 "registry.example.com/acme/beta": {"1.0.0"}, 1434 "gamma": {"2.0.0"}, 1435 }) 1436 defer close() 1437 m := Meta{ 1438 testingOverrides: overrides, 1439 Ui: ui, 1440 View: view, 1441 ProviderSource: providerSource, 1442 } 1443 1444 c := &InitCommand{ 1445 Meta: m, 1446 } 1447 1448 args := []string{ 1449 "-backend=false", // should be possible to install plugins without backend init 1450 } 1451 if code := c.Run(args); code != 0 { 1452 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 1453 } 1454 1455 // check that we got the providers for our config 1456 exactPath := fmt.Sprintf(".terraform/providers/registry.opentofu.org/acme/alpha/1.2.3/%s", getproviders.CurrentPlatform) 1457 if _, err := os.Stat(exactPath); os.IsNotExist(err) { 1458 t.Error("provider 'alpha' not downloaded") 1459 } 1460 greaterThanPath := fmt.Sprintf(".terraform/providers/registry.example.com/acme/beta/1.0.0/%s", getproviders.CurrentPlatform) 1461 if _, err := os.Stat(greaterThanPath); os.IsNotExist(err) { 1462 t.Error("provider 'beta' not downloaded") 1463 } 1464 betweenPath := fmt.Sprintf(".terraform/providers/registry.opentofu.org/hashicorp/gamma/2.0.0/%s", getproviders.CurrentPlatform) 1465 if _, err := os.Stat(betweenPath); os.IsNotExist(err) { 1466 t.Error("provider 'gamma' not downloaded") 1467 } 1468 } 1469 1470 func TestInit_getProviderLegacyFromState(t *testing.T) { 1471 // Create a temporary working directory that is empty 1472 td := t.TempDir() 1473 testCopyDir(t, testFixturePath("init-get-provider-legacy-from-state"), td) 1474 defer testChdir(t, td)() 1475 1476 overrides := metaOverridesForProvider(testProvider()) 1477 ui := new(cli.MockUi) 1478 view, _ := testView(t) 1479 providerSource, close := newMockProviderSource(t, map[string][]string{ 1480 "acme/alpha": {"1.2.3"}, 1481 }) 1482 defer close() 1483 m := Meta{ 1484 testingOverrides: overrides, 1485 Ui: ui, 1486 View: view, 1487 ProviderSource: providerSource, 1488 } 1489 1490 c := &InitCommand{ 1491 Meta: m, 1492 } 1493 1494 if code := c.Run(nil); code != 1 { 1495 t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) 1496 } 1497 1498 // Expect this diagnostic output 1499 wants := []string{ 1500 "Invalid legacy provider address", 1501 "You must complete the Terraform 0.13 upgrade process", 1502 } 1503 got := ui.ErrorWriter.String() 1504 for _, want := range wants { 1505 if !strings.Contains(got, want) { 1506 t.Fatalf("expected output to contain %q, got:\n\n%s", want, got) 1507 } 1508 } 1509 } 1510 1511 func TestInit_getProviderInvalidPackage(t *testing.T) { 1512 // Create a temporary working directory that is empty 1513 td := t.TempDir() 1514 testCopyDir(t, testFixturePath("init-get-provider-invalid-package"), td) 1515 defer testChdir(t, td)() 1516 1517 overrides := metaOverridesForProvider(testProvider()) 1518 ui := new(cli.MockUi) 1519 view, _ := testView(t) 1520 1521 // create a provider source which allows installing an invalid package 1522 addr := addrs.MustParseProviderSourceString("invalid/package") 1523 version := getproviders.MustParseVersion("1.0.0") 1524 meta, close, err := getproviders.FakeInstallablePackageMeta( 1525 addr, 1526 version, 1527 getproviders.VersionList{getproviders.MustParseVersion("5.0")}, 1528 getproviders.CurrentPlatform, 1529 "terraform-package", // should be "terraform-provider-package" 1530 ) 1531 defer close() 1532 if err != nil { 1533 t.Fatalf("failed to prepare fake package for %s %s: %s", addr.ForDisplay(), version, err) 1534 } 1535 providerSource := getproviders.NewMockSource([]getproviders.PackageMeta{meta}, nil) 1536 1537 m := Meta{ 1538 testingOverrides: overrides, 1539 Ui: ui, 1540 View: view, 1541 ProviderSource: providerSource, 1542 } 1543 1544 c := &InitCommand{ 1545 Meta: m, 1546 } 1547 1548 args := []string{ 1549 "-backend=false", // should be possible to install plugins without backend init 1550 } 1551 if code := c.Run(args); code != 1 { 1552 t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) 1553 } 1554 1555 // invalid provider should be installed 1556 packagePath := fmt.Sprintf(".terraform/providers/registry.opentofu.org/invalid/package/1.0.0/%s/terraform-package", getproviders.CurrentPlatform) 1557 if _, err := os.Stat(packagePath); os.IsNotExist(err) { 1558 t.Fatal("provider 'invalid/package' not downloaded") 1559 } 1560 1561 wantErrors := []string{ 1562 "Failed to install provider", 1563 "could not find executable file starting with terraform-provider-package", 1564 } 1565 got := ui.ErrorWriter.String() 1566 for _, wantError := range wantErrors { 1567 if !strings.Contains(got, wantError) { 1568 t.Fatalf("missing error:\nwant: %q\ngot:\n%s", wantError, got) 1569 } 1570 } 1571 } 1572 1573 func TestInit_getProviderDetectedLegacy(t *testing.T) { 1574 // Create a temporary working directory that is empty 1575 td := t.TempDir() 1576 testCopyDir(t, testFixturePath("init-get-provider-detected-legacy"), td) 1577 defer testChdir(t, td)() 1578 1579 // We need to construct a multisource with a mock source and a registry 1580 // source: the mock source will return ErrRegistryProviderNotKnown for an 1581 // unknown provider, and the registry source will allow us to look up the 1582 // appropriate namespace if possible. 1583 providerSource, psClose := newMockProviderSource(t, map[string][]string{ 1584 "hashicorp/foo": {"1.2.3"}, 1585 "terraform-providers/baz": {"2.3.4"}, // this will not be installed 1586 }) 1587 defer psClose() 1588 registrySource, rsClose := testRegistrySource(t) 1589 defer rsClose() 1590 multiSource := getproviders.MultiSource{ 1591 {Source: providerSource}, 1592 {Source: registrySource}, 1593 } 1594 1595 ui := new(cli.MockUi) 1596 view, _ := testView(t) 1597 m := Meta{ 1598 Ui: ui, 1599 View: view, 1600 ProviderSource: multiSource, 1601 } 1602 1603 c := &InitCommand{ 1604 Meta: m, 1605 } 1606 1607 args := []string{ 1608 "-backend=false", // should be possible to install plugins without backend init 1609 } 1610 if code := c.Run(args); code == 0 { 1611 t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) 1612 } 1613 1614 // foo should be installed 1615 fooPath := fmt.Sprintf(".terraform/providers/registry.opentofu.org/hashicorp/foo/1.2.3/%s", getproviders.CurrentPlatform) 1616 if _, err := os.Stat(fooPath); os.IsNotExist(err) { 1617 t.Error("provider 'foo' not installed") 1618 } 1619 // baz should not be installed 1620 bazPath := fmt.Sprintf(".terraform/providers/registry.opentofu.org/terraform-providers/baz/2.3.4/%s", getproviders.CurrentPlatform) 1621 if _, err := os.Stat(bazPath); !os.IsNotExist(err) { 1622 t.Error("provider 'baz' installed, but should not be") 1623 } 1624 1625 // error output is the main focus of this test 1626 errOutput := ui.ErrorWriter.String() 1627 errors := []string{ 1628 "Failed to query available provider packages", 1629 "Could not retrieve the list of available versions", 1630 "registry.opentofu.org/hashicorp/baz", 1631 "registry.opentofu.org/hashicorp/frob", 1632 } 1633 for _, want := range errors { 1634 if !strings.Contains(errOutput, want) { 1635 t.Fatalf("expected error %q: %s", want, errOutput) 1636 } 1637 } 1638 } 1639 1640 func TestInit_getProviderDetectedDuplicate(t *testing.T) { 1641 // Create a temporary working directory that is empty 1642 td := t.TempDir() 1643 testCopyDir(t, testFixturePath("init-get-provider-detected-duplicate"), td) 1644 defer testChdir(t, td)() 1645 1646 // We need to construct a multisource with a mock source and a registry 1647 // source: the mock source will return ErrRegistryProviderNotKnown for an 1648 // unknown provider, and the registry source will allow us to look up the 1649 // appropriate namespace if possible. 1650 providerSource, psClose := newMockProviderSource(t, map[string][]string{ 1651 "hashicorp/foo": {"1.2.3"}, 1652 "opentofu/foo": {"1.2.3"}, 1653 "hashicorp/bar": {"1.2.3"}, 1654 }) 1655 defer psClose() 1656 registrySource, rsClose := testRegistrySource(t) 1657 defer rsClose() 1658 multiSource := getproviders.MultiSource{ 1659 {Source: providerSource}, 1660 {Source: registrySource}, 1661 } 1662 1663 ui := new(cli.MockUi) 1664 view, _ := testView(t) 1665 m := Meta{ 1666 Ui: ui, 1667 View: view, 1668 ProviderSource: multiSource, 1669 } 1670 1671 c := &InitCommand{ 1672 Meta: m, 1673 } 1674 1675 args := []string{ 1676 "-backend=false", // should be possible to install plugins without backend init 1677 } 1678 if code := c.Run(args); code != 0 { 1679 t.Fatalf("expected error, got output: \n%s\n%s", ui.OutputWriter.String(), ui.ErrorWriter.String()) 1680 } 1681 1682 // error output is the main focus of this test 1683 errOutput := ui.ErrorWriter.String() 1684 errors := []string{ 1685 "Warning: Potential provider misconfiguration", 1686 "OpenTofu has detected multiple providers of type foo", 1687 "If this is intentional you can ignore this warning", 1688 } 1689 unexpected := []string{ 1690 "OpenTofu has detected multiple providers of type bar", 1691 } 1692 for _, want := range errors { 1693 if !strings.Contains(errOutput, want) { 1694 t.Fatalf("expected error %q: %s", want, errOutput) 1695 } 1696 } 1697 for _, unwanted := range unexpected { 1698 if strings.Contains(errOutput, unwanted) { 1699 t.Fatalf("unexpected error %q: %s", unwanted, errOutput) 1700 } 1701 } 1702 1703 } 1704 1705 func TestInit_providerSource(t *testing.T) { 1706 // Create a temporary working directory that is empty 1707 td := t.TempDir() 1708 testCopyDir(t, testFixturePath("init-required-providers"), td) 1709 defer testChdir(t, td)() 1710 1711 providerSource, close := newMockProviderSource(t, map[string][]string{ 1712 "test": {"1.2.3", "1.2.4"}, 1713 "test-beta": {"1.2.4"}, 1714 "source": {"1.2.2", "1.2.3", "1.2.1"}, 1715 }) 1716 defer close() 1717 1718 ui := cli.NewMockUi() 1719 view, _ := testView(t) 1720 m := Meta{ 1721 testingOverrides: metaOverridesForProvider(testProvider()), 1722 Ui: ui, 1723 View: view, 1724 ProviderSource: providerSource, 1725 } 1726 1727 c := &InitCommand{ 1728 Meta: m, 1729 } 1730 1731 args := []string{} 1732 1733 if code := c.Run(args); code != 0 { 1734 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 1735 } 1736 if strings.Contains(ui.OutputWriter.String(), "OpenTofu has initialized, but configuration upgrades may be needed") { 1737 t.Fatalf("unexpected \"configuration upgrade\" warning in output") 1738 } 1739 1740 cacheDir := m.providerLocalCacheDir() 1741 gotPackages := cacheDir.AllAvailablePackages() 1742 wantPackages := map[addrs.Provider][]providercache.CachedProvider{ 1743 addrs.NewDefaultProvider("test"): { 1744 { 1745 Provider: addrs.NewDefaultProvider("test"), 1746 Version: getproviders.MustParseVersion("1.2.3"), 1747 PackageDir: expectedPackageInstallPath("test", "1.2.3", false), 1748 }, 1749 }, 1750 addrs.NewDefaultProvider("test-beta"): { 1751 { 1752 Provider: addrs.NewDefaultProvider("test-beta"), 1753 Version: getproviders.MustParseVersion("1.2.4"), 1754 PackageDir: expectedPackageInstallPath("test-beta", "1.2.4", false), 1755 }, 1756 }, 1757 addrs.NewDefaultProvider("source"): { 1758 { 1759 Provider: addrs.NewDefaultProvider("source"), 1760 Version: getproviders.MustParseVersion("1.2.3"), 1761 PackageDir: expectedPackageInstallPath("source", "1.2.3", false), 1762 }, 1763 }, 1764 } 1765 if diff := cmp.Diff(wantPackages, gotPackages); diff != "" { 1766 t.Errorf("wrong cache directory contents after upgrade\n%s", diff) 1767 } 1768 1769 locks, err := m.lockedDependencies() 1770 if err != nil { 1771 t.Fatalf("failed to get locked dependencies: %s", err) 1772 } 1773 gotProviderLocks := locks.AllProviders() 1774 wantProviderLocks := map[addrs.Provider]*depsfile.ProviderLock{ 1775 addrs.NewDefaultProvider("test-beta"): depsfile.NewProviderLock( 1776 addrs.NewDefaultProvider("test-beta"), 1777 getproviders.MustParseVersion("1.2.4"), 1778 getproviders.MustParseVersionConstraints("= 1.2.4"), 1779 []getproviders.Hash{ 1780 getproviders.HashScheme1.New("vEthLkqAecdQimaW6JHZ0SBRNtHibLnOb31tX9ZXlcI="), 1781 }, 1782 ), 1783 addrs.NewDefaultProvider("test"): depsfile.NewProviderLock( 1784 addrs.NewDefaultProvider("test"), 1785 getproviders.MustParseVersion("1.2.3"), 1786 getproviders.MustParseVersionConstraints("= 1.2.3"), 1787 []getproviders.Hash{ 1788 getproviders.HashScheme1.New("8CjxaUBuegKZSFnRos39Fs+CS78ax0Dyb7aIA5XBiNI="), 1789 }, 1790 ), 1791 addrs.NewDefaultProvider("source"): depsfile.NewProviderLock( 1792 addrs.NewDefaultProvider("source"), 1793 getproviders.MustParseVersion("1.2.3"), 1794 getproviders.MustParseVersionConstraints("= 1.2.3"), 1795 []getproviders.Hash{ 1796 getproviders.HashScheme1.New("ACYytVQ2Q6JfoEs7xxCqa1yGFf9HwF3SEHzJKBoJfo0="), 1797 }, 1798 ), 1799 } 1800 1801 if diff := cmp.Diff(gotProviderLocks, wantProviderLocks, depsfile.ProviderLockComparer); diff != "" { 1802 t.Errorf("wrong version selections after upgrade\n%s", diff) 1803 } 1804 1805 if got, want := ui.OutputWriter.String(), "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(got, want) { 1806 t.Fatalf("unexpected output: %s\nexpected to include %q", got, want) 1807 } 1808 if got, want := ui.ErrorWriter.String(), "\n - hashicorp/source\n - hashicorp/test\n - hashicorp/test-beta"; !strings.Contains(got, want) { 1809 t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) 1810 } 1811 } 1812 1813 func TestInit_cancelModules(t *testing.T) { 1814 // This test runs `tofu init` as if SIGINT (or similar on other 1815 // platforms) were sent to it, testing that it is interruptible. 1816 1817 td := t.TempDir() 1818 testCopyDir(t, testFixturePath("init-registry-module"), td) 1819 defer testChdir(t, td)() 1820 1821 // Our shutdown channel is pre-closed so init will exit as soon as it 1822 // starts a cancelable portion of the process. 1823 shutdownCh := make(chan struct{}) 1824 close(shutdownCh) 1825 1826 ui := cli.NewMockUi() 1827 view, _ := testView(t) 1828 m := Meta{ 1829 testingOverrides: metaOverridesForProvider(testProvider()), 1830 Ui: ui, 1831 View: view, 1832 ShutdownCh: shutdownCh, 1833 } 1834 1835 c := &InitCommand{ 1836 Meta: m, 1837 } 1838 1839 args := []string{} 1840 1841 if code := c.Run(args); code == 0 { 1842 t.Fatalf("succeeded; wanted error\n%s", ui.OutputWriter.String()) 1843 } 1844 1845 if got, want := ui.ErrorWriter.String(), `Module installation was canceled by an interrupt signal`; !strings.Contains(got, want) { 1846 t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) 1847 } 1848 } 1849 1850 func TestInit_cancelProviders(t *testing.T) { 1851 // This test runs `tofu init` as if SIGINT (or similar on other 1852 // platforms) were sent to it, testing that it is interruptible. 1853 1854 td := t.TempDir() 1855 testCopyDir(t, testFixturePath("init-required-providers"), td) 1856 defer testChdir(t, td)() 1857 1858 // Use a provider source implementation which is designed to hang indefinitely, 1859 // to avoid a race between the closed shutdown channel and the provider source 1860 // operations. 1861 providerSource := &getproviders.HangingSource{} 1862 1863 // Our shutdown channel is pre-closed so init will exit as soon as it 1864 // starts a cancelable portion of the process. 1865 shutdownCh := make(chan struct{}) 1866 close(shutdownCh) 1867 1868 ui := cli.NewMockUi() 1869 view, _ := testView(t) 1870 m := Meta{ 1871 testingOverrides: metaOverridesForProvider(testProvider()), 1872 Ui: ui, 1873 View: view, 1874 ProviderSource: providerSource, 1875 ShutdownCh: shutdownCh, 1876 } 1877 1878 c := &InitCommand{ 1879 Meta: m, 1880 } 1881 1882 args := []string{} 1883 1884 if code := c.Run(args); code == 0 { 1885 t.Fatalf("succeeded; wanted error\n%s", ui.OutputWriter.String()) 1886 } 1887 // Currently the first operation that is cancelable is provider 1888 // installation, so our error message comes from there. If we 1889 // make the earlier steps cancelable in future then it'd be 1890 // expected for this particular message to change. 1891 if got, want := ui.ErrorWriter.String(), `Provider installation was canceled by an interrupt signal`; !strings.Contains(got, want) { 1892 t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) 1893 } 1894 } 1895 1896 func TestInit_getUpgradePlugins(t *testing.T) { 1897 // Create a temporary working directory that is empty 1898 td := t.TempDir() 1899 testCopyDir(t, testFixturePath("init-get-providers"), td) 1900 defer testChdir(t, td)() 1901 1902 providerSource, close := newMockProviderSource(t, map[string][]string{ 1903 // looking for an exact version 1904 "exact": {"1.2.3"}, 1905 // config requires >= 2.3.3 1906 "greater-than": {"2.3.4", "2.3.3", "2.3.0"}, 1907 // config specifies > 1.0.0 , < 3.0.0 1908 "between": {"3.4.5", "2.3.4", "1.2.3"}, 1909 }) 1910 defer close() 1911 1912 ui := new(cli.MockUi) 1913 view, _ := testView(t) 1914 m := Meta{ 1915 testingOverrides: metaOverridesForProvider(testProvider()), 1916 Ui: ui, 1917 View: view, 1918 ProviderSource: providerSource, 1919 } 1920 1921 installFakeProviderPackages(t, &m, map[string][]string{ 1922 "exact": {"0.0.1"}, 1923 "greater-than": {"2.3.3"}, 1924 }) 1925 1926 c := &InitCommand{ 1927 Meta: m, 1928 } 1929 1930 args := []string{ 1931 "-upgrade=true", 1932 } 1933 if code := c.Run(args); code != 0 { 1934 t.Fatalf("command did not complete successfully:\n%s", ui.ErrorWriter.String()) 1935 } 1936 1937 cacheDir := m.providerLocalCacheDir() 1938 gotPackages := cacheDir.AllAvailablePackages() 1939 wantPackages := map[addrs.Provider][]providercache.CachedProvider{ 1940 // "between" wasn't previously installed at all, so we installed 1941 // the newest available version that matched the version constraints. 1942 addrs.NewDefaultProvider("between"): { 1943 { 1944 Provider: addrs.NewDefaultProvider("between"), 1945 Version: getproviders.MustParseVersion("2.3.4"), 1946 PackageDir: expectedPackageInstallPath("between", "2.3.4", false), 1947 }, 1948 }, 1949 // The existing version of "exact" did not match the version constraints, 1950 // so we installed what the configuration selected as well. 1951 addrs.NewDefaultProvider("exact"): { 1952 { 1953 Provider: addrs.NewDefaultProvider("exact"), 1954 Version: getproviders.MustParseVersion("1.2.3"), 1955 PackageDir: expectedPackageInstallPath("exact", "1.2.3", false), 1956 }, 1957 // Previous version is still there, but not selected 1958 { 1959 Provider: addrs.NewDefaultProvider("exact"), 1960 Version: getproviders.MustParseVersion("0.0.1"), 1961 PackageDir: expectedPackageInstallPath("exact", "0.0.1", false), 1962 }, 1963 }, 1964 // The existing version of "greater-than" _did_ match the constraints, 1965 // but a newer version was available and the user specified 1966 // -upgrade and so we upgraded it anyway. 1967 addrs.NewDefaultProvider("greater-than"): { 1968 { 1969 Provider: addrs.NewDefaultProvider("greater-than"), 1970 Version: getproviders.MustParseVersion("2.3.4"), 1971 PackageDir: expectedPackageInstallPath("greater-than", "2.3.4", false), 1972 }, 1973 // Previous version is still there, but not selected 1974 { 1975 Provider: addrs.NewDefaultProvider("greater-than"), 1976 Version: getproviders.MustParseVersion("2.3.3"), 1977 PackageDir: expectedPackageInstallPath("greater-than", "2.3.3", false), 1978 }, 1979 }, 1980 } 1981 if diff := cmp.Diff(wantPackages, gotPackages); diff != "" { 1982 t.Errorf("wrong cache directory contents after upgrade\n%s", diff) 1983 } 1984 1985 locks, err := m.lockedDependencies() 1986 if err != nil { 1987 t.Fatalf("failed to get locked dependencies: %s", err) 1988 } 1989 gotProviderLocks := locks.AllProviders() 1990 wantProviderLocks := map[addrs.Provider]*depsfile.ProviderLock{ 1991 addrs.NewDefaultProvider("between"): depsfile.NewProviderLock( 1992 addrs.NewDefaultProvider("between"), 1993 getproviders.MustParseVersion("2.3.4"), 1994 getproviders.MustParseVersionConstraints("> 1.0.0, < 3.0.0"), 1995 []getproviders.Hash{ 1996 getproviders.HashScheme1.New("ntfa04OlRqIfGL/Gkd+nGMJSHGWyAgMQplFWk7WEsOk="), 1997 }, 1998 ), 1999 addrs.NewDefaultProvider("exact"): depsfile.NewProviderLock( 2000 addrs.NewDefaultProvider("exact"), 2001 getproviders.MustParseVersion("1.2.3"), 2002 getproviders.MustParseVersionConstraints("= 1.2.3"), 2003 []getproviders.Hash{ 2004 getproviders.HashScheme1.New("Xgk+LFrzi9Mop6+d01TCTaD3kgSrUASCAUU1aDsEsJU="), 2005 }, 2006 ), 2007 addrs.NewDefaultProvider("greater-than"): depsfile.NewProviderLock( 2008 addrs.NewDefaultProvider("greater-than"), 2009 getproviders.MustParseVersion("2.3.4"), 2010 getproviders.MustParseVersionConstraints(">= 2.3.3"), 2011 []getproviders.Hash{ 2012 getproviders.HashScheme1.New("8M5DXICmUiVjbkxNNO0zXNsV6duCVNWzq3/Kf0mNIo4="), 2013 }, 2014 ), 2015 } 2016 if diff := cmp.Diff(gotProviderLocks, wantProviderLocks, depsfile.ProviderLockComparer); diff != "" { 2017 t.Errorf("wrong version selections after upgrade\n%s", diff) 2018 } 2019 } 2020 2021 func TestInit_getProviderMissing(t *testing.T) { 2022 // Create a temporary working directory that is empty 2023 td := t.TempDir() 2024 testCopyDir(t, testFixturePath("init-get-providers"), td) 2025 defer testChdir(t, td)() 2026 2027 providerSource, close := newMockProviderSource(t, map[string][]string{ 2028 // looking for exact version 1.2.3 2029 "exact": {"1.2.4"}, 2030 // config requires >= 2.3.3 2031 "greater-than": {"2.3.4", "2.3.3", "2.3.0"}, 2032 // config specifies 2033 "between": {"3.4.5", "2.3.4", "1.2.3"}, 2034 }) 2035 defer close() 2036 2037 ui := new(cli.MockUi) 2038 view, _ := testView(t) 2039 m := Meta{ 2040 testingOverrides: metaOverridesForProvider(testProvider()), 2041 Ui: ui, 2042 View: view, 2043 ProviderSource: providerSource, 2044 } 2045 2046 c := &InitCommand{ 2047 Meta: m, 2048 } 2049 2050 args := []string{} 2051 if code := c.Run(args); code == 0 { 2052 t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) 2053 } 2054 2055 if !strings.Contains(ui.ErrorWriter.String(), "no available releases match") { 2056 t.Fatalf("unexpected error output: %s", ui.ErrorWriter) 2057 } 2058 } 2059 2060 func TestInit_checkRequiredVersion(t *testing.T) { 2061 // Create a temporary working directory that is empty 2062 td := t.TempDir() 2063 testCopyDir(t, testFixturePath("init-check-required-version"), td) 2064 defer testChdir(t, td)() 2065 2066 ui := cli.NewMockUi() 2067 view, _ := testView(t) 2068 c := &InitCommand{ 2069 Meta: Meta{ 2070 testingOverrides: metaOverridesForProvider(testProvider()), 2071 Ui: ui, 2072 View: view, 2073 }, 2074 } 2075 2076 args := []string{} 2077 if code := c.Run(args); code != 1 { 2078 t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) 2079 } 2080 errStr := ui.ErrorWriter.String() 2081 if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { 2082 t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) 2083 } 2084 if strings.Contains(errStr, `required_version = ">= 0.13.0"`) { 2085 t.Fatalf("output should not point to met version constraint, but is:\n\n%s", errStr) 2086 } 2087 } 2088 2089 // Verify that init will error out with an invalid version constraint, even if 2090 // there are other invalid configuration constructs. 2091 func TestInit_checkRequiredVersionFirst(t *testing.T) { 2092 t.Run("root_module", func(t *testing.T) { 2093 td := t.TempDir() 2094 testCopyDir(t, testFixturePath("init-check-required-version-first"), td) 2095 defer testChdir(t, td)() 2096 2097 ui := cli.NewMockUi() 2098 view, _ := testView(t) 2099 c := &InitCommand{ 2100 Meta: Meta{ 2101 testingOverrides: metaOverridesForProvider(testProvider()), 2102 Ui: ui, 2103 View: view, 2104 }, 2105 } 2106 2107 args := []string{} 2108 if code := c.Run(args); code != 1 { 2109 t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) 2110 } 2111 errStr := ui.ErrorWriter.String() 2112 if !strings.Contains(errStr, `Unsupported OpenTofu Core version`) { 2113 t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) 2114 } 2115 }) 2116 t.Run("sub_module", func(t *testing.T) { 2117 td := t.TempDir() 2118 testCopyDir(t, testFixturePath("init-check-required-version-first-module"), td) 2119 defer testChdir(t, td)() 2120 2121 ui := cli.NewMockUi() 2122 view, _ := testView(t) 2123 c := &InitCommand{ 2124 Meta: Meta{ 2125 testingOverrides: metaOverridesForProvider(testProvider()), 2126 Ui: ui, 2127 View: view, 2128 }, 2129 } 2130 2131 args := []string{} 2132 if code := c.Run(args); code != 1 { 2133 t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) 2134 } 2135 errStr := ui.ErrorWriter.String() 2136 if !strings.Contains(errStr, `Unsupported OpenTofu Core version`) { 2137 t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) 2138 } 2139 }) 2140 } 2141 2142 func TestInit_providerLockFile(t *testing.T) { 2143 // Create a temporary working directory that is empty 2144 td := t.TempDir() 2145 testCopyDir(t, testFixturePath("init-provider-lock-file"), td) 2146 // The temporary directory does not have write permission (dr-xr-xr-x) after the copy 2147 defer os.Chmod(td, os.ModePerm) 2148 defer testChdir(t, td)() 2149 2150 providerSource, close := newMockProviderSource(t, map[string][]string{ 2151 "test": {"1.2.3"}, 2152 }) 2153 defer close() 2154 2155 ui := new(cli.MockUi) 2156 view, _ := testView(t) 2157 m := Meta{ 2158 testingOverrides: metaOverridesForProvider(testProvider()), 2159 Ui: ui, 2160 View: view, 2161 ProviderSource: providerSource, 2162 } 2163 2164 c := &InitCommand{ 2165 Meta: m, 2166 } 2167 2168 args := []string{} 2169 if code := c.Run(args); code != 0 { 2170 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 2171 } 2172 2173 lockFile := ".terraform.lock.hcl" 2174 buf, err := os.ReadFile(lockFile) 2175 if err != nil { 2176 t.Fatalf("failed to read dependency lock file %s: %s", lockFile, err) 2177 } 2178 buf = bytes.TrimSpace(buf) 2179 // The hash in here is for the fake package that newMockProviderSource produces 2180 // (so it'll change if newMockProviderSource starts producing different contents) 2181 wantLockFile := strings.TrimSpace(` 2182 # This file is maintained automatically by "tofu init". 2183 # Manual edits may be lost in future updates. 2184 2185 provider "registry.opentofu.org/hashicorp/test" { 2186 version = "1.2.3" 2187 constraints = "1.2.3" 2188 hashes = [ 2189 "h1:8CjxaUBuegKZSFnRos39Fs+CS78ax0Dyb7aIA5XBiNI=", 2190 ] 2191 } 2192 `) 2193 if diff := cmp.Diff(wantLockFile, string(buf)); diff != "" { 2194 t.Errorf("wrong dependency lock file contents\n%s", diff) 2195 } 2196 2197 // Make the local directory read-only, and verify that rerunning init 2198 // succeeds, to ensure that we don't try to rewrite an unchanged lock file 2199 os.Chmod(".", 0555) 2200 if code := c.Run(args); code != 0 { 2201 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 2202 } 2203 } 2204 2205 func TestInit_providerLockFileReadonly(t *testing.T) { 2206 // The hash in here is for the fake package that newMockProviderSource produces 2207 // (so it'll change if newMockProviderSource starts producing different contents) 2208 inputLockFile := strings.TrimSpace(` 2209 # This file is maintained automatically by "tofu init". 2210 # Manual edits may be lost in future updates. 2211 2212 provider "registry.opentofu.org/hashicorp/test" { 2213 version = "1.2.3" 2214 constraints = "1.2.3" 2215 hashes = [ 2216 "zh:6f85a1f747dd09455cd77683c0e06da647d8240461b8b36b304b9056814d91f2", 2217 ] 2218 } 2219 `) 2220 2221 badLockFile := strings.TrimSpace(` 2222 # This file is maintained automatically by "tofu init". 2223 # Manual edits may be lost in future updates. 2224 2225 provider "registry.opentofu.org/hashicorp/test" { 2226 version = "1.2.3" 2227 constraints = "1.2.3" 2228 hashes = [ 2229 "zh:0000000000000000000000000000000000000000000000000000000000000000", 2230 ] 2231 } 2232 `) 2233 2234 updatedLockFile := strings.TrimSpace(` 2235 # This file is maintained automatically by "tofu init". 2236 # Manual edits may be lost in future updates. 2237 2238 provider "registry.opentofu.org/hashicorp/test" { 2239 version = "1.2.3" 2240 constraints = "1.2.3" 2241 hashes = [ 2242 "h1:8CjxaUBuegKZSFnRos39Fs+CS78ax0Dyb7aIA5XBiNI=", 2243 "zh:6f85a1f747dd09455cd77683c0e06da647d8240461b8b36b304b9056814d91f2", 2244 ] 2245 } 2246 `) 2247 2248 emptyUpdatedLockFile := strings.TrimSpace(` 2249 # This file is maintained automatically by "tofu init". 2250 # Manual edits may be lost in future updates. 2251 `) 2252 2253 cases := []struct { 2254 desc string 2255 fixture string 2256 providers map[string][]string 2257 input string 2258 args []string 2259 ok bool 2260 want string 2261 }{ 2262 { 2263 desc: "default", 2264 fixture: "init-provider-lock-file", 2265 providers: map[string][]string{"test": {"1.2.3"}}, 2266 input: inputLockFile, 2267 args: []string{}, 2268 ok: true, 2269 want: updatedLockFile, 2270 }, 2271 { 2272 desc: "unused provider", 2273 fixture: "init-provider-now-unused", 2274 providers: map[string][]string{"test": {"1.2.3"}}, 2275 input: inputLockFile, 2276 args: []string{}, 2277 ok: true, 2278 want: emptyUpdatedLockFile, 2279 }, 2280 { 2281 desc: "readonly", 2282 fixture: "init-provider-lock-file", 2283 providers: map[string][]string{"test": {"1.2.3"}}, 2284 input: inputLockFile, 2285 args: []string{"-lockfile=readonly"}, 2286 ok: true, 2287 want: inputLockFile, 2288 }, 2289 { 2290 desc: "unused provider readonly", 2291 fixture: "init-provider-now-unused", 2292 providers: map[string][]string{"test": {"1.2.3"}}, 2293 input: inputLockFile, 2294 args: []string{"-lockfile=readonly"}, 2295 ok: false, 2296 want: inputLockFile, 2297 }, 2298 { 2299 desc: "conflict", 2300 fixture: "init-provider-lock-file", 2301 providers: map[string][]string{"test": {"1.2.3"}}, 2302 input: inputLockFile, 2303 args: []string{"-lockfile=readonly", "-upgrade"}, 2304 ok: false, 2305 want: inputLockFile, 2306 }, 2307 { 2308 desc: "checksum mismatch", 2309 fixture: "init-provider-lock-file", 2310 providers: map[string][]string{"test": {"1.2.3"}}, 2311 input: badLockFile, 2312 args: []string{"-lockfile=readonly"}, 2313 ok: false, 2314 want: badLockFile, 2315 }, 2316 { 2317 desc: "reject to change required provider dependences", 2318 fixture: "init-provider-lock-file-readonly-add", 2319 providers: map[string][]string{ 2320 "test": {"1.2.3"}, 2321 "foo": {"1.0.0"}, 2322 }, 2323 input: inputLockFile, 2324 args: []string{"-lockfile=readonly"}, 2325 ok: false, 2326 want: inputLockFile, 2327 }, 2328 } 2329 2330 for _, tc := range cases { 2331 t.Run(tc.desc, func(t *testing.T) { 2332 // Create a temporary working directory that is empty 2333 td := t.TempDir() 2334 testCopyDir(t, testFixturePath(tc.fixture), td) 2335 defer testChdir(t, td)() 2336 2337 providerSource, close := newMockProviderSource(t, tc.providers) 2338 defer close() 2339 2340 ui := new(cli.MockUi) 2341 m := Meta{ 2342 testingOverrides: metaOverridesForProvider(testProvider()), 2343 Ui: ui, 2344 ProviderSource: providerSource, 2345 } 2346 2347 c := &InitCommand{ 2348 Meta: m, 2349 } 2350 2351 //write input lockfile 2352 lockFile := ".terraform.lock.hcl" 2353 if err := os.WriteFile(lockFile, []byte(tc.input), 0644); err != nil { 2354 t.Fatalf("failed to write input lockfile: %s", err) 2355 } 2356 2357 code := c.Run(tc.args) 2358 if tc.ok && code != 0 { 2359 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 2360 } 2361 if !tc.ok && code == 0 { 2362 t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) 2363 } 2364 2365 buf, err := os.ReadFile(lockFile) 2366 if err != nil { 2367 t.Fatalf("failed to read dependency lock file %s: %s", lockFile, err) 2368 } 2369 buf = bytes.TrimSpace(buf) 2370 if diff := cmp.Diff(tc.want, string(buf)); diff != "" { 2371 t.Errorf("wrong dependency lock file contents\n%s", diff) 2372 } 2373 }) 2374 } 2375 } 2376 2377 func TestInit_pluginDirReset(t *testing.T) { 2378 td := testTempDir(t) 2379 defer os.RemoveAll(td) 2380 defer testChdir(t, td)() 2381 2382 // An empty provider source 2383 providerSource, close := newMockProviderSource(t, nil) 2384 defer close() 2385 2386 ui := new(cli.MockUi) 2387 view, _ := testView(t) 2388 c := &InitCommand{ 2389 Meta: Meta{ 2390 testingOverrides: metaOverridesForProvider(testProvider()), 2391 Ui: ui, 2392 View: view, 2393 ProviderSource: providerSource, 2394 }, 2395 } 2396 2397 // make our vendor paths 2398 pluginPath := []string{"a", "b", "c"} 2399 for _, p := range pluginPath { 2400 if err := os.MkdirAll(p, 0755); err != nil { 2401 t.Fatal(err) 2402 } 2403 } 2404 2405 // run once and save the -plugin-dir 2406 args := []string{"-plugin-dir", "a"} 2407 if code := c.Run(args); code != 0 { 2408 t.Fatalf("bad: \n%s", ui.ErrorWriter) 2409 } 2410 2411 pluginDirs, err := c.loadPluginPath() 2412 if err != nil { 2413 t.Fatal(err) 2414 } 2415 2416 if len(pluginDirs) != 1 || pluginDirs[0] != "a" { 2417 t.Fatalf(`expected plugin dir ["a"], got %q`, pluginDirs) 2418 } 2419 2420 ui = new(cli.MockUi) 2421 c = &InitCommand{ 2422 Meta: Meta{ 2423 testingOverrides: metaOverridesForProvider(testProvider()), 2424 Ui: ui, 2425 View: view, 2426 ProviderSource: providerSource, // still empty 2427 }, 2428 } 2429 2430 // make sure we remove the plugin-dir record 2431 args = []string{"-plugin-dir="} 2432 if code := c.Run(args); code != 0 { 2433 t.Fatalf("bad: \n%s", ui.ErrorWriter) 2434 } 2435 2436 pluginDirs, err = c.loadPluginPath() 2437 if err != nil { 2438 t.Fatal(err) 2439 } 2440 2441 if len(pluginDirs) != 0 { 2442 t.Fatalf("expected no plugin dirs got %q", pluginDirs) 2443 } 2444 } 2445 2446 // Test user-supplied -plugin-dir 2447 func TestInit_pluginDirProviders(t *testing.T) { 2448 td := t.TempDir() 2449 testCopyDir(t, testFixturePath("init-get-providers"), td) 2450 defer testChdir(t, td)() 2451 2452 // An empty provider source 2453 providerSource, close := newMockProviderSource(t, nil) 2454 defer close() 2455 2456 ui := new(cli.MockUi) 2457 view, _ := testView(t) 2458 m := Meta{ 2459 testingOverrides: metaOverridesForProvider(testProvider()), 2460 Ui: ui, 2461 View: view, 2462 ProviderSource: providerSource, 2463 } 2464 2465 c := &InitCommand{ 2466 Meta: m, 2467 } 2468 2469 // make our vendor paths 2470 pluginPath := []string{"a", "b", "c"} 2471 for _, p := range pluginPath { 2472 if err := os.MkdirAll(p, 0755); err != nil { 2473 t.Fatal(err) 2474 } 2475 } 2476 2477 // We'll put some providers in our plugin dirs. To do this, we'll pretend 2478 // for a moment that they are provider cache directories just because that 2479 // allows us to lean on our existing test helper functions to do this. 2480 for i, def := range [][]string{ 2481 {"exact", "1.2.3"}, 2482 {"greater-than", "2.3.4"}, 2483 {"between", "2.3.4"}, 2484 } { 2485 name, version := def[0], def[1] 2486 dir := providercache.NewDir(pluginPath[i]) 2487 installFakeProviderPackagesElsewhere(t, dir, map[string][]string{ 2488 name: {version}, 2489 }) 2490 } 2491 2492 args := []string{ 2493 "-plugin-dir", "a", 2494 "-plugin-dir", "b", 2495 "-plugin-dir", "c", 2496 } 2497 if code := c.Run(args); code != 0 { 2498 t.Fatalf("bad: \n%s", ui.ErrorWriter) 2499 } 2500 2501 locks, err := m.lockedDependencies() 2502 if err != nil { 2503 t.Fatalf("failed to get locked dependencies: %s", err) 2504 } 2505 gotProviderLocks := locks.AllProviders() 2506 wantProviderLocks := map[addrs.Provider]*depsfile.ProviderLock{ 2507 addrs.NewDefaultProvider("between"): depsfile.NewProviderLock( 2508 addrs.NewDefaultProvider("between"), 2509 getproviders.MustParseVersion("2.3.4"), 2510 getproviders.MustParseVersionConstraints("> 1.0.0, < 3.0.0"), 2511 []getproviders.Hash{ 2512 getproviders.HashScheme1.New("ntfa04OlRqIfGL/Gkd+nGMJSHGWyAgMQplFWk7WEsOk="), 2513 }, 2514 ), 2515 addrs.NewDefaultProvider("exact"): depsfile.NewProviderLock( 2516 addrs.NewDefaultProvider("exact"), 2517 getproviders.MustParseVersion("1.2.3"), 2518 getproviders.MustParseVersionConstraints("= 1.2.3"), 2519 []getproviders.Hash{ 2520 getproviders.HashScheme1.New("Xgk+LFrzi9Mop6+d01TCTaD3kgSrUASCAUU1aDsEsJU="), 2521 }, 2522 ), 2523 addrs.NewDefaultProvider("greater-than"): depsfile.NewProviderLock( 2524 addrs.NewDefaultProvider("greater-than"), 2525 getproviders.MustParseVersion("2.3.4"), 2526 getproviders.MustParseVersionConstraints(">= 2.3.3"), 2527 []getproviders.Hash{ 2528 getproviders.HashScheme1.New("8M5DXICmUiVjbkxNNO0zXNsV6duCVNWzq3/Kf0mNIo4="), 2529 }, 2530 ), 2531 } 2532 if diff := cmp.Diff(gotProviderLocks, wantProviderLocks, depsfile.ProviderLockComparer); diff != "" { 2533 t.Errorf("wrong version selections after upgrade\n%s", diff) 2534 } 2535 2536 // -plugin-dir overrides the normal provider source, so it should not have 2537 // seen any calls at all. 2538 if calls := providerSource.CallLog(); len(calls) > 0 { 2539 t.Errorf("unexpected provider source calls (want none)\n%s", spew.Sdump(calls)) 2540 } 2541 } 2542 2543 // Test user-supplied -plugin-dir doesn't allow auto-install 2544 func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { 2545 td := t.TempDir() 2546 testCopyDir(t, testFixturePath("init-get-providers"), td) 2547 defer testChdir(t, td)() 2548 2549 // Our provider source has a suitable package for "between" available, 2550 // but we should ignore it because -plugin-dir is set and thus this 2551 // source is temporarily overridden during install. 2552 providerSource, close := newMockProviderSource(t, map[string][]string{ 2553 "between": {"2.3.4"}, 2554 }) 2555 defer close() 2556 2557 ui := cli.NewMockUi() 2558 view, _ := testView(t) 2559 m := Meta{ 2560 testingOverrides: metaOverridesForProvider(testProvider()), 2561 Ui: ui, 2562 View: view, 2563 ProviderSource: providerSource, 2564 } 2565 2566 c := &InitCommand{ 2567 Meta: m, 2568 } 2569 2570 // make our vendor paths 2571 pluginPath := []string{"a", "b"} 2572 for _, p := range pluginPath { 2573 if err := os.MkdirAll(p, 0755); err != nil { 2574 t.Fatal(err) 2575 } 2576 } 2577 2578 // We'll put some providers in our plugin dirs. To do this, we'll pretend 2579 // for a moment that they are provider cache directories just because that 2580 // allows us to lean on our existing test helper functions to do this. 2581 for i, def := range [][]string{ 2582 {"exact", "1.2.3"}, 2583 {"greater-than", "2.3.4"}, 2584 } { 2585 name, version := def[0], def[1] 2586 dir := providercache.NewDir(pluginPath[i]) 2587 installFakeProviderPackagesElsewhere(t, dir, map[string][]string{ 2588 name: {version}, 2589 }) 2590 } 2591 2592 args := []string{ 2593 "-plugin-dir", "a", 2594 "-plugin-dir", "b", 2595 } 2596 if code := c.Run(args); code == 0 { 2597 // should have been an error 2598 t.Fatalf("succeeded; want error\nstdout:\n%s\nstderr\n%s", ui.OutputWriter, ui.ErrorWriter) 2599 } 2600 2601 // The error output should mention the "between" provider but should not 2602 // mention either the "exact" or "greater-than" provider, because the 2603 // latter two are available via the -plugin-dir directories. 2604 errStr := ui.ErrorWriter.String() 2605 if subStr := "hashicorp/between"; !strings.Contains(errStr, subStr) { 2606 t.Errorf("error output should mention the 'between' provider\nwant substr: %s\ngot:\n%s", subStr, errStr) 2607 } 2608 if subStr := "hashicorp/exact"; strings.Contains(errStr, subStr) { 2609 t.Errorf("error output should not mention the 'exact' provider\ndo not want substr: %s\ngot:\n%s", subStr, errStr) 2610 } 2611 if subStr := "hashicorp/greater-than"; strings.Contains(errStr, subStr) { 2612 t.Errorf("error output should not mention the 'greater-than' provider\ndo not want substr: %s\ngot:\n%s", subStr, errStr) 2613 } 2614 2615 if calls := providerSource.CallLog(); len(calls) > 0 { 2616 t.Errorf("unexpected provider source calls (want none)\n%s", spew.Sdump(calls)) 2617 } 2618 } 2619 2620 // Verify that plugin-dir doesn't prevent discovery of internal providers 2621 func TestInit_pluginDirWithBuiltIn(t *testing.T) { 2622 td := t.TempDir() 2623 testCopyDir(t, testFixturePath("init-internal"), td) 2624 defer testChdir(t, td)() 2625 2626 // An empty provider source 2627 providerSource, close := newMockProviderSource(t, nil) 2628 defer close() 2629 2630 ui := cli.NewMockUi() 2631 view, _ := testView(t) 2632 m := Meta{ 2633 testingOverrides: metaOverridesForProvider(testProvider()), 2634 Ui: ui, 2635 View: view, 2636 ProviderSource: providerSource, 2637 } 2638 2639 c := &InitCommand{ 2640 Meta: m, 2641 } 2642 2643 args := []string{"-plugin-dir", "./"} 2644 if code := c.Run(args); code != 0 { 2645 t.Fatalf("error: %s", ui.ErrorWriter) 2646 } 2647 2648 outputStr := ui.OutputWriter.String() 2649 if subStr := "terraform.io/builtin/terraform is built in to OpenTofu"; !strings.Contains(outputStr, subStr) { 2650 t.Errorf("output should mention the tofu provider\nwant substr: %s\ngot:\n%s", subStr, outputStr) 2651 } 2652 } 2653 2654 func TestInit_invalidBuiltInProviders(t *testing.T) { 2655 // This test fixture includes two invalid provider dependencies: 2656 // - an implied dependency on terraform.io/builtin/terraform with an 2657 // explicit version number, which is not allowed because it's builtin. 2658 // - an explicit dependency on terraform.io/builtin/nonexist, which does 2659 // not exist at all. 2660 td := t.TempDir() 2661 testCopyDir(t, testFixturePath("init-internal-invalid"), td) 2662 defer testChdir(t, td)() 2663 2664 // An empty provider source 2665 providerSource, close := newMockProviderSource(t, nil) 2666 defer close() 2667 2668 ui := cli.NewMockUi() 2669 view, _ := testView(t) 2670 m := Meta{ 2671 testingOverrides: metaOverridesForProvider(testProvider()), 2672 Ui: ui, 2673 View: view, 2674 ProviderSource: providerSource, 2675 } 2676 2677 c := &InitCommand{ 2678 Meta: m, 2679 } 2680 2681 if code := c.Run(nil); code == 0 { 2682 t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) 2683 } 2684 2685 errStr := ui.ErrorWriter.String() 2686 if subStr := "Cannot use terraform.io/builtin/terraform: built-in"; !strings.Contains(errStr, subStr) { 2687 t.Errorf("error output should mention the terraform provider\nwant substr: %s\ngot:\n%s", subStr, errStr) 2688 } 2689 if subStr := "Cannot use terraform.io/builtin/nonexist: this OpenTofu release"; !strings.Contains(errStr, subStr) { 2690 t.Errorf("error output should mention the 'nonexist' provider\nwant substr: %s\ngot:\n%s", subStr, errStr) 2691 } 2692 } 2693 2694 func TestInit_invalidSyntaxNoBackend(t *testing.T) { 2695 td := t.TempDir() 2696 testCopyDir(t, testFixturePath("init-syntax-invalid-no-backend"), td) 2697 defer testChdir(t, td)() 2698 2699 ui := cli.NewMockUi() 2700 view, _ := testView(t) 2701 m := Meta{ 2702 Ui: ui, 2703 View: view, 2704 } 2705 2706 c := &InitCommand{ 2707 Meta: m, 2708 } 2709 2710 if code := c.Run(nil); code == 0 { 2711 t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) 2712 } 2713 2714 errStr := ui.ErrorWriter.String() 2715 if subStr := "OpenTofu encountered problems during initialization, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { 2716 t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) 2717 } 2718 if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { 2719 t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) 2720 } 2721 } 2722 2723 func TestInit_invalidSyntaxWithBackend(t *testing.T) { 2724 td := t.TempDir() 2725 testCopyDir(t, testFixturePath("init-syntax-invalid-with-backend"), td) 2726 defer testChdir(t, td)() 2727 2728 ui := cli.NewMockUi() 2729 view, _ := testView(t) 2730 m := Meta{ 2731 Ui: ui, 2732 View: view, 2733 } 2734 2735 c := &InitCommand{ 2736 Meta: m, 2737 } 2738 2739 if code := c.Run(nil); code == 0 { 2740 t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) 2741 } 2742 2743 errStr := ui.ErrorWriter.String() 2744 if subStr := "OpenTofu encountered problems during initialization, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { 2745 t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) 2746 } 2747 if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { 2748 t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) 2749 } 2750 } 2751 2752 func TestInit_invalidSyntaxInvalidBackend(t *testing.T) { 2753 td := t.TempDir() 2754 testCopyDir(t, testFixturePath("init-syntax-invalid-backend-invalid"), td) 2755 defer testChdir(t, td)() 2756 2757 ui := cli.NewMockUi() 2758 view, _ := testView(t) 2759 m := Meta{ 2760 Ui: ui, 2761 View: view, 2762 } 2763 2764 c := &InitCommand{ 2765 Meta: m, 2766 } 2767 2768 if code := c.Run(nil); code == 0 { 2769 t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) 2770 } 2771 2772 errStr := ui.ErrorWriter.String() 2773 if subStr := "OpenTofu encountered problems during initialization, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { 2774 t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) 2775 } 2776 if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { 2777 t.Errorf("Error output should mention syntax errors\nwant substr: %s\ngot:\n%s", subStr, errStr) 2778 } 2779 if subStr := "Error: Unsupported backend type"; !strings.Contains(errStr, subStr) { 2780 t.Errorf("Error output should mention the invalid backend\nwant substr: %s\ngot:\n%s", subStr, errStr) 2781 } 2782 } 2783 2784 func TestInit_invalidSyntaxBackendAttribute(t *testing.T) { 2785 td := t.TempDir() 2786 testCopyDir(t, testFixturePath("init-syntax-invalid-backend-attribute-invalid"), td) 2787 defer testChdir(t, td)() 2788 2789 ui := cli.NewMockUi() 2790 view, _ := testView(t) 2791 m := Meta{ 2792 Ui: ui, 2793 View: view, 2794 } 2795 2796 c := &InitCommand{ 2797 Meta: m, 2798 } 2799 2800 if code := c.Run(nil); code == 0 { 2801 t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) 2802 } 2803 2804 errStr := ui.ErrorWriter.String() 2805 if subStr := "OpenTofu encountered problems during initialization, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { 2806 t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) 2807 } 2808 if subStr := "Error: Invalid character"; !strings.Contains(errStr, subStr) { 2809 t.Errorf("Error output should mention the invalid character\nwant substr: %s\ngot:\n%s", subStr, errStr) 2810 } 2811 if subStr := "Error: Invalid expression"; !strings.Contains(errStr, subStr) { 2812 t.Errorf("Error output should mention the invalid expression\nwant substr: %s\ngot:\n%s", subStr, errStr) 2813 } 2814 } 2815 2816 func TestInit_tests(t *testing.T) { 2817 // Create a temporary working directory that is empty 2818 td := t.TempDir() 2819 testCopyDir(t, testFixturePath("init-with-tests"), td) 2820 defer testChdir(t, td)() 2821 2822 provider := applyFixtureProvider() // We just want the types from this provider. 2823 2824 providerSource, close := newMockProviderSource(t, map[string][]string{ 2825 "hashicorp/test": {"1.0.0"}, 2826 }) 2827 defer close() 2828 2829 ui := new(cli.MockUi) 2830 view, _ := testView(t) 2831 c := &InitCommand{ 2832 Meta: Meta{ 2833 testingOverrides: metaOverridesForProvider(provider), 2834 Ui: ui, 2835 View: view, 2836 ProviderSource: providerSource, 2837 }, 2838 } 2839 2840 args := []string{} 2841 if code := c.Run(args); code != 0 { 2842 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 2843 } 2844 } 2845 2846 func TestInit_testsWithProvider(t *testing.T) { 2847 // Create a temporary working directory that is empty 2848 td := t.TempDir() 2849 testCopyDir(t, testFixturePath("init-with-tests-with-provider"), td) 2850 defer testChdir(t, td)() 2851 2852 provider := applyFixtureProvider() // We just want the types from this provider. 2853 2854 providerSource, close := newMockProviderSource(t, map[string][]string{ 2855 "hashicorp/test": {"1.0.0"}, 2856 }) 2857 defer close() 2858 2859 ui := new(cli.MockUi) 2860 view, _ := testView(t) 2861 c := &InitCommand{ 2862 Meta: Meta{ 2863 testingOverrides: metaOverridesForProvider(provider), 2864 Ui: ui, 2865 View: view, 2866 ProviderSource: providerSource, 2867 }, 2868 } 2869 2870 args := []string{} 2871 if code := c.Run(args); code == 0 { 2872 t.Fatalf("expected failure but got: \n%s", ui.OutputWriter.String()) 2873 } 2874 2875 got := ui.ErrorWriter.String() 2876 want := ` 2877 Error: Failed to resolve provider packages 2878 2879 Could not resolve provider hashicorp/test: no available releases match the 2880 given constraints 1.0.1, 1.0.2 2881 2882 ` 2883 if diff := cmp.Diff(got, want); len(diff) > 0 { 2884 t.Fatalf("wrong error message: \ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) 2885 } 2886 } 2887 2888 func TestInit_testsWithModule(t *testing.T) { 2889 // Create a temporary working directory that is empty 2890 td := t.TempDir() 2891 testCopyDir(t, testFixturePath("init-with-tests-with-module"), td) 2892 defer testChdir(t, td)() 2893 2894 provider := applyFixtureProvider() // We just want the types from this provider. 2895 2896 providerSource, close := newMockProviderSource(t, map[string][]string{ 2897 "hashicorp/test": {"1.0.0"}, 2898 }) 2899 defer close() 2900 2901 ui := new(cli.MockUi) 2902 view, _ := testView(t) 2903 c := &InitCommand{ 2904 Meta: Meta{ 2905 testingOverrides: metaOverridesForProvider(provider), 2906 Ui: ui, 2907 View: view, 2908 ProviderSource: providerSource, 2909 }, 2910 } 2911 2912 args := []string{} 2913 if code := c.Run(args); code != 0 { 2914 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 2915 } 2916 2917 // Check output 2918 output := ui.OutputWriter.String() 2919 if !strings.Contains(output, "test.main.setup in setup") { 2920 t.Fatalf("doesn't look like we installed the test module': %s", output) 2921 } 2922 } 2923 2924 // newMockProviderSource is a helper to succinctly construct a mock provider 2925 // source that contains a set of packages matching the given provider versions 2926 // that are available for installation (from temporary local files). 2927 // 2928 // The caller must call the returned close callback once the source is no 2929 // longer needed, at which point it will clean up all of the temporary files 2930 // and the packages in the source will no longer be available for installation. 2931 // 2932 // Provider addresses must be valid source strings, and passing only the 2933 // provider name will be interpreted as a "default" provider under 2934 // registry.opentofu.org/hashicorp. If you need more control over the 2935 // provider addresses, pass a full provider source string. 2936 // 2937 // This function also registers providers as belonging to the current platform, 2938 // to ensure that they will be available to a provider installer operating in 2939 // its default configuration. 2940 // 2941 // In case of any errors while constructing the source, this function will 2942 // abort the current test using the given testing.T. Therefore a caller can 2943 // assume that if this function returns then the result is valid and ready 2944 // to use. 2945 func newMockProviderSource(t *testing.T, availableProviderVersions map[string][]string) (source *getproviders.MockSource, close func()) { 2946 t.Helper() 2947 var packages []getproviders.PackageMeta 2948 var closes []func() 2949 close = func() { 2950 for _, f := range closes { 2951 f() 2952 } 2953 } 2954 for source, versions := range availableProviderVersions { 2955 addr := addrs.MustParseProviderSourceString(source) 2956 for _, versionStr := range versions { 2957 version, err := getproviders.ParseVersion(versionStr) 2958 if err != nil { 2959 close() 2960 t.Fatalf("failed to parse %q as a version number for %q: %s", versionStr, addr.ForDisplay(), err) 2961 } 2962 meta, close, err := getproviders.FakeInstallablePackageMeta(addr, version, getproviders.VersionList{getproviders.MustParseVersion("5.0")}, getproviders.CurrentPlatform, "") 2963 if err != nil { 2964 close() 2965 t.Fatalf("failed to prepare fake package for %s %s: %s", addr.ForDisplay(), versionStr, err) 2966 } 2967 closes = append(closes, close) 2968 packages = append(packages, meta) 2969 } 2970 } 2971 2972 return getproviders.NewMockSource(packages, nil), close 2973 } 2974 2975 // installFakeProviderPackages installs a fake package for the given provider 2976 // names (interpreted as a "default" provider address) and versions into the 2977 // local plugin cache for the given "meta". 2978 // 2979 // Any test using this must be using testChdir or some similar mechanism to 2980 // make sure that it isn't writing directly into a test fixture or source 2981 // directory within the codebase. 2982 // 2983 // If a requested package cannot be installed for some reason, this function 2984 // will abort the test using the given testing.T. Therefore if this function 2985 // returns the caller can assume that the requested providers have been 2986 // installed. 2987 func installFakeProviderPackages(t *testing.T, meta *Meta, providerVersions map[string][]string) { 2988 t.Helper() 2989 2990 cacheDir := meta.providerLocalCacheDir() 2991 installFakeProviderPackagesElsewhere(t, cacheDir, providerVersions) 2992 } 2993 2994 // installFakeProviderPackagesElsewhere is a variant of installFakeProviderPackages 2995 // that will install packages into the given provider cache directory, rather 2996 // than forcing the use of the local cache of the current "Meta". 2997 func installFakeProviderPackagesElsewhere(t *testing.T, cacheDir *providercache.Dir, providerVersions map[string][]string) { 2998 t.Helper() 2999 3000 // It can be hard to spot the mistake of forgetting to run testChdir before 3001 // modifying the working directory, so we'll use a simple heuristic here 3002 // to try to detect that mistake and make a noisy error about it instead. 3003 wd, err := os.Getwd() 3004 if err == nil { 3005 wd = filepath.Clean(wd) 3006 // If the directory we're in is named "command" or if we're under a 3007 // directory named "testdata" then we'll assume a mistake and generate 3008 // an error. This will cause the test to fail but won't block it from 3009 // running. 3010 if filepath.Base(wd) == "command" || filepath.Base(wd) == "testdata" || strings.Contains(filepath.ToSlash(wd), "/testdata/") { 3011 t.Errorf("installFakeProviderPackage may be used only by tests that switch to a temporary working directory, e.g. using testChdir") 3012 } 3013 } 3014 3015 for name, versions := range providerVersions { 3016 addr := addrs.NewDefaultProvider(name) 3017 for _, versionStr := range versions { 3018 version, err := getproviders.ParseVersion(versionStr) 3019 if err != nil { 3020 t.Fatalf("failed to parse %q as a version number for %q: %s", versionStr, name, err) 3021 } 3022 meta, close, err := getproviders.FakeInstallablePackageMeta(addr, version, getproviders.VersionList{getproviders.MustParseVersion("5.0")}, getproviders.CurrentPlatform, "") 3023 // We're going to install all these fake packages before we return, 3024 // so we don't need to preserve them afterwards. 3025 defer close() 3026 if err != nil { 3027 t.Fatalf("failed to prepare fake package for %s %s: %s", name, versionStr, err) 3028 } 3029 _, err = cacheDir.InstallPackage(context.Background(), meta, nil) 3030 if err != nil { 3031 t.Fatalf("failed to install fake package for %s %s: %s", name, versionStr, err) 3032 } 3033 } 3034 } 3035 } 3036 3037 // expectedPackageInstallPath is a companion to installFakeProviderPackages 3038 // that returns the path where the provider with the given name and version 3039 // would be installed and, relatedly, where the installer will expect to 3040 // find an already-installed version. 3041 // 3042 // Just as with installFakeProviderPackages, this function is a shortcut helper 3043 // for "default-namespaced" providers as we commonly use in tests. If you need 3044 // more control over the provider addresses, use functions of the underlying 3045 // getproviders and providercache packages instead. 3046 // 3047 // The result always uses forward slashes, even on Windows, for consistency 3048 // with how the getproviders and providercache packages build paths. 3049 func expectedPackageInstallPath(name, version string, exe bool) string { 3050 platform := getproviders.CurrentPlatform 3051 baseDir := ".terraform/providers" 3052 if exe { 3053 p := fmt.Sprintf("registry.opentofu.org/hashicorp/%s/%s/%s/terraform-provider-%s_%s", name, version, platform, name, version) 3054 if platform.OS == "windows" { 3055 p += ".exe" 3056 } 3057 return filepath.ToSlash(filepath.Join(baseDir, p)) 3058 } 3059 return filepath.ToSlash(filepath.Join( 3060 baseDir, fmt.Sprintf("registry.opentofu.org/hashicorp/%s/%s/%s", name, version, platform), 3061 )) 3062 }