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  }