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