1package command
2
3import (
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/internal/addrs"
14	"github.com/hashicorp/terraform/internal/configs/configschema"
15	"github.com/hashicorp/terraform/internal/plans"
16	"github.com/hashicorp/terraform/internal/providers"
17	"github.com/hashicorp/terraform/internal/states"
18	"github.com/hashicorp/terraform/internal/terraform"
19	"github.com/mitchellh/cli"
20	"github.com/zclconf/go-cty/cty"
21)
22
23func TestShow(t *testing.T) {
24	ui := new(cli.MockUi)
25	view, _ := testView(t)
26	c := &ShowCommand{
27		Meta: Meta{
28			testingOverrides: metaOverridesForProvider(testProvider()),
29			Ui:               ui,
30			View:             view,
31		},
32	}
33
34	args := []string{
35		"bad",
36		"bad",
37	}
38	if code := c.Run(args); code != 1 {
39		t.Fatalf("bad: \n%s", ui.OutputWriter.String())
40	}
41}
42
43func TestShow_noArgs(t *testing.T) {
44	// Create the default state
45	statePath := testStateFile(t, testState())
46	stateDir := filepath.Dir(statePath)
47	defer os.RemoveAll(stateDir)
48	defer testChdir(t, stateDir)()
49
50	ui := new(cli.MockUi)
51	view, _ := testView(t)
52	c := &ShowCommand{
53		Meta: Meta{
54			testingOverrides: metaOverridesForProvider(testProvider()),
55			Ui:               ui,
56			View:             view,
57		},
58	}
59
60	// the statefile created by testStateFile is named state.tfstate
61	// so one arg is required
62	args := []string{"state.tfstate"}
63	if code := c.Run(args); code != 0 {
64		t.Fatalf("bad: \n%s", ui.OutputWriter.String())
65	}
66
67	if !strings.Contains(ui.OutputWriter.String(), "# test_instance.foo:") {
68		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
69	}
70}
71
72// https://github.com/hashicorp/terraform/issues/21462
73func TestShow_aliasedProvider(t *testing.T) {
74	// Create the default state with aliased resource
75	testState := states.BuildState(func(s *states.SyncState) {
76		s.SetResourceInstanceCurrent(
77			addrs.Resource{
78				Mode: addrs.ManagedResourceMode,
79				Type: "test_instance",
80				Name: "foo",
81			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
82			&states.ResourceInstanceObjectSrc{
83				// The weird whitespace here is reflective of how this would
84				// get written out in a real state file, due to the indentation
85				// of all of the containing wrapping objects and arrays.
86				AttrsJSON:    []byte("{\n            \"id\": \"bar\"\n          }"),
87				Status:       states.ObjectReady,
88				Dependencies: []addrs.ConfigResource{},
89			},
90			addrs.RootModuleInstance.ProviderConfigAliased(addrs.NewDefaultProvider("test"), "alias"),
91		)
92	})
93
94	statePath := testStateFile(t, testState)
95	stateDir := filepath.Dir(statePath)
96	defer os.RemoveAll(stateDir)
97	defer testChdir(t, stateDir)()
98
99	ui := new(cli.MockUi)
100	view, _ := testView(t)
101	c := &ShowCommand{
102		Meta: Meta{
103			testingOverrides: metaOverridesForProvider(testProvider()),
104			Ui:               ui,
105			View:             view,
106		},
107	}
108
109	// the statefile created by testStateFile is named state.tfstate
110	args := []string{"state.tfstate"}
111	if code := c.Run(args); code != 0 {
112		t.Fatalf("bad exit code: \n%s", ui.OutputWriter.String())
113	}
114
115	if strings.Contains(ui.OutputWriter.String(), "# missing schema for provider \"test.alias\"") {
116		t.Fatalf("bad output: \n%s", ui.OutputWriter.String())
117	}
118}
119
120func TestShow_noArgsNoState(t *testing.T) {
121	// Create the default state
122	statePath := testStateFile(t, testState())
123	stateDir := filepath.Dir(statePath)
124	defer os.RemoveAll(stateDir)
125	defer testChdir(t, stateDir)()
126
127	ui := new(cli.MockUi)
128	view, _ := testView(t)
129	c := &ShowCommand{
130		Meta: Meta{
131			testingOverrides: metaOverridesForProvider(testProvider()),
132			Ui:               ui,
133			View:             view,
134		},
135	}
136
137	// the statefile created by testStateFile is named state.tfstate
138	args := []string{"state.tfstate"}
139	if code := c.Run(args); code != 0 {
140		t.Fatalf("bad: \n%s", ui.OutputWriter.String())
141	}
142}
143
144func TestShow_planNoop(t *testing.T) {
145	planPath := testPlanFileNoop(t)
146
147	ui := cli.NewMockUi()
148	view, done := testView(t)
149	c := &ShowCommand{
150		Meta: Meta{
151			testingOverrides: metaOverridesForProvider(testProvider()),
152			Ui:               ui,
153			View:             view,
154		},
155	}
156
157	args := []string{
158		planPath,
159	}
160	if code := c.Run(args); code != 0 {
161		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
162	}
163
164	want := `No changes. Your infrastructure matches the configuration.`
165	got := done(t).Stdout()
166	if !strings.Contains(got, want) {
167		t.Errorf("missing expected output\nwant: %s\ngot:\n%s", want, got)
168	}
169}
170
171func TestShow_planWithChanges(t *testing.T) {
172	planPathWithChanges := showFixturePlanFile(t, plans.DeleteThenCreate)
173
174	ui := cli.NewMockUi()
175	view, done := testView(t)
176	c := &ShowCommand{
177		Meta: Meta{
178			testingOverrides: metaOverridesForProvider(showFixtureProvider()),
179			Ui:               ui,
180			View:             view,
181		},
182	}
183
184	args := []string{
185		planPathWithChanges,
186	}
187
188	if code := c.Run(args); code != 0 {
189		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
190	}
191
192	want := `test_instance.foo must be replaced`
193	got := done(t).Stdout()
194	if !strings.Contains(got, want) {
195		t.Errorf("missing expected output\nwant: %s\ngot:\n%s", want, got)
196	}
197}
198
199func TestShow_planWithForceReplaceChange(t *testing.T) {
200	// The main goal of this test is to see that the "replace by request"
201	// resource instance action reason can round-trip through a plan file and
202	// be reflected correctly in the "terraform show" output, the same way
203	// as it would appear in "terraform plan" output.
204
205	_, snap := testModuleWithSnapshot(t, "show")
206	plannedVal := cty.ObjectVal(map[string]cty.Value{
207		"id":  cty.UnknownVal(cty.String),
208		"ami": cty.StringVal("bar"),
209	})
210	priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type())
211	if err != nil {
212		t.Fatal(err)
213	}
214	plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type())
215	if err != nil {
216		t.Fatal(err)
217	}
218	plan := testPlan(t)
219	plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
220		Addr: addrs.Resource{
221			Mode: addrs.ManagedResourceMode,
222			Type: "test_instance",
223			Name: "foo",
224		}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
225		ProviderAddr: addrs.AbsProviderConfig{
226			Provider: addrs.NewDefaultProvider("test"),
227			Module:   addrs.RootModule,
228		},
229		ChangeSrc: plans.ChangeSrc{
230			Action: plans.CreateThenDelete,
231			Before: priorValRaw,
232			After:  plannedValRaw,
233		},
234		ActionReason: plans.ResourceInstanceReplaceByRequest,
235	})
236	planFilePath := testPlanFile(
237		t,
238		snap,
239		states.NewState(),
240		plan,
241	)
242
243	ui := cli.NewMockUi()
244	view, done := testView(t)
245	c := &ShowCommand{
246		Meta: Meta{
247			testingOverrides: metaOverridesForProvider(showFixtureProvider()),
248			Ui:               ui,
249			View:             view,
250		},
251	}
252
253	args := []string{
254		planFilePath,
255	}
256
257	if code := c.Run(args); code != 0 {
258		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
259	}
260
261	got := done(t).Stdout()
262	if want := `test_instance.foo will be replaced, as requested`; !strings.Contains(got, want) {
263		t.Errorf("wrong output\ngot:\n%s\n\nwant substring: %s", got, want)
264	}
265	if want := `Plan: 1 to add, 0 to change, 1 to destroy.`; !strings.Contains(got, want) {
266		t.Errorf("wrong output\ngot:\n%s\n\nwant substring: %s", got, want)
267	}
268
269}
270
271func TestShow_plan_json(t *testing.T) {
272	planPath := showFixturePlanFile(t, plans.Create)
273
274	ui := new(cli.MockUi)
275	view, _ := testView(t)
276	c := &ShowCommand{
277		Meta: Meta{
278			testingOverrides: metaOverridesForProvider(showFixtureProvider()),
279			Ui:               ui,
280			View:             view,
281		},
282	}
283
284	args := []string{
285		"-json",
286		planPath,
287	}
288	if code := c.Run(args); code != 0 {
289		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
290	}
291}
292
293func TestShow_state(t *testing.T) {
294	originalState := testState()
295	statePath := testStateFile(t, originalState)
296	defer os.RemoveAll(filepath.Dir(statePath))
297
298	ui := new(cli.MockUi)
299	view, _ := testView(t)
300	c := &ShowCommand{
301		Meta: Meta{
302			testingOverrides: metaOverridesForProvider(testProvider()),
303			Ui:               ui,
304			View:             view,
305		},
306	}
307
308	args := []string{
309		statePath,
310	}
311	if code := c.Run(args); code != 0 {
312		t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
313	}
314}
315
316func TestShow_json_output(t *testing.T) {
317	fixtureDir := "testdata/show-json"
318	testDirs, err := ioutil.ReadDir(fixtureDir)
319	if err != nil {
320		t.Fatal(err)
321	}
322
323	for _, entry := range testDirs {
324		if !entry.IsDir() {
325			continue
326		}
327
328		t.Run(entry.Name(), func(t *testing.T) {
329			td := tempDir(t)
330			inputDir := filepath.Join(fixtureDir, entry.Name())
331			testCopyDir(t, inputDir, td)
332			defer os.RemoveAll(td)
333			defer testChdir(t, td)()
334
335			expectError := strings.Contains(entry.Name(), "error")
336
337			providerSource, close := newMockProviderSource(t, map[string][]string{
338				"test": {"1.2.3"},
339			})
340			defer close()
341
342			p := showFixtureProvider()
343			ui := new(cli.MockUi)
344			view, _ := testView(t)
345			m := Meta{
346				testingOverrides: metaOverridesForProvider(p),
347				Ui:               ui,
348				View:             view,
349				ProviderSource:   providerSource,
350			}
351
352			// init
353			ic := &InitCommand{
354				Meta: m,
355			}
356			if code := ic.Run([]string{}); code != 0 {
357				if expectError {
358					// this should error, but not panic.
359					return
360				}
361				t.Fatalf("init failed\n%s", ui.ErrorWriter)
362			}
363
364			pc := &PlanCommand{
365				Meta: m,
366			}
367
368			args := []string{
369				"-out=terraform.plan",
370			}
371
372			if code := pc.Run(args); code != 0 {
373				t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
374			}
375
376			// flush the plan output from the mock ui
377			ui.OutputWriter.Reset()
378			sc := &ShowCommand{
379				Meta: m,
380			}
381
382			args = []string{
383				"-json",
384				"terraform.plan",
385			}
386			defer os.Remove("terraform.plan")
387
388			if code := sc.Run(args); code != 0 {
389				t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
390			}
391
392			// compare ui output to wanted output
393			var got, want plan
394
395			gotString := ui.OutputWriter.String()
396			json.Unmarshal([]byte(gotString), &got)
397
398			wantFile, err := os.Open("output.json")
399			if err != nil {
400				t.Fatalf("err: %s", err)
401			}
402			defer wantFile.Close()
403			byteValue, err := ioutil.ReadAll(wantFile)
404			if err != nil {
405				t.Fatalf("err: %s", err)
406			}
407			json.Unmarshal([]byte(byteValue), &want)
408
409			if !cmp.Equal(got, want) {
410				t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
411			}
412		})
413	}
414}
415
416func TestShow_json_output_sensitive(t *testing.T) {
417	td := tempDir(t)
418	inputDir := "testdata/show-json-sensitive"
419	testCopyDir(t, inputDir, td)
420	defer os.RemoveAll(td)
421	defer testChdir(t, td)()
422
423	providerSource, close := newMockProviderSource(t, map[string][]string{"test": {"1.2.3"}})
424	defer close()
425
426	p := showFixtureSensitiveProvider()
427	ui := new(cli.MockUi)
428	view, _ := testView(t)
429	m := Meta{
430		testingOverrides: metaOverridesForProvider(p),
431		Ui:               ui,
432		View:             view,
433		ProviderSource:   providerSource,
434	}
435
436	// init
437	ic := &InitCommand{
438		Meta: m,
439	}
440	if code := ic.Run([]string{}); code != 0 {
441		t.Fatalf("init failed\n%s", ui.ErrorWriter)
442	}
443
444	// flush init output
445	ui.OutputWriter.Reset()
446
447	pc := &PlanCommand{
448		Meta: m,
449	}
450
451	args := []string{
452		"-out=terraform.plan",
453	}
454
455	if code := pc.Run(args); code != 0 {
456		fmt.Println(ui.OutputWriter.String())
457		t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
458	}
459
460	// flush the plan output from the mock ui
461	ui.OutputWriter.Reset()
462	sc := &ShowCommand{
463		Meta: m,
464	}
465
466	args = []string{
467		"-json",
468		"terraform.plan",
469	}
470	defer os.Remove("terraform.plan")
471
472	if code := sc.Run(args); code != 0 {
473		t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
474	}
475
476	// compare ui output to wanted output
477	var got, want plan
478
479	gotString := ui.OutputWriter.String()
480	json.Unmarshal([]byte(gotString), &got)
481
482	wantFile, err := os.Open("output.json")
483	if err != nil {
484		t.Fatalf("err: %s", err)
485	}
486	defer wantFile.Close()
487	byteValue, err := ioutil.ReadAll(wantFile)
488	if err != nil {
489		t.Fatalf("err: %s", err)
490	}
491	json.Unmarshal([]byte(byteValue), &want)
492
493	if !cmp.Equal(got, want) {
494		t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
495	}
496}
497
498// similar test as above, without the plan
499func TestShow_json_output_state(t *testing.T) {
500	fixtureDir := "testdata/show-json-state"
501	testDirs, err := ioutil.ReadDir(fixtureDir)
502	if err != nil {
503		t.Fatal(err)
504	}
505
506	for _, entry := range testDirs {
507		if !entry.IsDir() {
508			continue
509		}
510
511		t.Run(entry.Name(), func(t *testing.T) {
512			td := tempDir(t)
513			inputDir := filepath.Join(fixtureDir, entry.Name())
514			testCopyDir(t, inputDir, td)
515			defer os.RemoveAll(td)
516			defer testChdir(t, td)()
517
518			providerSource, close := newMockProviderSource(t, map[string][]string{
519				"test": {"1.2.3"},
520			})
521			defer close()
522
523			p := showFixtureProvider()
524			ui := new(cli.MockUi)
525			view, _ := testView(t)
526			m := Meta{
527				testingOverrides: metaOverridesForProvider(p),
528				Ui:               ui,
529				View:             view,
530				ProviderSource:   providerSource,
531			}
532
533			// init
534			ic := &InitCommand{
535				Meta: m,
536			}
537			if code := ic.Run([]string{}); code != 0 {
538				t.Fatalf("init failed\n%s", ui.ErrorWriter)
539			}
540
541			// flush the plan output from the mock ui
542			ui.OutputWriter.Reset()
543			sc := &ShowCommand{
544				Meta: m,
545			}
546
547			if code := sc.Run([]string{"-json"}); code != 0 {
548				t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String())
549			}
550
551			// compare ui output to wanted output
552			type state struct {
553				FormatVersion    string                 `json:"format_version,omitempty"`
554				TerraformVersion string                 `json:"terraform_version"`
555				Values           map[string]interface{} `json:"values,omitempty"`
556				SensitiveValues  map[string]bool        `json:"sensitive_values,omitempty"`
557			}
558			var got, want state
559
560			gotString := ui.OutputWriter.String()
561			json.Unmarshal([]byte(gotString), &got)
562
563			wantFile, err := os.Open("output.json")
564			if err != nil {
565				t.Fatalf("err: %s", err)
566			}
567			defer wantFile.Close()
568			byteValue, err := ioutil.ReadAll(wantFile)
569			if err != nil {
570				t.Fatalf("err: %s", err)
571			}
572			json.Unmarshal([]byte(byteValue), &want)
573
574			if !cmp.Equal(got, want) {
575				t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
576			}
577		})
578	}
579}
580
581// showFixtureSchema returns a schema suitable for processing the configuration
582// in testdata/show. This schema should be assigned to a mock provider
583// named "test".
584func showFixtureSchema() *providers.GetProviderSchemaResponse {
585	return &providers.GetProviderSchemaResponse{
586		Provider: providers.Schema{
587			Block: &configschema.Block{
588				Attributes: map[string]*configschema.Attribute{
589					"region": {Type: cty.String, Optional: true},
590				},
591			},
592		},
593		ResourceTypes: map[string]providers.Schema{
594			"test_instance": {
595				Block: &configschema.Block{
596					Attributes: map[string]*configschema.Attribute{
597						"id":  {Type: cty.String, Optional: true, Computed: true},
598						"ami": {Type: cty.String, Optional: true},
599					},
600				},
601			},
602		},
603	}
604}
605
606// showFixtureSensitiveSchema returns a schema suitable for processing the configuration
607// in testdata/show. This schema should be assigned to a mock provider
608// named "test". It includes a sensitive attribute.
609func showFixtureSensitiveSchema() *providers.GetProviderSchemaResponse {
610	return &providers.GetProviderSchemaResponse{
611		Provider: providers.Schema{
612			Block: &configschema.Block{
613				Attributes: map[string]*configschema.Attribute{
614					"region": {Type: cty.String, Optional: true},
615				},
616			},
617		},
618		ResourceTypes: map[string]providers.Schema{
619			"test_instance": {
620				Block: &configschema.Block{
621					Attributes: map[string]*configschema.Attribute{
622						"id":       {Type: cty.String, Optional: true, Computed: true},
623						"ami":      {Type: cty.String, Optional: true},
624						"password": {Type: cty.String, Optional: true, Sensitive: true},
625					},
626				},
627			},
628		},
629	}
630}
631
632// showFixtureProvider returns a mock provider that is configured for basic
633// operation with the configuration in testdata/show. This mock has
634// GetSchemaResponse, PlanResourceChangeFn, and ApplyResourceChangeFn populated,
635// with the plan/apply steps just passing through the data determined by
636// Terraform Core.
637func showFixtureProvider() *terraform.MockProvider {
638	p := testProvider()
639	p.GetProviderSchemaResponse = showFixtureSchema()
640	p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse {
641		idVal := req.PriorState.GetAttr("id")
642		amiVal := req.PriorState.GetAttr("ami")
643		if amiVal.RawEquals(cty.StringVal("refresh-me")) {
644			amiVal = cty.StringVal("refreshed")
645		}
646		return providers.ReadResourceResponse{
647			NewState: cty.ObjectVal(map[string]cty.Value{
648				"id":  idVal,
649				"ami": amiVal,
650			}),
651			Private: req.Private,
652		}
653	}
654	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
655		idVal := req.ProposedNewState.GetAttr("id")
656		amiVal := req.ProposedNewState.GetAttr("ami")
657		if idVal.IsNull() {
658			idVal = cty.UnknownVal(cty.String)
659		}
660		var reqRep []cty.Path
661		if amiVal.RawEquals(cty.StringVal("force-replace")) {
662			reqRep = append(reqRep, cty.GetAttrPath("ami"))
663		}
664		return providers.PlanResourceChangeResponse{
665			PlannedState: cty.ObjectVal(map[string]cty.Value{
666				"id":  idVal,
667				"ami": amiVal,
668			}),
669			RequiresReplace: reqRep,
670		}
671	}
672	p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
673		idVal := req.PlannedState.GetAttr("id")
674		amiVal := req.PlannedState.GetAttr("ami")
675		if !idVal.IsKnown() {
676			idVal = cty.StringVal("placeholder")
677		}
678		return providers.ApplyResourceChangeResponse{
679			NewState: cty.ObjectVal(map[string]cty.Value{
680				"id":  idVal,
681				"ami": amiVal,
682			}),
683		}
684	}
685	return p
686}
687
688// showFixtureSensitiveProvider returns a mock provider that is configured for basic
689// operation with the configuration in testdata/show. This mock has
690// GetSchemaResponse, PlanResourceChangeFn, and ApplyResourceChangeFn populated,
691// with the plan/apply steps just passing through the data determined by
692// Terraform Core. It also has a sensitive attribute in the provider schema.
693func showFixtureSensitiveProvider() *terraform.MockProvider {
694	p := testProvider()
695	p.GetProviderSchemaResponse = showFixtureSensitiveSchema()
696	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
697		idVal := req.ProposedNewState.GetAttr("id")
698		if idVal.IsNull() {
699			idVal = cty.UnknownVal(cty.String)
700		}
701		return providers.PlanResourceChangeResponse{
702			PlannedState: cty.ObjectVal(map[string]cty.Value{
703				"id":       idVal,
704				"ami":      req.ProposedNewState.GetAttr("ami"),
705				"password": req.ProposedNewState.GetAttr("password"),
706			}),
707		}
708	}
709	p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
710		idVal := req.PlannedState.GetAttr("id")
711		if !idVal.IsKnown() {
712			idVal = cty.StringVal("placeholder")
713		}
714		return providers.ApplyResourceChangeResponse{
715			NewState: cty.ObjectVal(map[string]cty.Value{
716				"id":       idVal,
717				"ami":      req.PlannedState.GetAttr("ami"),
718				"password": req.PlannedState.GetAttr("password"),
719			}),
720		}
721	}
722	return p
723}
724
725// showFixturePlanFile creates a plan file at a temporary location containing a
726// single change to create or update the test_instance.foo that is included in the "show"
727// test fixture, returning the location of that plan file.
728// `action` is the planned change you would like to elicit
729func showFixturePlanFile(t *testing.T, action plans.Action) string {
730	_, snap := testModuleWithSnapshot(t, "show")
731	plannedVal := cty.ObjectVal(map[string]cty.Value{
732		"id":  cty.UnknownVal(cty.String),
733		"ami": cty.StringVal("bar"),
734	})
735	priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type())
736	if err != nil {
737		t.Fatal(err)
738	}
739	plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type())
740	if err != nil {
741		t.Fatal(err)
742	}
743	plan := testPlan(t)
744	plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
745		Addr: addrs.Resource{
746			Mode: addrs.ManagedResourceMode,
747			Type: "test_instance",
748			Name: "foo",
749		}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
750		ProviderAddr: addrs.AbsProviderConfig{
751			Provider: addrs.NewDefaultProvider("test"),
752			Module:   addrs.RootModule,
753		},
754		ChangeSrc: plans.ChangeSrc{
755			Action: action,
756			Before: priorValRaw,
757			After:  plannedValRaw,
758		},
759	})
760	return testPlanFile(
761		t,
762		snap,
763		states.NewState(),
764		plan,
765	)
766}
767
768// this simplified plan struct allows us to preserve field order when marshaling
769// the command output. NOTE: we are leaving "terraform_version" out of this test
770// to avoid needing to constantly update the expected output; as a potential
771// TODO we could write a jsonplan compare function.
772type plan struct {
773	FormatVersion   string                 `json:"format_version,omitempty"`
774	Variables       map[string]interface{} `json:"variables,omitempty"`
775	PlannedValues   map[string]interface{} `json:"planned_values,omitempty"`
776	ResourceDrift   []interface{}          `json:"resource_drift,omitempty"`
777	ResourceChanges []interface{}          `json:"resource_changes,omitempty"`
778	OutputChanges   map[string]interface{} `json:"output_changes,omitempty"`
779	PriorState      priorState             `json:"prior_state,omitempty"`
780	Config          map[string]interface{} `json:"configuration,omitempty"`
781}
782
783type priorState struct {
784	FormatVersion   string                 `json:"format_version,omitempty"`
785	Values          map[string]interface{} `json:"values,omitempty"`
786	SensitiveValues map[string]bool        `json:"sensitive_values,omitempty"`
787}
788