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