github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/show_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "encoding/json" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "strings" 12 "testing" 13 14 "github.com/google/go-cmp/cmp" 15 "github.com/mitchellh/cli" 16 "github.com/terramate-io/tf/addrs" 17 "github.com/terramate-io/tf/configs/configschema" 18 "github.com/terramate-io/tf/plans" 19 "github.com/terramate-io/tf/providers" 20 "github.com/terramate-io/tf/states" 21 "github.com/terramate-io/tf/states/statemgr" 22 "github.com/terramate-io/tf/terraform" 23 "github.com/terramate-io/tf/version" 24 "github.com/zclconf/go-cty/cty" 25 ) 26 27 func TestShow_badArgs(t *testing.T) { 28 view, done := testView(t) 29 c := &ShowCommand{ 30 Meta: Meta{ 31 testingOverrides: metaOverridesForProvider(testProvider()), 32 View: view, 33 }, 34 } 35 36 args := []string{ 37 "bad", 38 "bad", 39 "-no-color", 40 } 41 42 code := c.Run(args) 43 output := done(t) 44 45 if code != 1 { 46 t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout()) 47 } 48 } 49 50 func TestShow_noArgsNoState(t *testing.T) { 51 view, done := testView(t) 52 c := &ShowCommand{ 53 Meta: Meta{ 54 testingOverrides: metaOverridesForProvider(testProvider()), 55 View: view, 56 }, 57 } 58 59 code := c.Run([]string{}) 60 output := done(t) 61 62 if code != 0 { 63 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 64 } 65 66 got := output.Stdout() 67 want := `No state.` 68 if !strings.Contains(got, want) { 69 t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) 70 } 71 } 72 73 func TestShow_noArgsWithState(t *testing.T) { 74 // Get a temp cwd 75 testCwd(t) 76 // Create the default state 77 testStateFileDefault(t, testState()) 78 79 view, done := testView(t) 80 c := &ShowCommand{ 81 Meta: Meta{ 82 testingOverrides: metaOverridesForProvider(showFixtureProvider()), 83 View: view, 84 }, 85 } 86 87 code := c.Run([]string{}) 88 output := done(t) 89 90 if code != 0 { 91 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 92 } 93 94 got := output.Stdout() 95 want := `# test_instance.foo:` 96 if !strings.Contains(got, want) { 97 t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) 98 } 99 } 100 101 func TestShow_argsWithState(t *testing.T) { 102 // Create the default state 103 statePath := testStateFile(t, testState()) 104 stateDir := filepath.Dir(statePath) 105 defer os.RemoveAll(stateDir) 106 defer testChdir(t, stateDir)() 107 108 view, done := testView(t) 109 c := &ShowCommand{ 110 Meta: Meta{ 111 testingOverrides: metaOverridesForProvider(showFixtureProvider()), 112 View: view, 113 }, 114 } 115 116 path := filepath.Base(statePath) 117 args := []string{ 118 path, 119 "-no-color", 120 } 121 code := c.Run(args) 122 output := done(t) 123 124 if code != 0 { 125 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 126 } 127 } 128 129 // https://github.com/terramate-io/tf/issues/21462 130 func TestShow_argsWithStateAliasedProvider(t *testing.T) { 131 // Create the default state with aliased resource 132 testState := states.BuildState(func(s *states.SyncState) { 133 s.SetResourceInstanceCurrent( 134 addrs.Resource{ 135 Mode: addrs.ManagedResourceMode, 136 Type: "test_instance", 137 Name: "foo", 138 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 139 &states.ResourceInstanceObjectSrc{ 140 // The weird whitespace here is reflective of how this would 141 // get written out in a real state file, due to the indentation 142 // of all of the containing wrapping objects and arrays. 143 AttrsJSON: []byte("{\n \"id\": \"bar\"\n }"), 144 Status: states.ObjectReady, 145 Dependencies: []addrs.ConfigResource{}, 146 }, 147 addrs.RootModuleInstance.ProviderConfigAliased(addrs.NewDefaultProvider("test"), "alias"), 148 ) 149 }) 150 151 statePath := testStateFile(t, testState) 152 stateDir := filepath.Dir(statePath) 153 defer os.RemoveAll(stateDir) 154 defer testChdir(t, stateDir)() 155 156 view, done := testView(t) 157 c := &ShowCommand{ 158 Meta: Meta{ 159 testingOverrides: metaOverridesForProvider(showFixtureProvider()), 160 View: view, 161 }, 162 } 163 164 path := filepath.Base(statePath) 165 args := []string{ 166 path, 167 "-no-color", 168 } 169 code := c.Run(args) 170 output := done(t) 171 172 if code != 0 { 173 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 174 } 175 176 got := output.Stdout() 177 want := `# missing schema for provider \"test.alias\"` 178 if strings.Contains(got, want) { 179 t.Fatalf("unexpected output\ngot: %s", got) 180 } 181 } 182 183 func TestShow_argsPlanFileDoesNotExist(t *testing.T) { 184 view, done := testView(t) 185 c := &ShowCommand{ 186 Meta: Meta{ 187 testingOverrides: metaOverridesForProvider(testProvider()), 188 View: view, 189 }, 190 } 191 192 args := []string{ 193 "doesNotExist.tfplan", 194 "-no-color", 195 } 196 code := c.Run(args) 197 output := done(t) 198 199 if code != 1 { 200 t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout()) 201 } 202 203 got := output.Stderr() 204 want1 := `Plan read error: couldn't load the provided path` 205 want2 := `open doesNotExist.tfplan: no such file or directory` 206 if !strings.Contains(got, want1) { 207 t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want1) 208 } 209 if !strings.Contains(got, want2) { 210 t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want2) 211 } 212 } 213 214 func TestShow_argsStatefileDoesNotExist(t *testing.T) { 215 view, done := testView(t) 216 c := &ShowCommand{ 217 Meta: Meta{ 218 testingOverrides: metaOverridesForProvider(testProvider()), 219 View: view, 220 }, 221 } 222 223 args := []string{ 224 "doesNotExist.tfstate", 225 "-no-color", 226 } 227 code := c.Run(args) 228 output := done(t) 229 230 if code != 1 { 231 t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout()) 232 } 233 234 got := output.Stderr() 235 want := `State read error: Error loading statefile:` 236 if !strings.Contains(got, want) { 237 t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want) 238 } 239 } 240 241 func TestShow_json_argsPlanFileDoesNotExist(t *testing.T) { 242 view, done := testView(t) 243 c := &ShowCommand{ 244 Meta: Meta{ 245 testingOverrides: metaOverridesForProvider(testProvider()), 246 View: view, 247 }, 248 } 249 250 args := []string{ 251 "-json", 252 "doesNotExist.tfplan", 253 "-no-color", 254 } 255 code := c.Run(args) 256 output := done(t) 257 258 if code != 1 { 259 t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout()) 260 } 261 262 got := output.Stderr() 263 want1 := `Plan read error: couldn't load the provided path` 264 want2 := `open doesNotExist.tfplan: no such file or directory` 265 if !strings.Contains(got, want1) { 266 t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want1) 267 } 268 if !strings.Contains(got, want2) { 269 t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want2) 270 } 271 } 272 273 func TestShow_json_argsStatefileDoesNotExist(t *testing.T) { 274 view, done := testView(t) 275 c := &ShowCommand{ 276 Meta: Meta{ 277 testingOverrides: metaOverridesForProvider(testProvider()), 278 View: view, 279 }, 280 } 281 282 args := []string{ 283 "-json", 284 "doesNotExist.tfstate", 285 "-no-color", 286 } 287 code := c.Run(args) 288 output := done(t) 289 290 if code != 1 { 291 t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout()) 292 } 293 294 got := output.Stderr() 295 want := `State read error: Error loading statefile:` 296 if !strings.Contains(got, want) { 297 t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want) 298 } 299 } 300 301 func TestShow_planNoop(t *testing.T) { 302 planPath := testPlanFileNoop(t) 303 304 view, done := testView(t) 305 c := &ShowCommand{ 306 Meta: Meta{ 307 testingOverrides: metaOverridesForProvider(testProvider()), 308 View: view, 309 }, 310 } 311 312 args := []string{ 313 planPath, 314 "-no-color", 315 } 316 code := c.Run(args) 317 output := done(t) 318 319 if code != 0 { 320 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 321 } 322 323 got := output.Stdout() 324 want := `No changes. Your infrastructure matches the configuration.` 325 if !strings.Contains(got, want) { 326 t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want) 327 } 328 } 329 330 func TestShow_planWithChanges(t *testing.T) { 331 planPathWithChanges := showFixturePlanFile(t, plans.DeleteThenCreate) 332 333 view, done := testView(t) 334 c := &ShowCommand{ 335 Meta: Meta{ 336 testingOverrides: metaOverridesForProvider(showFixtureProvider()), 337 View: view, 338 }, 339 } 340 341 args := []string{ 342 planPathWithChanges, 343 "-no-color", 344 } 345 code := c.Run(args) 346 output := done(t) 347 348 if code != 0 { 349 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 350 } 351 352 got := output.Stdout() 353 want := `test_instance.foo must be replaced` 354 if !strings.Contains(got, want) { 355 t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) 356 } 357 } 358 359 func TestShow_planWithForceReplaceChange(t *testing.T) { 360 // The main goal of this test is to see that the "replace by request" 361 // resource instance action reason can round-trip through a plan file and 362 // be reflected correctly in the "terraform show" output, the same way 363 // as it would appear in "terraform plan" output. 364 365 _, snap := testModuleWithSnapshot(t, "show") 366 plannedVal := cty.ObjectVal(map[string]cty.Value{ 367 "id": cty.UnknownVal(cty.String), 368 "ami": cty.StringVal("bar"), 369 }) 370 priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type()) 371 if err != nil { 372 t.Fatal(err) 373 } 374 plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type()) 375 if err != nil { 376 t.Fatal(err) 377 } 378 plan := testPlan(t) 379 plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ 380 Addr: addrs.Resource{ 381 Mode: addrs.ManagedResourceMode, 382 Type: "test_instance", 383 Name: "foo", 384 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 385 ProviderAddr: addrs.AbsProviderConfig{ 386 Provider: addrs.NewDefaultProvider("test"), 387 Module: addrs.RootModule, 388 }, 389 ChangeSrc: plans.ChangeSrc{ 390 Action: plans.CreateThenDelete, 391 Before: priorValRaw, 392 After: plannedValRaw, 393 }, 394 ActionReason: plans.ResourceInstanceReplaceByRequest, 395 }) 396 planFilePath := testPlanFile( 397 t, 398 snap, 399 states.NewState(), 400 plan, 401 ) 402 403 view, done := testView(t) 404 c := &ShowCommand{ 405 Meta: Meta{ 406 testingOverrides: metaOverridesForProvider(showFixtureProvider()), 407 View: view, 408 }, 409 } 410 411 args := []string{ 412 planFilePath, 413 "-no-color", 414 } 415 code := c.Run(args) 416 output := done(t) 417 418 if code != 0 { 419 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 420 } 421 422 got := output.Stdout() 423 want := `test_instance.foo will be replaced, as requested` 424 if !strings.Contains(got, want) { 425 t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) 426 } 427 428 want = `Plan: 1 to add, 0 to change, 1 to destroy.` 429 if !strings.Contains(got, want) { 430 t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) 431 } 432 } 433 434 func TestShow_planErrored(t *testing.T) { 435 _, snap := testModuleWithSnapshot(t, "show") 436 plan := testPlan(t) 437 plan.Errored = true 438 planFilePath := testPlanFile( 439 t, 440 snap, 441 states.NewState(), 442 plan, 443 ) 444 445 view, done := testView(t) 446 c := &ShowCommand{ 447 Meta: Meta{ 448 testingOverrides: metaOverridesForProvider(showFixtureProvider()), 449 View: view, 450 }, 451 } 452 453 args := []string{ 454 planFilePath, 455 "-no-color", 456 } 457 code := c.Run(args) 458 output := done(t) 459 460 if code != 0 { 461 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 462 } 463 464 got := output.Stdout() 465 want := `Planning failed. Terraform encountered an error while generating this plan.` 466 if !strings.Contains(got, want) { 467 t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) 468 } 469 } 470 471 func TestShow_plan_json(t *testing.T) { 472 planPath := showFixturePlanFile(t, plans.Create) 473 474 view, done := testView(t) 475 c := &ShowCommand{ 476 Meta: Meta{ 477 testingOverrides: metaOverridesForProvider(showFixtureProvider()), 478 View: view, 479 }, 480 } 481 482 args := []string{ 483 "-json", 484 planPath, 485 "-no-color", 486 } 487 code := c.Run(args) 488 output := done(t) 489 490 if code != 0 { 491 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 492 } 493 } 494 495 func TestShow_state(t *testing.T) { 496 originalState := testState() 497 root := originalState.RootModule() 498 root.SetOutputValue("test", cty.ObjectVal(map[string]cty.Value{ 499 "attr": cty.NullVal(cty.DynamicPseudoType), 500 "null": cty.NullVal(cty.String), 501 "list": cty.ListVal([]cty.Value{cty.NullVal(cty.Number)}), 502 }), false) 503 504 statePath := testStateFile(t, originalState) 505 defer os.RemoveAll(filepath.Dir(statePath)) 506 507 view, done := testView(t) 508 c := &ShowCommand{ 509 Meta: Meta{ 510 testingOverrides: metaOverridesForProvider(showFixtureProvider()), 511 View: view, 512 }, 513 } 514 515 args := []string{ 516 statePath, 517 "-no-color", 518 } 519 code := c.Run(args) 520 output := done(t) 521 522 if code != 0 { 523 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 524 } 525 } 526 527 func TestShow_json_output(t *testing.T) { 528 fixtureDir := "testdata/show-json" 529 testDirs, err := ioutil.ReadDir(fixtureDir) 530 if err != nil { 531 t.Fatal(err) 532 } 533 534 for _, entry := range testDirs { 535 if !entry.IsDir() { 536 continue 537 } 538 539 t.Run(entry.Name(), func(t *testing.T) { 540 td := t.TempDir() 541 inputDir := filepath.Join(fixtureDir, entry.Name()) 542 testCopyDir(t, inputDir, td) 543 defer testChdir(t, td)() 544 545 expectError := strings.Contains(entry.Name(), "error") 546 547 providerSource, close := newMockProviderSource(t, map[string][]string{ 548 "test": {"1.2.3"}, 549 "hashicorp2/test": {"1.2.3"}, 550 }) 551 defer close() 552 553 p := showFixtureProvider() 554 555 // init 556 ui := new(cli.MockUi) 557 ic := &InitCommand{ 558 Meta: Meta{ 559 testingOverrides: metaOverridesForProvider(p), 560 Ui: ui, 561 ProviderSource: providerSource, 562 }, 563 } 564 if code := ic.Run([]string{}); code != 0 { 565 if expectError { 566 // this should error, but not panic. 567 return 568 } 569 t.Fatalf("init failed\n%s", ui.ErrorWriter) 570 } 571 572 // read expected output 573 wantFile, err := os.Open("output.json") 574 if err != nil { 575 t.Fatalf("unexpected err: %s", err) 576 } 577 defer wantFile.Close() 578 byteValue, err := ioutil.ReadAll(wantFile) 579 if err != nil { 580 t.Fatalf("unexpected err: %s", err) 581 } 582 583 var want plan 584 json.Unmarshal([]byte(byteValue), &want) 585 586 // plan 587 planView, planDone := testView(t) 588 pc := &PlanCommand{ 589 Meta: Meta{ 590 testingOverrides: metaOverridesForProvider(p), 591 View: planView, 592 ProviderSource: providerSource, 593 }, 594 } 595 596 args := []string{ 597 "-out=terraform.plan", 598 } 599 600 code := pc.Run(args) 601 planOutput := planDone(t) 602 603 var wantedCode int 604 if want.Errored { 605 wantedCode = 1 606 } else { 607 wantedCode = 0 608 } 609 610 if code != wantedCode { 611 t.Fatalf("unexpected exit status %d; want %d\ngot: %s", code, wantedCode, planOutput.Stderr()) 612 } 613 614 // show 615 showView, showDone := testView(t) 616 sc := &ShowCommand{ 617 Meta: Meta{ 618 testingOverrides: metaOverridesForProvider(p), 619 View: showView, 620 ProviderSource: providerSource, 621 }, 622 } 623 624 args = []string{ 625 "-json", 626 "terraform.plan", 627 } 628 defer os.Remove("terraform.plan") 629 code = sc.Run(args) 630 showOutput := showDone(t) 631 632 if code != 0 { 633 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, showOutput.Stderr()) 634 } 635 636 // compare view output to wanted output 637 var got plan 638 639 gotString := showOutput.Stdout() 640 json.Unmarshal([]byte(gotString), &got) 641 642 // Disregard format version to reduce needless test fixture churn 643 want.FormatVersion = got.FormatVersion 644 645 if !cmp.Equal(got, want) { 646 t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want)) 647 } 648 }) 649 } 650 } 651 652 func TestShow_json_output_sensitive(t *testing.T) { 653 td := t.TempDir() 654 inputDir := "testdata/show-json-sensitive" 655 testCopyDir(t, inputDir, td) 656 defer testChdir(t, td)() 657 658 providerSource, close := newMockProviderSource(t, map[string][]string{"test": {"1.2.3"}}) 659 defer close() 660 661 p := showFixtureSensitiveProvider() 662 663 // init 664 ui := new(cli.MockUi) 665 ic := &InitCommand{ 666 Meta: Meta{ 667 testingOverrides: metaOverridesForProvider(p), 668 Ui: ui, 669 ProviderSource: providerSource, 670 }, 671 } 672 if code := ic.Run([]string{}); code != 0 { 673 t.Fatalf("init failed\n%s", ui.ErrorWriter) 674 } 675 676 // plan 677 planView, planDone := testView(t) 678 pc := &PlanCommand{ 679 Meta: Meta{ 680 testingOverrides: metaOverridesForProvider(p), 681 View: planView, 682 ProviderSource: providerSource, 683 }, 684 } 685 686 args := []string{ 687 "-out=terraform.plan", 688 } 689 code := pc.Run(args) 690 planOutput := planDone(t) 691 692 if code != 0 { 693 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, planOutput.Stderr()) 694 } 695 696 // show 697 showView, showDone := testView(t) 698 sc := &ShowCommand{ 699 Meta: Meta{ 700 testingOverrides: metaOverridesForProvider(p), 701 View: showView, 702 ProviderSource: providerSource, 703 }, 704 } 705 706 args = []string{ 707 "-json", 708 "terraform.plan", 709 } 710 defer os.Remove("terraform.plan") 711 code = sc.Run(args) 712 showOutput := showDone(t) 713 714 if code != 0 { 715 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, showOutput.Stderr()) 716 } 717 718 // compare ui output to wanted output 719 var got, want plan 720 721 gotString := showOutput.Stdout() 722 json.Unmarshal([]byte(gotString), &got) 723 724 wantFile, err := os.Open("output.json") 725 if err != nil { 726 t.Fatalf("unexpected err: %s", err) 727 } 728 defer wantFile.Close() 729 byteValue, err := ioutil.ReadAll(wantFile) 730 if err != nil { 731 t.Fatalf("unexpected err: %s", err) 732 } 733 json.Unmarshal([]byte(byteValue), &want) 734 735 // Disregard format version to reduce needless test fixture churn 736 want.FormatVersion = got.FormatVersion 737 738 if !cmp.Equal(got, want) { 739 t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want)) 740 } 741 } 742 743 // Failing conditions are only present in JSON output for refresh-only plans, 744 // so we test that separately here. 745 func TestShow_json_output_conditions_refresh_only(t *testing.T) { 746 td := t.TempDir() 747 inputDir := "testdata/show-json/conditions" 748 testCopyDir(t, inputDir, td) 749 defer testChdir(t, td)() 750 751 providerSource, close := newMockProviderSource(t, map[string][]string{"test": {"1.2.3"}}) 752 defer close() 753 754 p := showFixtureSensitiveProvider() 755 756 // init 757 ui := new(cli.MockUi) 758 ic := &InitCommand{ 759 Meta: Meta{ 760 testingOverrides: metaOverridesForProvider(p), 761 Ui: ui, 762 ProviderSource: providerSource, 763 }, 764 } 765 if code := ic.Run([]string{}); code != 0 { 766 t.Fatalf("init failed\n%s", ui.ErrorWriter) 767 } 768 769 // plan 770 planView, planDone := testView(t) 771 pc := &PlanCommand{ 772 Meta: Meta{ 773 testingOverrides: metaOverridesForProvider(p), 774 View: planView, 775 ProviderSource: providerSource, 776 }, 777 } 778 779 args := []string{ 780 "-refresh-only", 781 "-out=terraform.plan", 782 "-var=ami=bad-ami", 783 "-state=for-refresh.tfstate", 784 } 785 code := pc.Run(args) 786 planOutput := planDone(t) 787 788 if code != 0 { 789 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, planOutput.Stderr()) 790 } 791 792 // show 793 showView, showDone := testView(t) 794 sc := &ShowCommand{ 795 Meta: Meta{ 796 testingOverrides: metaOverridesForProvider(p), 797 View: showView, 798 ProviderSource: providerSource, 799 }, 800 } 801 802 args = []string{ 803 "-json", 804 "terraform.plan", 805 } 806 defer os.Remove("terraform.plan") 807 code = sc.Run(args) 808 showOutput := showDone(t) 809 810 if code != 0 { 811 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, showOutput.Stderr()) 812 } 813 814 // compare JSON output to wanted output 815 var got, want plan 816 817 gotString := showOutput.Stdout() 818 json.Unmarshal([]byte(gotString), &got) 819 820 wantFile, err := os.Open("output-refresh-only.json") 821 if err != nil { 822 t.Fatalf("unexpected err: %s", err) 823 } 824 defer wantFile.Close() 825 byteValue, err := ioutil.ReadAll(wantFile) 826 if err != nil { 827 t.Fatalf("unexpected err: %s", err) 828 } 829 json.Unmarshal([]byte(byteValue), &want) 830 831 // Disregard format version to reduce needless test fixture churn 832 want.FormatVersion = got.FormatVersion 833 834 if !cmp.Equal(got, want) { 835 t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want)) 836 } 837 } 838 839 // similar test as above, without the plan 840 func TestShow_json_output_state(t *testing.T) { 841 fixtureDir := "testdata/show-json-state" 842 testDirs, err := ioutil.ReadDir(fixtureDir) 843 if err != nil { 844 t.Fatal(err) 845 } 846 847 for _, entry := range testDirs { 848 if !entry.IsDir() { 849 continue 850 } 851 852 t.Run(entry.Name(), func(t *testing.T) { 853 td := t.TempDir() 854 inputDir := filepath.Join(fixtureDir, entry.Name()) 855 testCopyDir(t, inputDir, td) 856 defer testChdir(t, td)() 857 858 providerSource, close := newMockProviderSource(t, map[string][]string{ 859 "test": {"1.2.3"}, 860 }) 861 defer close() 862 863 p := showFixtureProvider() 864 865 // init 866 ui := new(cli.MockUi) 867 ic := &InitCommand{ 868 Meta: Meta{ 869 testingOverrides: metaOverridesForProvider(p), 870 Ui: ui, 871 ProviderSource: providerSource, 872 }, 873 } 874 if code := ic.Run([]string{}); code != 0 { 875 t.Fatalf("init failed\n%s", ui.ErrorWriter) 876 } 877 878 // show 879 showView, showDone := testView(t) 880 sc := &ShowCommand{ 881 Meta: Meta{ 882 testingOverrides: metaOverridesForProvider(p), 883 View: showView, 884 ProviderSource: providerSource, 885 }, 886 } 887 888 code := sc.Run([]string{"-json"}) 889 showOutput := showDone(t) 890 891 if code != 0 { 892 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, showOutput.Stderr()) 893 } 894 895 // compare ui output to wanted output 896 type state struct { 897 FormatVersion string `json:"format_version,omitempty"` 898 TerraformVersion string `json:"terraform_version"` 899 Values map[string]interface{} `json:"values,omitempty"` 900 SensitiveValues map[string]bool `json:"sensitive_values,omitempty"` 901 } 902 var got, want state 903 904 gotString := showOutput.Stdout() 905 json.Unmarshal([]byte(gotString), &got) 906 907 wantFile, err := os.Open("output.json") 908 if err != nil { 909 t.Fatalf("unexpected error: %s", err) 910 } 911 defer wantFile.Close() 912 byteValue, err := ioutil.ReadAll(wantFile) 913 if err != nil { 914 t.Fatalf("unexpected err: %s", err) 915 } 916 json.Unmarshal([]byte(byteValue), &want) 917 918 if !cmp.Equal(got, want) { 919 t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want)) 920 } 921 }) 922 } 923 } 924 925 func TestShow_planWithNonDefaultStateLineage(t *testing.T) { 926 // Create a temporary working directory that is empty 927 td := t.TempDir() 928 testCopyDir(t, testFixturePath("show"), td) 929 defer testChdir(t, td)() 930 931 // Write default state file with a testing lineage ("fake-for-testing") 932 testStateFileDefault(t, testState()) 933 934 // Create a plan with a different lineage, which we should still be able 935 // to show 936 _, snap := testModuleWithSnapshot(t, "show") 937 state := testState() 938 plan := testPlan(t) 939 stateMeta := statemgr.SnapshotMeta{ 940 Lineage: "fake-for-plan", 941 Serial: 1, 942 TerraformVersion: version.SemVer, 943 } 944 planPath := testPlanFileMatchState(t, snap, state, plan, stateMeta) 945 946 view, done := testView(t) 947 c := &ShowCommand{ 948 Meta: Meta{ 949 testingOverrides: metaOverridesForProvider(testProvider()), 950 View: view, 951 }, 952 } 953 954 args := []string{ 955 planPath, 956 "-no-color", 957 } 958 code := c.Run(args) 959 output := done(t) 960 961 if code != 0 { 962 t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) 963 } 964 965 got := output.Stdout() 966 want := `No changes. Your infrastructure matches the configuration.` 967 if !strings.Contains(got, want) { 968 t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) 969 } 970 } 971 972 func TestShow_corruptStatefile(t *testing.T) { 973 td := t.TempDir() 974 inputDir := "testdata/show-corrupt-statefile" 975 testCopyDir(t, inputDir, td) 976 defer testChdir(t, td)() 977 978 view, done := testView(t) 979 c := &ShowCommand{ 980 Meta: Meta{ 981 testingOverrides: metaOverridesForProvider(testProvider()), 982 View: view, 983 }, 984 } 985 986 code := c.Run([]string{}) 987 output := done(t) 988 989 if code != 1 { 990 t.Fatalf("unexpected exit status %d; want 1\ngot: %s", code, output.Stdout()) 991 } 992 993 got := output.Stderr() 994 want := `Unsupported state file format` 995 if !strings.Contains(got, want) { 996 t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want) 997 } 998 } 999 1000 // showFixtureSchema returns a schema suitable for processing the configuration 1001 // in testdata/show. This schema should be assigned to a mock provider 1002 // named "test". 1003 func showFixtureSchema() *providers.GetProviderSchemaResponse { 1004 return &providers.GetProviderSchemaResponse{ 1005 Provider: providers.Schema{ 1006 Block: &configschema.Block{ 1007 Attributes: map[string]*configschema.Attribute{ 1008 "region": {Type: cty.String, Optional: true}, 1009 }, 1010 }, 1011 }, 1012 ResourceTypes: map[string]providers.Schema{ 1013 "test_instance": { 1014 Block: &configschema.Block{ 1015 Attributes: map[string]*configschema.Attribute{ 1016 "id": {Type: cty.String, Optional: true, Computed: true}, 1017 "ami": {Type: cty.String, Optional: true}, 1018 }, 1019 }, 1020 }, 1021 }, 1022 } 1023 } 1024 1025 // showFixtureSensitiveSchema returns a schema suitable for processing the configuration 1026 // in testdata/show. This schema should be assigned to a mock provider 1027 // named "test". It includes a sensitive attribute. 1028 func showFixtureSensitiveSchema() *providers.GetProviderSchemaResponse { 1029 return &providers.GetProviderSchemaResponse{ 1030 Provider: providers.Schema{ 1031 Block: &configschema.Block{ 1032 Attributes: map[string]*configschema.Attribute{ 1033 "region": {Type: cty.String, Optional: true}, 1034 }, 1035 }, 1036 }, 1037 ResourceTypes: map[string]providers.Schema{ 1038 "test_instance": { 1039 Block: &configschema.Block{ 1040 Attributes: map[string]*configschema.Attribute{ 1041 "id": {Type: cty.String, Optional: true, Computed: true}, 1042 "ami": {Type: cty.String, Optional: true}, 1043 "password": {Type: cty.String, Optional: true, Sensitive: true}, 1044 }, 1045 }, 1046 }, 1047 }, 1048 } 1049 } 1050 1051 // showFixtureProvider returns a mock provider that is configured for basic 1052 // operation with the configuration in testdata/show. This mock has 1053 // GetSchemaResponse, PlanResourceChangeFn, and ApplyResourceChangeFn populated, 1054 // with the plan/apply steps just passing through the data determined by 1055 // Terraform Core. 1056 func showFixtureProvider() *terraform.MockProvider { 1057 p := testProvider() 1058 p.GetProviderSchemaResponse = showFixtureSchema() 1059 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 1060 idVal := req.PriorState.GetAttr("id") 1061 amiVal := req.PriorState.GetAttr("ami") 1062 if amiVal.RawEquals(cty.StringVal("refresh-me")) { 1063 amiVal = cty.StringVal("refreshed") 1064 } 1065 return providers.ReadResourceResponse{ 1066 NewState: cty.ObjectVal(map[string]cty.Value{ 1067 "id": idVal, 1068 "ami": amiVal, 1069 }), 1070 Private: req.Private, 1071 } 1072 } 1073 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { 1074 // this is a destroy plan, 1075 if req.ProposedNewState.IsNull() { 1076 resp.PlannedState = req.ProposedNewState 1077 resp.PlannedPrivate = req.PriorPrivate 1078 return resp 1079 } 1080 1081 idVal := req.ProposedNewState.GetAttr("id") 1082 amiVal := req.ProposedNewState.GetAttr("ami") 1083 if idVal.IsNull() { 1084 idVal = cty.UnknownVal(cty.String) 1085 } 1086 var reqRep []cty.Path 1087 if amiVal.RawEquals(cty.StringVal("force-replace")) { 1088 reqRep = append(reqRep, cty.GetAttrPath("ami")) 1089 } 1090 return providers.PlanResourceChangeResponse{ 1091 PlannedState: cty.ObjectVal(map[string]cty.Value{ 1092 "id": idVal, 1093 "ami": amiVal, 1094 }), 1095 RequiresReplace: reqRep, 1096 } 1097 } 1098 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { 1099 idVal := req.PlannedState.GetAttr("id") 1100 amiVal := req.PlannedState.GetAttr("ami") 1101 if !idVal.IsKnown() { 1102 idVal = cty.StringVal("placeholder") 1103 } 1104 return providers.ApplyResourceChangeResponse{ 1105 NewState: cty.ObjectVal(map[string]cty.Value{ 1106 "id": idVal, 1107 "ami": amiVal, 1108 }), 1109 } 1110 } 1111 return p 1112 } 1113 1114 // showFixtureSensitiveProvider returns a mock provider that is configured for basic 1115 // operation with the configuration in testdata/show. This mock has 1116 // GetSchemaResponse, PlanResourceChangeFn, and ApplyResourceChangeFn populated, 1117 // with the plan/apply steps just passing through the data determined by 1118 // Terraform Core. It also has a sensitive attribute in the provider schema. 1119 func showFixtureSensitiveProvider() *terraform.MockProvider { 1120 p := testProvider() 1121 p.GetProviderSchemaResponse = showFixtureSensitiveSchema() 1122 p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { 1123 idVal := req.ProposedNewState.GetAttr("id") 1124 if idVal.IsNull() { 1125 idVal = cty.UnknownVal(cty.String) 1126 } 1127 return providers.PlanResourceChangeResponse{ 1128 PlannedState: cty.ObjectVal(map[string]cty.Value{ 1129 "id": idVal, 1130 "ami": req.ProposedNewState.GetAttr("ami"), 1131 "password": req.ProposedNewState.GetAttr("password"), 1132 }), 1133 } 1134 } 1135 p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { 1136 idVal := req.PlannedState.GetAttr("id") 1137 if !idVal.IsKnown() { 1138 idVal = cty.StringVal("placeholder") 1139 } 1140 return providers.ApplyResourceChangeResponse{ 1141 NewState: cty.ObjectVal(map[string]cty.Value{ 1142 "id": idVal, 1143 "ami": req.PlannedState.GetAttr("ami"), 1144 "password": req.PlannedState.GetAttr("password"), 1145 }), 1146 } 1147 } 1148 return p 1149 } 1150 1151 // showFixturePlanFile creates a plan file at a temporary location containing a 1152 // single change to create or update the test_instance.foo that is included in the "show" 1153 // test fixture, returning the location of that plan file. 1154 // `action` is the planned change you would like to elicit 1155 func showFixturePlanFile(t *testing.T, action plans.Action) string { 1156 _, snap := testModuleWithSnapshot(t, "show") 1157 plannedVal := cty.ObjectVal(map[string]cty.Value{ 1158 "id": cty.UnknownVal(cty.String), 1159 "ami": cty.StringVal("bar"), 1160 }) 1161 priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type()) 1162 if err != nil { 1163 t.Fatal(err) 1164 } 1165 plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type()) 1166 if err != nil { 1167 t.Fatal(err) 1168 } 1169 plan := testPlan(t) 1170 plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ 1171 Addr: addrs.Resource{ 1172 Mode: addrs.ManagedResourceMode, 1173 Type: "test_instance", 1174 Name: "foo", 1175 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 1176 ProviderAddr: addrs.AbsProviderConfig{ 1177 Provider: addrs.NewDefaultProvider("test"), 1178 Module: addrs.RootModule, 1179 }, 1180 ChangeSrc: plans.ChangeSrc{ 1181 Action: action, 1182 Before: priorValRaw, 1183 After: plannedValRaw, 1184 }, 1185 }) 1186 return testPlanFile( 1187 t, 1188 snap, 1189 states.NewState(), 1190 plan, 1191 ) 1192 } 1193 1194 // this simplified plan struct allows us to preserve field order when marshaling 1195 // the command output. NOTE: we are leaving "terraform_version" out of this test 1196 // to avoid needing to constantly update the expected output; as a potential 1197 // TODO we could write a jsonplan compare function. 1198 type plan struct { 1199 FormatVersion string `json:"format_version,omitempty"` 1200 Variables map[string]interface{} `json:"variables,omitempty"` 1201 PlannedValues map[string]interface{} `json:"planned_values,omitempty"` 1202 ResourceDrift []interface{} `json:"resource_drift,omitempty"` 1203 ResourceChanges []interface{} `json:"resource_changes,omitempty"` 1204 OutputChanges map[string]interface{} `json:"output_changes,omitempty"` 1205 PriorState priorState `json:"prior_state,omitempty"` 1206 Config map[string]interface{} `json:"configuration,omitempty"` 1207 Errored bool `json:"errored"` 1208 } 1209 1210 type priorState struct { 1211 FormatVersion string `json:"format_version,omitempty"` 1212 Values map[string]interface{} `json:"values,omitempty"` 1213 SensitiveValues map[string]bool `json:"sensitive_values,omitempty"` 1214 }