github.com/hugorut/terraform@v1.1.3/src/command/command_test.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/md5" 7 "encoding/base64" 8 "encoding/json" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net/http" 13 "net/http/httptest" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "strings" 18 "syscall" 19 "testing" 20 21 svchost "github.com/hashicorp/terraform-svchost" 22 23 "github.com/hashicorp/terraform-svchost/disco" 24 "github.com/hugorut/terraform/src/addrs" 25 backendInit "github.com/hugorut/terraform/src/backend/init" 26 backendLocal "github.com/hugorut/terraform/src/backend/local" 27 "github.com/hugorut/terraform/src/command/views" 28 "github.com/hugorut/terraform/src/command/workdir" 29 "github.com/hugorut/terraform/src/configs" 30 "github.com/hugorut/terraform/src/configs/configload" 31 "github.com/hugorut/terraform/src/configs/configschema" 32 "github.com/hugorut/terraform/src/copy" 33 "github.com/hugorut/terraform/src/depsfile" 34 "github.com/hugorut/terraform/src/getproviders" 35 "github.com/hugorut/terraform/src/initwd" 36 legacy "github.com/hugorut/terraform/src/legacy/terraform" 37 _ "github.com/hugorut/terraform/src/logging" 38 "github.com/hugorut/terraform/src/plans" 39 "github.com/hugorut/terraform/src/plans/planfile" 40 "github.com/hugorut/terraform/src/providers" 41 "github.com/hugorut/terraform/src/registry" 42 "github.com/hugorut/terraform/src/states" 43 "github.com/hugorut/terraform/src/states/statefile" 44 "github.com/hugorut/terraform/src/states/statemgr" 45 "github.com/hugorut/terraform/src/terminal" 46 "github.com/hugorut/terraform/src/terraform" 47 "github.com/hugorut/terraform/version" 48 "github.com/zclconf/go-cty/cty" 49 ) 50 51 // These are the directories for our test data and fixtures. 52 var ( 53 fixtureDir = "./testdata" 54 testDataDir = "./testdata" 55 ) 56 57 // a top level temp directory which will be cleaned after all tests 58 var testingDir string 59 60 func init() { 61 test = true 62 63 // Initialize the backends 64 backendInit.Init(nil) 65 66 // Expand the data and fixture dirs on init because 67 // we change the working directory in some tests. 68 var err error 69 fixtureDir, err = filepath.Abs(fixtureDir) 70 if err != nil { 71 panic(err) 72 } 73 74 testDataDir, err = filepath.Abs(testDataDir) 75 if err != nil { 76 panic(err) 77 } 78 79 testingDir, err = ioutil.TempDir(testingDir, "tf") 80 if err != nil { 81 panic(err) 82 } 83 } 84 85 func TestMain(m *testing.M) { 86 defer os.RemoveAll(testingDir) 87 88 // Make sure backend init is initialized, since our tests tend to assume it. 89 backendInit.Init(nil) 90 91 os.Exit(m.Run()) 92 } 93 94 func tempDir(t *testing.T) string { 95 t.Helper() 96 97 dir, err := ioutil.TempDir(testingDir, "tf") 98 if err != nil { 99 t.Fatalf("err: %s", err) 100 } 101 102 dir, err = filepath.EvalSymlinks(dir) 103 if err != nil { 104 t.Fatal(err) 105 } 106 107 if err := os.RemoveAll(dir); err != nil { 108 t.Fatalf("err: %s", err) 109 } 110 111 return dir 112 } 113 114 // tempWorkingDir constructs a workdir.Dir object referring to a newly-created 115 // temporary directory, and returns that object along with a cleanup function 116 // to call once the calling test is complete. 117 // 118 // Although workdir.Dir is built to support arbitrary base directories, the 119 // not-yet-migrated behaviors in command.Meta tend to expect the root module 120 // directory to be the real process working directory, and so if you intend 121 // to use the result inside a command.Meta object you must use a pattern 122 // similar to the following when initializing your test: 123 // 124 // wd, cleanup := tempWorkingDir(t) 125 // defer cleanup() 126 // defer testChdir(t, wd.RootModuleDir())() 127 // 128 // Note that testChdir modifies global state for the test process, and so a 129 // test using this pattern must never call t.Parallel(). 130 func tempWorkingDir(t *testing.T) (*workdir.Dir, func() error) { 131 t.Helper() 132 133 dirPath, err := os.MkdirTemp("", "tf-command-test-") 134 if err != nil { 135 t.Fatal(err) 136 } 137 done := func() error { 138 return os.RemoveAll(dirPath) 139 } 140 t.Logf("temporary directory %s", dirPath) 141 142 return workdir.NewDir(dirPath), done 143 } 144 145 // tempWorkingDirFixture is like tempWorkingDir but it also copies the content 146 // from a fixture directory into the temporary directory before returning it. 147 // 148 // The same caveats about working directory apply as for testWorkingDir. See 149 // the testWorkingDir commentary for an example of how to use this function 150 // along with testChdir to meet the expectations of command.Meta legacy 151 // functionality. 152 func tempWorkingDirFixture(t *testing.T, fixtureName string) *workdir.Dir { 153 t.Helper() 154 155 dirPath := testTempDir(t) 156 t.Logf("temporary directory %s with fixture %q", dirPath, fixtureName) 157 158 fixturePath := testFixturePath(fixtureName) 159 testCopyDir(t, fixturePath, dirPath) 160 // NOTE: Unfortunately because testCopyDir immediately aborts the test 161 // on failure, a failure to copy will prevent us from cleaning up the 162 // temporary directory. Oh well. :( 163 164 return workdir.NewDir(dirPath) 165 } 166 167 func testFixturePath(name string) string { 168 return filepath.Join(fixtureDir, name) 169 } 170 171 func metaOverridesForProvider(p providers.Interface) *testingOverrides { 172 return &testingOverrides{ 173 Providers: map[addrs.Provider]providers.Factory{ 174 addrs.NewDefaultProvider("test"): providers.FactoryFixed(p), 175 }, 176 } 177 } 178 179 func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *configload.Snapshot) { 180 t.Helper() 181 182 dir := filepath.Join(fixtureDir, name) 183 // FIXME: We're not dealing with the cleanup function here because 184 // this testModule function is used all over and so we don't want to 185 // change its interface at this late stage. 186 loader, _ := configload.NewLoaderForTests(t) 187 188 // Test modules usually do not refer to remote sources, and for local 189 // sources only this ultimately just records all of the module paths 190 // in a JSON file so that we can load them below. 191 inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) 192 _, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{}) 193 if instDiags.HasErrors() { 194 t.Fatal(instDiags.Err()) 195 } 196 197 config, snap, diags := loader.LoadConfigWithSnapshot(dir) 198 if diags.HasErrors() { 199 t.Fatal(diags.Error()) 200 } 201 202 return config, snap 203 } 204 205 // testPlan returns a non-nil noop plan. 206 func testPlan(t *testing.T) *plans.Plan { 207 t.Helper() 208 209 // This is what an empty configuration block would look like after being 210 // decoded with the schema of the "local" backend. 211 backendConfig := cty.ObjectVal(map[string]cty.Value{ 212 "path": cty.NullVal(cty.String), 213 "workspace_dir": cty.NullVal(cty.String), 214 }) 215 backendConfigRaw, err := plans.NewDynamicValue(backendConfig, backendConfig.Type()) 216 if err != nil { 217 t.Fatal(err) 218 } 219 220 return &plans.Plan{ 221 Backend: plans.Backend{ 222 // This is just a placeholder so that the plan file can be written 223 // out. Caller may wish to override it to something more "real" 224 // where the plan will actually be subsequently applied. 225 Type: "local", 226 Config: backendConfigRaw, 227 }, 228 Changes: plans.NewChanges(), 229 } 230 } 231 232 func testPlanFile(t *testing.T, configSnap *configload.Snapshot, state *states.State, plan *plans.Plan) string { 233 return testPlanFileMatchState(t, configSnap, state, plan, statemgr.SnapshotMeta{}) 234 } 235 236 func testPlanFileMatchState(t *testing.T, configSnap *configload.Snapshot, state *states.State, plan *plans.Plan, stateMeta statemgr.SnapshotMeta) string { 237 t.Helper() 238 239 stateFile := &statefile.File{ 240 Lineage: stateMeta.Lineage, 241 Serial: stateMeta.Serial, 242 State: state, 243 TerraformVersion: version.SemVer, 244 } 245 prevStateFile := &statefile.File{ 246 Lineage: stateMeta.Lineage, 247 Serial: stateMeta.Serial, 248 State: state, // we just assume no changes detected during refresh 249 TerraformVersion: version.SemVer, 250 } 251 252 path := testTempFile(t) 253 err := planfile.Create(path, planfile.CreateArgs{ 254 ConfigSnapshot: configSnap, 255 PreviousRunStateFile: prevStateFile, 256 StateFile: stateFile, 257 Plan: plan, 258 DependencyLocks: depsfile.NewLocks(), 259 }) 260 if err != nil { 261 t.Fatalf("failed to create temporary plan file: %s", err) 262 } 263 264 return path 265 } 266 267 // testPlanFileNoop is a shortcut function that creates a plan file that 268 // represents no changes and returns its path. This is useful when a test 269 // just needs any plan file, and it doesn't matter what is inside it. 270 func testPlanFileNoop(t *testing.T) string { 271 snap := &configload.Snapshot{ 272 Modules: map[string]*configload.SnapshotModule{ 273 "": { 274 Dir: ".", 275 Files: map[string][]byte{ 276 "main.tf": nil, 277 }, 278 }, 279 }, 280 } 281 state := states.NewState() 282 plan := testPlan(t) 283 return testPlanFile(t, snap, state, plan) 284 } 285 286 func testReadPlan(t *testing.T, path string) *plans.Plan { 287 t.Helper() 288 289 f, err := planfile.Open(path) 290 if err != nil { 291 t.Fatalf("error opening plan file %q: %s", path, err) 292 } 293 defer f.Close() 294 295 p, err := f.ReadPlan() 296 if err != nil { 297 t.Fatalf("error reading plan from plan file %q: %s", path, err) 298 } 299 300 return p 301 } 302 303 // testState returns a test State structure that we use for a lot of tests. 304 func testState() *states.State { 305 return states.BuildState(func(s *states.SyncState) { 306 s.SetResourceInstanceCurrent( 307 addrs.Resource{ 308 Mode: addrs.ManagedResourceMode, 309 Type: "test_instance", 310 Name: "foo", 311 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 312 &states.ResourceInstanceObjectSrc{ 313 // The weird whitespace here is reflective of how this would 314 // get written out in a real state file, due to the indentation 315 // of all of the containing wrapping objects and arrays. 316 AttrsJSON: []byte("{\n \"id\": \"bar\"\n }"), 317 Status: states.ObjectReady, 318 Dependencies: []addrs.ConfigResource{}, 319 }, 320 addrs.AbsProviderConfig{ 321 Provider: addrs.NewDefaultProvider("test"), 322 Module: addrs.RootModule, 323 }, 324 ) 325 // DeepCopy is used here to ensure our synthetic state matches exactly 326 // with a state that will have been copied during the command 327 // operation, and all fields have been copied correctly. 328 }).DeepCopy() 329 } 330 331 // writeStateForTesting is a helper that writes the given naked state to the 332 // given writer, generating a stub *statefile.File wrapper which is then 333 // immediately discarded. 334 func writeStateForTesting(state *states.State, w io.Writer) error { 335 sf := &statefile.File{ 336 Serial: 0, 337 Lineage: "fake-for-testing", 338 State: state, 339 } 340 return statefile.Write(sf, w) 341 } 342 343 // testStateMgrCurrentLineage returns the current lineage for the given state 344 // manager, or the empty string if it does not use lineage. This is primarily 345 // for testing against the local backend, which always supports lineage. 346 func testStateMgrCurrentLineage(mgr statemgr.Persistent) string { 347 if pm, ok := mgr.(statemgr.PersistentMeta); ok { 348 m := pm.StateSnapshotMeta() 349 return m.Lineage 350 } 351 return "" 352 } 353 354 // markStateForMatching is a helper that writes a specific marker value to 355 // a state so that it can be recognized later with getStateMatchingMarker. 356 // 357 // Internally this just sets a root module output value called "testing_mark" 358 // to the given string value. If the state is being checked in other ways, 359 // the test code may need to compensate for the addition or overwriting of this 360 // special output value name. 361 // 362 // The given mark string is returned verbatim, to allow the following pattern 363 // in tests: 364 // 365 // mark := markStateForMatching(state, "foo") 366 // // (do stuff to the state) 367 // assertStateHasMarker(state, mark) 368 func markStateForMatching(state *states.State, mark string) string { 369 state.RootModule().SetOutputValue("testing_mark", cty.StringVal(mark), false) 370 return mark 371 } 372 373 // getStateMatchingMarker is used with markStateForMatching to retrieve the 374 // mark string previously added to the given state. If no such mark is present, 375 // the result is an empty string. 376 func getStateMatchingMarker(state *states.State) string { 377 os := state.RootModule().OutputValues["testing_mark"] 378 if os == nil { 379 return "" 380 } 381 v := os.Value 382 if v.Type() == cty.String && v.IsKnown() && !v.IsNull() { 383 return v.AsString() 384 } 385 return "" 386 } 387 388 // stateHasMarker is a helper around getStateMatchingMarker that also includes 389 // the equality test, for more convenient use in test assertion branches. 390 func stateHasMarker(state *states.State, want string) bool { 391 return getStateMatchingMarker(state) == want 392 } 393 394 // assertStateHasMarker wraps stateHasMarker to automatically generate a 395 // fatal test result (i.e. t.Fatal) if the marker doesn't match. 396 func assertStateHasMarker(t *testing.T, state *states.State, want string) { 397 if !stateHasMarker(state, want) { 398 t.Fatalf("wrong state marker\ngot: %q\nwant: %q", getStateMatchingMarker(state), want) 399 } 400 } 401 402 func testStateFile(t *testing.T, s *states.State) string { 403 t.Helper() 404 405 path := testTempFile(t) 406 407 f, err := os.Create(path) 408 if err != nil { 409 t.Fatalf("failed to create temporary state file %s: %s", path, err) 410 } 411 defer f.Close() 412 413 err = writeStateForTesting(s, f) 414 if err != nil { 415 t.Fatalf("failed to write state to temporary file %s: %s", path, err) 416 } 417 418 return path 419 } 420 421 // testStateFileDefault writes the state out to the default statefile 422 // in the cwd. Use `testCwd` to change into a temp cwd. 423 func testStateFileDefault(t *testing.T, s *states.State) { 424 t.Helper() 425 426 f, err := os.Create(DefaultStateFilename) 427 if err != nil { 428 t.Fatalf("err: %s", err) 429 } 430 defer f.Close() 431 432 if err := writeStateForTesting(s, f); err != nil { 433 t.Fatalf("err: %s", err) 434 } 435 } 436 437 // testStateFileWorkspaceDefault writes the state out to the default statefile 438 // for the given workspace in the cwd. Use `testCwd` to change into a temp cwd. 439 func testStateFileWorkspaceDefault(t *testing.T, workspace string, s *states.State) string { 440 t.Helper() 441 442 workspaceDir := filepath.Join(backendLocal.DefaultWorkspaceDir, workspace) 443 err := os.MkdirAll(workspaceDir, os.ModePerm) 444 if err != nil { 445 t.Fatalf("err: %s", err) 446 } 447 448 path := filepath.Join(workspaceDir, DefaultStateFilename) 449 f, err := os.Create(path) 450 if err != nil { 451 t.Fatalf("err: %s", err) 452 } 453 defer f.Close() 454 455 if err := writeStateForTesting(s, f); err != nil { 456 t.Fatalf("err: %s", err) 457 } 458 459 return path 460 } 461 462 // testStateFileRemote writes the state out to the remote statefile 463 // in the cwd. Use `testCwd` to change into a temp cwd. 464 func testStateFileRemote(t *testing.T, s *legacy.State) string { 465 t.Helper() 466 467 path := filepath.Join(DefaultDataDir, DefaultStateFilename) 468 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 469 t.Fatalf("err: %s", err) 470 } 471 472 f, err := os.Create(path) 473 if err != nil { 474 t.Fatalf("err: %s", err) 475 } 476 defer f.Close() 477 478 if err := legacy.WriteState(s, f); err != nil { 479 t.Fatalf("err: %s", err) 480 } 481 482 return path 483 } 484 485 // testStateRead reads the state from a file 486 func testStateRead(t *testing.T, path string) *states.State { 487 t.Helper() 488 489 f, err := os.Open(path) 490 if err != nil { 491 t.Fatalf("err: %s", err) 492 } 493 defer f.Close() 494 495 sf, err := statefile.Read(f) 496 if err != nil { 497 t.Fatalf("err: %s", err) 498 } 499 500 return sf.State 501 } 502 503 // testDataStateRead reads a "data state", which is a file format resembling 504 // our state format v3 that is used only to track current backend settings. 505 // 506 // This old format still uses *legacy.State, but should be replaced with 507 // a more specialized type in a later release. 508 func testDataStateRead(t *testing.T, path string) *legacy.State { 509 t.Helper() 510 511 f, err := os.Open(path) 512 if err != nil { 513 t.Fatalf("err: %s", err) 514 } 515 defer f.Close() 516 517 s, err := legacy.ReadState(f) 518 if err != nil { 519 t.Fatalf("err: %s", err) 520 } 521 522 return s 523 } 524 525 // testStateOutput tests that the state at the given path contains 526 // the expected state string. 527 func testStateOutput(t *testing.T, path string, expected string) { 528 t.Helper() 529 530 newState := testStateRead(t, path) 531 actual := strings.TrimSpace(newState.String()) 532 expected = strings.TrimSpace(expected) 533 if actual != expected { 534 t.Fatalf("expected:\n%s\nactual:\n%s", expected, actual) 535 } 536 } 537 538 func testProvider() *terraform.MockProvider { 539 p := new(terraform.MockProvider) 540 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 541 resp.PlannedState = req.ProposedNewState 542 return resp 543 } 544 545 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 546 return providers.ReadResourceResponse{ 547 NewState: req.PriorState, 548 } 549 } 550 return p 551 } 552 553 func testTempFile(t *testing.T) string { 554 t.Helper() 555 556 return filepath.Join(testTempDir(t), "state.tfstate") 557 } 558 559 func testTempDir(t *testing.T) string { 560 t.Helper() 561 d := t.TempDir() 562 d, err := filepath.EvalSymlinks(d) 563 if err != nil { 564 t.Fatal(err) 565 } 566 567 return d 568 } 569 570 // testChdir changes the directory and returns a function to defer to 571 // revert the old cwd. 572 func testChdir(t *testing.T, new string) func() { 573 t.Helper() 574 575 old, err := os.Getwd() 576 if err != nil { 577 t.Fatalf("err: %s", err) 578 } 579 580 if err := os.Chdir(new); err != nil { 581 t.Fatalf("err: %v", err) 582 } 583 584 return func() { 585 // Re-run the function ignoring the defer result 586 testChdir(t, old) 587 } 588 } 589 590 // testCwd is used to change the current working directory 591 // into a test directory that should be removed after 592 func testCwd(t *testing.T) (string, string) { 593 t.Helper() 594 595 tmp, err := ioutil.TempDir(testingDir, "tf") 596 if err != nil { 597 t.Fatalf("err: %v", err) 598 } 599 600 cwd, err := os.Getwd() 601 if err != nil { 602 t.Fatalf("err: %v", err) 603 } 604 605 if err := os.Chdir(tmp); err != nil { 606 t.Fatalf("err: %v", err) 607 } 608 609 return tmp, cwd 610 } 611 612 // testFixCwd is used to as a defer to testDir 613 func testFixCwd(t *testing.T, tmp, cwd string) { 614 t.Helper() 615 616 if err := os.Chdir(cwd); err != nil { 617 t.Fatalf("err: %v", err) 618 } 619 620 if err := os.RemoveAll(tmp); err != nil { 621 t.Fatalf("err: %v", err) 622 } 623 } 624 625 // testStdinPipe changes os.Stdin to be a pipe that sends the data from 626 // the reader before closing the pipe. 627 // 628 // The returned function should be deferred to properly clean up and restore 629 // the original stdin. 630 func testStdinPipe(t *testing.T, src io.Reader) func() { 631 t.Helper() 632 633 r, w, err := os.Pipe() 634 if err != nil { 635 t.Fatalf("err: %s", err) 636 } 637 638 // Modify stdin to point to our new pipe 639 old := os.Stdin 640 os.Stdin = r 641 642 // Copy the data from the reader to the pipe 643 go func() { 644 defer w.Close() 645 io.Copy(w, src) 646 }() 647 648 return func() { 649 // Close our read end 650 r.Close() 651 652 // Reset stdin 653 os.Stdin = old 654 } 655 } 656 657 // Modify os.Stdout to write to the given buffer. Note that this is generally 658 // not useful since the commands are configured to write to a cli.Ui, not 659 // Stdout directly. Commands like `console` though use the raw stdout. 660 func testStdoutCapture(t *testing.T, dst io.Writer) func() { 661 t.Helper() 662 663 r, w, err := os.Pipe() 664 if err != nil { 665 t.Fatalf("err: %s", err) 666 } 667 668 // Modify stdout 669 old := os.Stdout 670 os.Stdout = w 671 672 // Copy 673 doneCh := make(chan struct{}) 674 go func() { 675 defer close(doneCh) 676 defer r.Close() 677 io.Copy(dst, r) 678 }() 679 680 return func() { 681 // Close the writer end of the pipe 682 w.Sync() 683 w.Close() 684 685 // Reset stdout 686 os.Stdout = old 687 688 // Wait for the data copy to complete to avoid a race reading data 689 <-doneCh 690 } 691 } 692 693 // testInteractiveInput configures tests so that the answers given are sent 694 // in order to interactive prompts. The returned function must be called 695 // in a defer to clean up. 696 func testInteractiveInput(t *testing.T, answers []string) func() { 697 t.Helper() 698 699 // Disable test mode so input is called 700 test = false 701 702 // Set up reader/writers 703 testInputResponse = answers 704 defaultInputReader = bytes.NewBufferString("") 705 defaultInputWriter = new(bytes.Buffer) 706 707 // Return the cleanup 708 return func() { 709 test = true 710 testInputResponse = nil 711 } 712 } 713 714 // testInputMap configures tests so that the given answers are returned 715 // for calls to Input when the right question is asked. The key is the 716 // question "Id" that is used. 717 func testInputMap(t *testing.T, answers map[string]string) func() { 718 t.Helper() 719 720 // Disable test mode so input is called 721 test = false 722 723 // Set up reader/writers 724 defaultInputReader = bytes.NewBufferString("") 725 defaultInputWriter = new(bytes.Buffer) 726 727 // Setup answers 728 testInputResponse = nil 729 testInputResponseMap = answers 730 731 // Return the cleanup 732 return func() { 733 var unusedAnswers = testInputResponseMap 734 735 // First, clean up! 736 test = true 737 testInputResponseMap = nil 738 739 if len(unusedAnswers) > 0 { 740 t.Fatalf("expected no unused answers provided to command.testInputMap, got: %v", unusedAnswers) 741 } 742 } 743 } 744 745 // testBackendState is used to make a test HTTP server to test a configured 746 // backend. This returns the complete state that can be saved. Use 747 // `testStateFileRemote` to write the returned state. 748 // 749 // When using this function, the configuration fixture for the test must 750 // include an empty configuration block for the HTTP backend, like this: 751 // 752 // terraform { 753 // backend "http" { 754 // } 755 // } 756 // 757 // If such a block isn't present, or if it isn't empty, then an error will 758 // be returned about the backend configuration having changed and that 759 // "terraform init" must be run, since the test backend config cache created 760 // by this function contains the hash for an empty configuration. 761 func testBackendState(t *testing.T, s *states.State, c int) (*legacy.State, *httptest.Server) { 762 t.Helper() 763 764 var b64md5 string 765 buf := bytes.NewBuffer(nil) 766 767 cb := func(resp http.ResponseWriter, req *http.Request) { 768 if req.Method == "PUT" { 769 resp.WriteHeader(c) 770 return 771 } 772 if s == nil { 773 resp.WriteHeader(404) 774 return 775 } 776 777 resp.Header().Set("Content-MD5", b64md5) 778 resp.Write(buf.Bytes()) 779 } 780 781 // If a state was given, make sure we calculate the proper b64md5 782 if s != nil { 783 err := statefile.Write(&statefile.File{State: s}, buf) 784 if err != nil { 785 t.Fatalf("err: %v", err) 786 } 787 md5 := md5.Sum(buf.Bytes()) 788 b64md5 = base64.StdEncoding.EncodeToString(md5[:16]) 789 } 790 791 srv := httptest.NewServer(http.HandlerFunc(cb)) 792 793 backendConfig := &configs.Backend{ 794 Type: "http", 795 Config: configs.SynthBody("<testBackendState>", map[string]cty.Value{}), 796 } 797 b := backendInit.Backend("http")() 798 configSchema := b.ConfigSchema() 799 hash := backendConfig.Hash(configSchema) 800 801 state := legacy.NewState() 802 state.Backend = &legacy.BackendState{ 803 Type: "http", 804 ConfigRaw: json.RawMessage(fmt.Sprintf(`{"address":%q}`, srv.URL)), 805 Hash: uint64(hash), 806 } 807 808 return state, srv 809 } 810 811 // testRemoteState is used to make a test HTTP server to return a given 812 // state file that can be used for testing legacy remote state. 813 // 814 // The return values are a *legacy.State instance that should be written 815 // as the "data state" (really: backend state) and the server that the 816 // returned data state refers to. 817 func testRemoteState(t *testing.T, s *states.State, c int) (*legacy.State, *httptest.Server) { 818 t.Helper() 819 820 var b64md5 string 821 buf := bytes.NewBuffer(nil) 822 823 cb := func(resp http.ResponseWriter, req *http.Request) { 824 if req.Method == "PUT" { 825 resp.WriteHeader(c) 826 return 827 } 828 if s == nil { 829 resp.WriteHeader(404) 830 return 831 } 832 833 resp.Header().Set("Content-MD5", b64md5) 834 resp.Write(buf.Bytes()) 835 } 836 837 retState := legacy.NewState() 838 839 srv := httptest.NewServer(http.HandlerFunc(cb)) 840 b := &legacy.BackendState{ 841 Type: "http", 842 } 843 b.SetConfig(cty.ObjectVal(map[string]cty.Value{ 844 "address": cty.StringVal(srv.URL), 845 }), &configschema.Block{ 846 Attributes: map[string]*configschema.Attribute{ 847 "address": { 848 Type: cty.String, 849 Required: true, 850 }, 851 }, 852 }) 853 retState.Backend = b 854 855 if s != nil { 856 err := statefile.Write(&statefile.File{State: s}, buf) 857 if err != nil { 858 t.Fatalf("failed to write initial state: %v", err) 859 } 860 } 861 862 return retState, srv 863 } 864 865 // testlockState calls a separate process to the lock the state file at path. 866 // deferFunc should be called in the caller to properly unlock the file. 867 // Since many tests change the working directory, the sourcedir argument must be 868 // supplied to locate the statelocker.go source. 869 func testLockState(sourceDir, path string) (func(), error) { 870 // build and run the binary ourselves so we can quickly terminate it for cleanup 871 buildDir, err := ioutil.TempDir(testingDir, "locker") 872 if err != nil { 873 return nil, err 874 } 875 cleanFunc := func() { 876 os.RemoveAll(buildDir) 877 } 878 879 source := filepath.Join(sourceDir, "statelocker.go") 880 lockBin := filepath.Join(buildDir, "statelocker") 881 882 cmd := exec.Command("go", "build", "-o", lockBin, source) 883 cmd.Dir = filepath.Dir(sourceDir) 884 885 out, err := cmd.CombinedOutput() 886 if err != nil { 887 cleanFunc() 888 return nil, fmt.Errorf("%s %s", err, out) 889 } 890 891 locker := exec.Command(lockBin, path) 892 pr, pw, err := os.Pipe() 893 if err != nil { 894 cleanFunc() 895 return nil, err 896 } 897 defer pr.Close() 898 defer pw.Close() 899 locker.Stderr = pw 900 locker.Stdout = pw 901 902 if err := locker.Start(); err != nil { 903 return nil, err 904 } 905 deferFunc := func() { 906 cleanFunc() 907 locker.Process.Signal(syscall.SIGTERM) 908 locker.Wait() 909 } 910 911 // wait for the process to lock 912 buf := make([]byte, 1024) 913 n, err := pr.Read(buf) 914 if err != nil { 915 return deferFunc, fmt.Errorf("read from statelocker returned: %s", err) 916 } 917 918 output := string(buf[:n]) 919 if !strings.HasPrefix(output, "LOCKID") { 920 return deferFunc, fmt.Errorf("statelocker wrote: %s", string(buf[:n])) 921 } 922 return deferFunc, nil 923 } 924 925 // testCopyDir recursively copies a directory tree, attempting to preserve 926 // permissions. Source directory must exist, destination directory may exist 927 // but will be created if not; it should typically be a temporary directory, 928 // and thus already created using os.MkdirTemp or similar. 929 // Symlinks are ignored and skipped. 930 func testCopyDir(t *testing.T, src, dst string) { 931 t.Helper() 932 933 src = filepath.Clean(src) 934 dst = filepath.Clean(dst) 935 936 si, err := os.Stat(src) 937 if err != nil { 938 t.Fatal(err) 939 } 940 if !si.IsDir() { 941 t.Fatal("source is not a directory") 942 } 943 944 _, err = os.Stat(dst) 945 if err != nil && !os.IsNotExist(err) { 946 t.Fatal(err) 947 } 948 949 err = os.MkdirAll(dst, si.Mode()) 950 if err != nil { 951 t.Fatal(err) 952 } 953 954 entries, err := ioutil.ReadDir(src) 955 if err != nil { 956 return 957 } 958 959 for _, entry := range entries { 960 srcPath := filepath.Join(src, entry.Name()) 961 dstPath := filepath.Join(dst, entry.Name()) 962 963 // If the entry is a symlink, we copy the contents 964 for entry.Mode()&os.ModeSymlink != 0 { 965 target, err := os.Readlink(srcPath) 966 if err != nil { 967 t.Fatal(err) 968 } 969 970 entry, err = os.Stat(target) 971 if err != nil { 972 t.Fatal(err) 973 } 974 } 975 976 if entry.IsDir() { 977 testCopyDir(t, srcPath, dstPath) 978 } else { 979 err = copy.CopyFile(srcPath, dstPath) 980 if err != nil { 981 t.Fatal(err) 982 } 983 } 984 } 985 } 986 987 // normalizeJSON removes all insignificant whitespace from the given JSON buffer 988 // and returns it as a string for easier comparison. 989 func normalizeJSON(t *testing.T, src []byte) string { 990 t.Helper() 991 var buf bytes.Buffer 992 err := json.Compact(&buf, src) 993 if err != nil { 994 t.Fatalf("error normalizing JSON: %s", err) 995 } 996 return buf.String() 997 } 998 999 func mustResourceAddr(s string) addrs.ConfigResource { 1000 addr, diags := addrs.ParseAbsResourceStr(s) 1001 if diags.HasErrors() { 1002 panic(diags.Err()) 1003 } 1004 return addr.Config() 1005 } 1006 1007 // This map from provider type name to namespace is used by the fake registry 1008 // when called via LookupLegacyProvider. Providers not in this map will return 1009 // a 404 Not Found error. 1010 var legacyProviderNamespaces = map[string]string{ 1011 "foo": "hashicorp", 1012 "bar": "hashicorp", 1013 "baz": "terraform-providers", 1014 "qux": "hashicorp", 1015 } 1016 1017 // This map is used to mock the provider redirect feature. 1018 var movedProviderNamespaces = map[string]string{ 1019 "qux": "acme", 1020 } 1021 1022 // testServices starts up a local HTTP server running a fake provider registry 1023 // service which responds only to discovery requests and legacy provider lookup 1024 // API calls. 1025 // 1026 // The final return value is a function to call at the end of a test function 1027 // to shut down the test server. After you call that function, the discovery 1028 // object becomes useless. 1029 func testServices(t *testing.T) (services *disco.Disco, cleanup func()) { 1030 server := httptest.NewServer(http.HandlerFunc(fakeRegistryHandler)) 1031 1032 services = disco.New() 1033 services.ForceHostServices(svchost.Hostname("registry.terraform.io"), map[string]interface{}{ 1034 "providers.v1": server.URL + "/providers/v1/", 1035 }) 1036 1037 return services, func() { 1038 server.Close() 1039 } 1040 } 1041 1042 // testRegistrySource is a wrapper around testServices that uses the created 1043 // discovery object to produce a Source instance that is ready to use with the 1044 // fake registry services. 1045 // 1046 // As with testServices, the final return value is a function to call at the end 1047 // of your test in order to shut down the test server. 1048 func testRegistrySource(t *testing.T) (source *getproviders.RegistrySource, cleanup func()) { 1049 services, close := testServices(t) 1050 source = getproviders.NewRegistrySource(services) 1051 return source, close 1052 } 1053 1054 func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) { 1055 path := req.URL.EscapedPath() 1056 1057 if !strings.HasPrefix(path, "/providers/v1/") { 1058 resp.WriteHeader(404) 1059 resp.Write([]byte(`not a provider registry endpoint`)) 1060 return 1061 } 1062 1063 pathParts := strings.Split(path, "/")[3:] 1064 1065 if len(pathParts) != 3 { 1066 resp.WriteHeader(404) 1067 resp.Write([]byte(`unrecognized path scheme`)) 1068 return 1069 } 1070 1071 if pathParts[2] != "versions" { 1072 resp.WriteHeader(404) 1073 resp.Write([]byte(`this registry only supports legacy namespace lookup requests`)) 1074 return 1075 } 1076 1077 name := pathParts[1] 1078 1079 // Legacy lookup 1080 if pathParts[0] == "-" { 1081 if namespace, ok := legacyProviderNamespaces[name]; ok { 1082 resp.Header().Set("Content-Type", "application/json") 1083 resp.WriteHeader(200) 1084 if movedNamespace, ok := movedProviderNamespaces[name]; ok { 1085 resp.Write([]byte(fmt.Sprintf(`{"id":"%s/%s","moved_to":"%s/%s","versions":[{"version":"1.0.0","protocols":["4"]}]}`, namespace, name, movedNamespace, name))) 1086 } else { 1087 resp.Write([]byte(fmt.Sprintf(`{"id":"%s/%s","versions":[{"version":"1.0.0","protocols":["4"]}]}`, namespace, name))) 1088 } 1089 } else { 1090 resp.WriteHeader(404) 1091 resp.Write([]byte(`provider not found`)) 1092 } 1093 return 1094 } 1095 1096 // Also return versions for redirect target 1097 if namespace, ok := movedProviderNamespaces[name]; ok && pathParts[0] == namespace { 1098 resp.Header().Set("Content-Type", "application/json") 1099 resp.WriteHeader(200) 1100 resp.Write([]byte(fmt.Sprintf(`{"id":"%s/%s","versions":[{"version":"1.0.0","protocols":["4"]}]}`, namespace, name))) 1101 } else { 1102 resp.WriteHeader(404) 1103 resp.Write([]byte(`provider not found`)) 1104 } 1105 } 1106 1107 func testView(t *testing.T) (*views.View, func(*testing.T) *terminal.TestOutput) { 1108 streams, done := terminal.StreamsForTesting(t) 1109 return views.NewView(streams), done 1110 }