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