github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/show_test.go (about)

     1  package command
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/hashicorp/terraform/addrs"
    14  	"github.com/hashicorp/terraform/configs/configschema"
    15  	"github.com/hashicorp/terraform/helper/copy"
    16  	"github.com/hashicorp/terraform/plans"
    17  	"github.com/hashicorp/terraform/providers"
    18  	"github.com/hashicorp/terraform/states"
    19  	"github.com/hashicorp/terraform/terraform"
    20  	"github.com/mitchellh/cli"
    21  	"github.com/zclconf/go-cty/cty"
    22  )
    23  
    24  func TestShow(t *testing.T) {
    25  	ui := new(cli.MockUi)
    26  	c := &ShowCommand{
    27  		Meta: Meta{
    28  			testingOverrides: metaOverridesForProvider(testProvider()),
    29  			Ui:               ui,
    30  		},
    31  	}
    32  
    33  	args := []string{
    34  		"bad",
    35  		"bad",
    36  	}
    37  	if code := c.Run(args); code != 1 {
    38  		t.Fatalf("bad: \n%s", ui.OutputWriter.String())
    39  	}
    40  }
    41  
    42  func TestShow_noArgs(t *testing.T) {
    43  	// Create the default state
    44  	statePath := testStateFile(t, testState())
    45  	stateDir := filepath.Dir(statePath)
    46  	defer os.RemoveAll(stateDir)
    47  	defer testChdir(t, stateDir)()
    48  
    49  	ui := new(cli.MockUi)
    50  	c := &ShowCommand{
    51  		Meta: Meta{
    52  			testingOverrides: metaOverridesForProvider(testProvider()),
    53  			Ui:               ui,
    54  		},
    55  	}
    56  
    57  	// the statefile created by testStateFile is named state.tfstate
    58  	// so one arg is required
    59  	args := []string{"state.tfstate"}
    60  	if code := c.Run(args); code != 0 {
    61  		t.Fatalf("bad: \n%s", ui.OutputWriter.String())
    62  	}
    63  
    64  	if !strings.Contains(ui.OutputWriter.String(), "# test_instance.foo:") {
    65  		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
    66  	}
    67  }
    68  
    69  // https://github.com/hashicorp/terraform/issues/21462
    70  func TestShow_aliasedProvider(t *testing.T) {
    71  	// Create the default state with aliased resource
    72  	testState := states.BuildState(func(s *states.SyncState) {
    73  		s.SetResourceInstanceCurrent(
    74  			addrs.Resource{
    75  				Mode: addrs.ManagedResourceMode,
    76  				Type: "test_instance",
    77  				Name: "foo",
    78  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
    79  			&states.ResourceInstanceObjectSrc{
    80  				// The weird whitespace here is reflective of how this would
    81  				// get written out in a real state file, due to the indentation
    82  				// of all of the containing wrapping objects and arrays.
    83  				AttrsJSON:    []byte("{\n            \"id\": \"bar\"\n          }"),
    84  				Status:       states.ObjectReady,
    85  				Dependencies: []addrs.AbsResource{},
    86  				DependsOn:    []addrs.Referenceable{},
    87  			},
    88  			addrs.RootModuleInstance.ProviderConfigAliased("test", "alias"),
    89  		)
    90  	})
    91  
    92  	statePath := testStateFile(t, testState)
    93  	stateDir := filepath.Dir(statePath)
    94  	defer os.RemoveAll(stateDir)
    95  	defer testChdir(t, stateDir)()
    96  
    97  	ui := new(cli.MockUi)
    98  	c := &ShowCommand{
    99  		Meta: Meta{
   100  			testingOverrides: metaOverridesForProvider(testProvider()),
   101  			Ui:               ui,
   102  		},
   103  	}
   104  
   105  	fmt.Println(os.Getwd())
   106  
   107  	// the statefile created by testStateFile is named state.tfstate
   108  	args := []string{"state.tfstate"}
   109  	if code := c.Run(args); code != 0 {
   110  		t.Fatalf("bad exit code: \n%s", ui.OutputWriter.String())
   111  	}
   112  
   113  	if strings.Contains(ui.OutputWriter.String(), "# missing schema for provider \"test.alias\"") {
   114  		t.Fatalf("bad output: \n%s", ui.OutputWriter.String())
   115  	}
   116  }
   117  
   118  func TestShow_noArgsNoState(t *testing.T) {
   119  	// Create the default state
   120  	statePath := testStateFile(t, testState())
   121  	stateDir := filepath.Dir(statePath)
   122  	defer os.RemoveAll(stateDir)
   123  	defer testChdir(t, stateDir)()
   124  
   125  	ui := new(cli.MockUi)
   126  	c := &ShowCommand{
   127  		Meta: Meta{
   128  			testingOverrides: metaOverridesForProvider(testProvider()),
   129  			Ui:               ui,
   130  		},
   131  	}
   132  
   133  	// the statefile created by testStateFile is named state.tfstate
   134  	args := []string{"state.tfstate"}
   135  	if code := c.Run(args); code != 0 {
   136  		t.Fatalf("bad: \n%s", ui.OutputWriter.String())
   137  	}
   138  }
   139  
   140  func TestShow_plan(t *testing.T) {
   141  	planPath := testPlanFileNoop(t)
   142  
   143  	ui := cli.NewMockUi()
   144  	c := &ShowCommand{
   145  		Meta: Meta{
   146  			testingOverrides: metaOverridesForProvider(testProvider()),
   147  			Ui:               ui,
   148  		},
   149  	}
   150  
   151  	args := []string{
   152  		planPath,
   153  	}
   154  	if code := c.Run(args); code != 0 {
   155  		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
   156  	}
   157  
   158  	want := `Terraform will perform the following actions`
   159  	got := ui.OutputWriter.String()
   160  	if !strings.Contains(got, want) {
   161  		t.Errorf("missing expected output\nwant: %s\ngot:\n%s", want, got)
   162  	}
   163  }
   164  
   165  func TestShow_planWithChanges(t *testing.T) {
   166  	planPathWithChanges := showFixturePlanFile(t, plans.DeleteThenCreate)
   167  
   168  	ui := cli.NewMockUi()
   169  	c := &ShowCommand{
   170  		Meta: Meta{
   171  			testingOverrides: metaOverridesForProvider(showFixtureProvider()),
   172  			Ui:               ui,
   173  		},
   174  	}
   175  
   176  	args := []string{
   177  		planPathWithChanges,
   178  	}
   179  
   180  	if code := c.Run(args); code != 0 {
   181  		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
   182  	}
   183  
   184  	want := `test_instance.foo must be replaced`
   185  	got := ui.OutputWriter.String()
   186  	if !strings.Contains(got, want) {
   187  		t.Errorf("missing expected output\nwant: %s\ngot:\n%s", want, got)
   188  	}
   189  }
   190  
   191  func TestShow_plan_json(t *testing.T) {
   192  	planPath := showFixturePlanFile(t, plans.Create)
   193  
   194  	ui := new(cli.MockUi)
   195  	c := &ShowCommand{
   196  		Meta: Meta{
   197  			testingOverrides: metaOverridesForProvider(showFixtureProvider()),
   198  			Ui:               ui,
   199  		},
   200  	}
   201  
   202  	args := []string{
   203  		"-json",
   204  		planPath,
   205  	}
   206  	if code := c.Run(args); code != 0 {
   207  		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
   208  	}
   209  }
   210  
   211  func TestShow_state(t *testing.T) {
   212  	originalState := testState()
   213  	statePath := testStateFile(t, originalState)
   214  	defer os.RemoveAll(filepath.Dir(statePath))
   215  
   216  	ui := new(cli.MockUi)
   217  	c := &ShowCommand{
   218  		Meta: Meta{
   219  			testingOverrides: metaOverridesForProvider(testProvider()),
   220  			Ui:               ui,
   221  		},
   222  	}
   223  
   224  	args := []string{
   225  		statePath,
   226  	}
   227  	if code := c.Run(args); code != 0 {
   228  		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
   229  	}
   230  }
   231  
   232  func TestShow_json_output(t *testing.T) {
   233  	fixtureDir := "testdata/show-json"
   234  	testDirs, err := ioutil.ReadDir(fixtureDir)
   235  	if err != nil {
   236  		t.Fatal(err)
   237  	}
   238  
   239  	for _, entry := range testDirs {
   240  		if !entry.IsDir() {
   241  			continue
   242  		}
   243  
   244  		t.Run(entry.Name(), func(t *testing.T) {
   245  			td := tempDir(t)
   246  			inputDir := filepath.Join(fixtureDir, entry.Name())
   247  			copy.CopyDir(inputDir, td)
   248  			defer os.RemoveAll(td)
   249  			defer testChdir(t, td)()
   250  
   251  			expectError := strings.Contains(entry.Name(), "error")
   252  
   253  			p := showFixtureProvider()
   254  			ui := new(cli.MockUi)
   255  			m := Meta{
   256  				testingOverrides: metaOverridesForProvider(p),
   257  				Ui:               ui,
   258  			}
   259  
   260  			// init
   261  			ic := &InitCommand{
   262  				Meta: m,
   263  				providerInstaller: &mockProviderInstaller{
   264  					Providers: map[string][]string{
   265  						"test": []string{"1.2.3"},
   266  					},
   267  					Dir: m.pluginDir(),
   268  				},
   269  			}
   270  			if code := ic.Run([]string{}); code != 0 {
   271  				if expectError {
   272  					// this should error, but not panic.
   273  					return
   274  				}
   275  				t.Fatalf("init failed\n%s", ui.ErrorWriter)
   276  			}
   277  
   278  			pc := &PlanCommand{
   279  				Meta: m,
   280  			}
   281  
   282  			args := []string{
   283  				"-out=terraform.plan",
   284  			}
   285  
   286  			if code := pc.Run(args); code != 0 {
   287  				t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
   288  			}
   289  
   290  			// flush the plan output from the mock ui
   291  			ui.OutputWriter.Reset()
   292  			sc := &ShowCommand{
   293  				Meta: m,
   294  			}
   295  
   296  			args = []string{
   297  				"-json",
   298  				"terraform.plan",
   299  			}
   300  			defer os.Remove("terraform.plan")
   301  
   302  			if code := sc.Run(args); code != 0 {
   303  				t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
   304  			}
   305  
   306  			// compare ui output to wanted output
   307  			var got, want plan
   308  
   309  			gotString := ui.OutputWriter.String()
   310  			json.Unmarshal([]byte(gotString), &got)
   311  
   312  			wantFile, err := os.Open("output.json")
   313  			if err != nil {
   314  				t.Fatalf("err: %s", err)
   315  			}
   316  			defer wantFile.Close()
   317  			byteValue, err := ioutil.ReadAll(wantFile)
   318  			if err != nil {
   319  				t.Fatalf("err: %s", err)
   320  			}
   321  			json.Unmarshal([]byte(byteValue), &want)
   322  
   323  			if !cmp.Equal(got, want) {
   324  				t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
   325  			}
   326  
   327  		})
   328  
   329  	}
   330  }
   331  
   332  // similar test as above, without the plan
   333  func TestShow_json_output_state(t *testing.T) {
   334  	fixtureDir := "testdata/show-json-state"
   335  	testDirs, err := ioutil.ReadDir(fixtureDir)
   336  	if err != nil {
   337  		t.Fatal(err)
   338  	}
   339  
   340  	for _, entry := range testDirs {
   341  		if !entry.IsDir() {
   342  			continue
   343  		}
   344  
   345  		t.Run(entry.Name(), func(t *testing.T) {
   346  			td := tempDir(t)
   347  			inputDir := filepath.Join(fixtureDir, entry.Name())
   348  			copy.CopyDir(inputDir, td)
   349  			defer os.RemoveAll(td)
   350  			defer testChdir(t, td)()
   351  
   352  			p := showFixtureProvider()
   353  			ui := new(cli.MockUi)
   354  			m := Meta{
   355  				testingOverrides: metaOverridesForProvider(p),
   356  				Ui:               ui,
   357  			}
   358  
   359  			// init
   360  			ic := &InitCommand{
   361  				Meta: m,
   362  				providerInstaller: &mockProviderInstaller{
   363  					Providers: map[string][]string{
   364  						"test": []string{"1.2.3"},
   365  					},
   366  					Dir: m.pluginDir(),
   367  				},
   368  			}
   369  			if code := ic.Run([]string{}); code != 0 {
   370  				t.Fatalf("init failed\n%s", ui.ErrorWriter)
   371  			}
   372  
   373  			// flush the plan output from the mock ui
   374  			ui.OutputWriter.Reset()
   375  			sc := &ShowCommand{
   376  				Meta: m,
   377  			}
   378  
   379  			if code := sc.Run([]string{"-json"}); code != 0 {
   380  				t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
   381  			}
   382  
   383  			// compare ui output to wanted output
   384  			type state struct {
   385  				FormatVersion    string                 `json:"format_version,omitempty"`
   386  				TerraformVersion string                 `json:"terraform_version"`
   387  				Values           map[string]interface{} `json:"values,omitempty"`
   388  			}
   389  			var got, want state
   390  
   391  			gotString := ui.OutputWriter.String()
   392  			json.Unmarshal([]byte(gotString), &got)
   393  
   394  			wantFile, err := os.Open("output.json")
   395  			if err != nil {
   396  				t.Fatalf("err: %s", err)
   397  			}
   398  			defer wantFile.Close()
   399  			byteValue, err := ioutil.ReadAll(wantFile)
   400  			if err != nil {
   401  				t.Fatalf("err: %s", err)
   402  			}
   403  			json.Unmarshal([]byte(byteValue), &want)
   404  
   405  			if !cmp.Equal(got, want) {
   406  				t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
   407  			}
   408  
   409  		})
   410  
   411  	}
   412  }
   413  
   414  // showFixtureSchema returns a schema suitable for processing the configuration
   415  // in testdata/show. This schema should be assigned to a mock provider
   416  // named "test".
   417  func showFixtureSchema() *terraform.ProviderSchema {
   418  	return &terraform.ProviderSchema{
   419  		ResourceTypes: map[string]*configschema.Block{
   420  			"test_instance": {
   421  				Attributes: map[string]*configschema.Attribute{
   422  					"id":  {Type: cty.String, Optional: true, Computed: true},
   423  					"ami": {Type: cty.String, Optional: true},
   424  				},
   425  			},
   426  		},
   427  	}
   428  }
   429  
   430  // showFixtureProvider returns a mock provider that is configured for basic
   431  // operation with the configuration in testdata/show. This mock has
   432  // GetSchemaReturn, PlanResourceChangeFn, and ApplyResourceChangeFn populated,
   433  // with the plan/apply steps just passing through the data determined by
   434  // Terraform Core.
   435  func showFixtureProvider() *terraform.MockProvider {
   436  	p := testProvider()
   437  	p.GetSchemaReturn = showFixtureSchema()
   438  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
   439  		idVal := req.ProposedNewState.GetAttr("id")
   440  		amiVal := req.ProposedNewState.GetAttr("ami")
   441  		if idVal.IsNull() {
   442  			idVal = cty.UnknownVal(cty.String)
   443  		}
   444  		return providers.PlanResourceChangeResponse{
   445  			PlannedState: cty.ObjectVal(map[string]cty.Value{
   446  				"id":  idVal,
   447  				"ami": amiVal,
   448  			}),
   449  		}
   450  	}
   451  	p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
   452  		idVal := req.PlannedState.GetAttr("id")
   453  		amiVal := req.PlannedState.GetAttr("ami")
   454  		if !idVal.IsKnown() {
   455  			idVal = cty.StringVal("placeholder")
   456  		}
   457  		return providers.ApplyResourceChangeResponse{
   458  			NewState: cty.ObjectVal(map[string]cty.Value{
   459  				"id":  idVal,
   460  				"ami": amiVal,
   461  			}),
   462  		}
   463  	}
   464  	return p
   465  }
   466  
   467  // showFixturePlanFile creates a plan file at a temporary location containing a
   468  // single change to create or update the test_instance.foo that is included in the "show"
   469  // test fixture, returning the location of that plan file.
   470  // `action` is the planned change you would like to elicit
   471  func showFixturePlanFile(t *testing.T, action plans.Action) string {
   472  	_, snap := testModuleWithSnapshot(t, "show")
   473  	plannedVal := cty.ObjectVal(map[string]cty.Value{
   474  		"id":  cty.UnknownVal(cty.String),
   475  		"ami": cty.StringVal("bar"),
   476  	})
   477  	priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type())
   478  	if err != nil {
   479  		t.Fatal(err)
   480  	}
   481  	plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type())
   482  	if err != nil {
   483  		t.Fatal(err)
   484  	}
   485  	plan := testPlan(t)
   486  	plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
   487  		Addr: addrs.Resource{
   488  			Mode: addrs.ManagedResourceMode,
   489  			Type: "test_instance",
   490  			Name: "foo",
   491  		}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   492  		ProviderAddr: addrs.ProviderConfig{Type: addrs.NewLegacyProvider("test")}.Absolute(addrs.RootModuleInstance),
   493  		ChangeSrc: plans.ChangeSrc{
   494  			Action: action,
   495  			Before: priorValRaw,
   496  			After:  plannedValRaw,
   497  		},
   498  	})
   499  	return testPlanFile(
   500  		t,
   501  		snap,
   502  		states.NewState(),
   503  		plan,
   504  	)
   505  }
   506  
   507  // this simplified plan struct allows us to preserve field order when marshaling
   508  // the command output. NOTE: we are leaving "terraform_version" out of this test
   509  // to avoid needing to constantly update the expected output; as a potential
   510  // TODO we could write a jsonplan compare function.
   511  type plan struct {
   512  	FormatVersion   string                 `json:"format_version,omitempty"`
   513  	Variables       map[string]interface{} `json:"variables,omitempty"`
   514  	PlannedValues   map[string]interface{} `json:"planned_values,omitempty"`
   515  	ResourceChanges []interface{}          `json:"resource_changes,omitempty"`
   516  	OutputChanges   map[string]interface{} `json:"output_changes,omitempty"`
   517  	PriorState      priorState             `json:"prior_state,omitempty"`
   518  	Config          map[string]interface{} `json:"configuration,omitempty"`
   519  }
   520  
   521  type priorState struct {
   522  	FormatVersion string                 `json:"format_version,omitempty"`
   523  	Values        map[string]interface{} `json:"values,omitempty"`
   524  }