github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/local/backend_plan_test.go (about) 1 package local 2 3 import ( 4 "context" 5 "os" 6 "path/filepath" 7 "reflect" 8 "strings" 9 "testing" 10 11 "github.com/hashicorp/terraform/addrs" 12 "github.com/hashicorp/terraform/backend" 13 "github.com/hashicorp/terraform/configs/configschema" 14 "github.com/hashicorp/terraform/internal/initwd" 15 "github.com/hashicorp/terraform/plans" 16 "github.com/hashicorp/terraform/plans/planfile" 17 "github.com/hashicorp/terraform/states" 18 "github.com/hashicorp/terraform/terraform" 19 "github.com/mitchellh/cli" 20 "github.com/zclconf/go-cty/cty" 21 ) 22 23 func TestLocal_planBasic(t *testing.T) { 24 b, cleanup := TestLocal(t) 25 defer cleanup() 26 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 27 28 op, configCleanup := testOperationPlan(t, "./testdata/plan") 29 defer configCleanup() 30 op.PlanRefresh = true 31 32 run, err := b.Operation(context.Background(), op) 33 if err != nil { 34 t.Fatalf("bad: %s", err) 35 } 36 <-run.Done() 37 if run.Result != backend.OperationSuccess { 38 t.Fatalf("plan operation failed") 39 } 40 41 if !p.PlanResourceChangeCalled { 42 t.Fatal("PlanResourceChange should be called") 43 } 44 } 45 46 func TestLocal_planInAutomation(t *testing.T) { 47 b, cleanup := TestLocal(t) 48 defer cleanup() 49 TestLocalProvider(t, b, "test", planFixtureSchema()) 50 51 const msg = `You didn't specify an "-out" parameter` 52 53 // When we're "in automation" we omit certain text from the 54 // plan output. However, testing for the absense of text is 55 // unreliable in the face of future copy changes, so we'll 56 // mitigate that by running both with and without the flag 57 // set so we can ensure that the expected messages _are_ 58 // included the first time. 59 b.RunningInAutomation = false 60 b.CLI = cli.NewMockUi() 61 { 62 op, configCleanup := testOperationPlan(t, "./testdata/plan") 63 defer configCleanup() 64 op.PlanRefresh = true 65 66 run, err := b.Operation(context.Background(), op) 67 if err != nil { 68 t.Fatalf("unexpected error: %s", err) 69 } 70 <-run.Done() 71 if run.Result != backend.OperationSuccess { 72 t.Fatalf("plan operation failed") 73 } 74 75 output := b.CLI.(*cli.MockUi).OutputWriter.String() 76 if !strings.Contains(output, msg) { 77 t.Fatalf("missing next-steps message when not in automation") 78 } 79 } 80 81 // On the second run, we expect the next-steps messaging to be absent 82 // since we're now "running in automation". 83 b.RunningInAutomation = true 84 b.CLI = cli.NewMockUi() 85 { 86 op, configCleanup := testOperationPlan(t, "./testdata/plan") 87 defer configCleanup() 88 op.PlanRefresh = true 89 90 run, err := b.Operation(context.Background(), op) 91 if err != nil { 92 t.Fatalf("unexpected error: %s", err) 93 } 94 <-run.Done() 95 if run.Result != backend.OperationSuccess { 96 t.Fatalf("plan operation failed") 97 } 98 99 output := b.CLI.(*cli.MockUi).OutputWriter.String() 100 if strings.Contains(output, msg) { 101 t.Fatalf("next-steps message present when in automation") 102 } 103 } 104 105 } 106 107 func TestLocal_planNoConfig(t *testing.T) { 108 b, cleanup := TestLocal(t) 109 defer cleanup() 110 TestLocalProvider(t, b, "test", &terraform.ProviderSchema{}) 111 112 b.CLI = cli.NewMockUi() 113 114 op, configCleanup := testOperationPlan(t, "./testdata/empty") 115 defer configCleanup() 116 op.PlanRefresh = true 117 118 run, err := b.Operation(context.Background(), op) 119 if err != nil { 120 t.Fatalf("bad: %s", err) 121 } 122 <-run.Done() 123 124 if run.Result == backend.OperationSuccess { 125 t.Fatal("plan operation succeeded; want failure") 126 } 127 output := b.CLI.(*cli.MockUi).ErrorWriter.String() 128 if !strings.Contains(output, "configuration") { 129 t.Fatalf("bad: %s", err) 130 } 131 } 132 133 func TestLocal_planTainted(t *testing.T) { 134 b, cleanup := TestLocal(t) 135 defer cleanup() 136 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 137 testStateFile(t, b.StatePath, testPlanState_tainted()) 138 b.CLI = cli.NewMockUi() 139 outDir := testTempDir(t) 140 defer os.RemoveAll(outDir) 141 planPath := filepath.Join(outDir, "plan.tfplan") 142 op, configCleanup := testOperationPlan(t, "./testdata/plan") 143 defer configCleanup() 144 op.PlanRefresh = true 145 op.PlanOutPath = planPath 146 cfg := cty.ObjectVal(map[string]cty.Value{ 147 "path": cty.StringVal(b.StatePath), 148 }) 149 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 150 if err != nil { 151 t.Fatal(err) 152 } 153 op.PlanOutBackend = &plans.Backend{ 154 // Just a placeholder so that we can generate a valid plan file. 155 Type: "local", 156 Config: cfgRaw, 157 } 158 run, err := b.Operation(context.Background(), op) 159 if err != nil { 160 t.Fatalf("bad: %s", err) 161 } 162 <-run.Done() 163 if run.Result != backend.OperationSuccess { 164 t.Fatalf("plan operation failed") 165 } 166 if !p.ReadResourceCalled { 167 t.Fatal("ReadResource should be called") 168 } 169 if run.PlanEmpty { 170 t.Fatal("plan should not be empty") 171 } 172 173 expectedOutput := `An execution plan has been generated and is shown below. 174 Resource actions are indicated with the following symbols: 175 -/+ destroy and then create replacement 176 177 Terraform will perform the following actions: 178 179 # test_instance.foo is tainted, so must be replaced 180 -/+ resource "test_instance" "foo" { 181 ami = "bar" 182 183 network_interface { 184 description = "Main network interface" 185 device_index = 0 186 } 187 } 188 189 Plan: 1 to add, 0 to change, 1 to destroy.` 190 output := b.CLI.(*cli.MockUi).OutputWriter.String() 191 if !strings.Contains(output, expectedOutput) { 192 t.Fatalf("Unexpected output:\n%s", output) 193 } 194 } 195 196 func TestLocal_planDeposedOnly(t *testing.T) { 197 b, cleanup := TestLocal(t) 198 defer cleanup() 199 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 200 testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) { 201 ss.SetResourceInstanceDeposed( 202 addrs.Resource{ 203 Mode: addrs.ManagedResourceMode, 204 Type: "test_instance", 205 Name: "foo", 206 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 207 states.DeposedKey("00000000"), 208 &states.ResourceInstanceObjectSrc{ 209 Status: states.ObjectReady, 210 AttrsJSON: []byte(`{ 211 "ami": "bar", 212 "network_interface": [{ 213 "device_index": 0, 214 "description": "Main network interface" 215 }] 216 }`), 217 }, 218 addrs.ProviderConfig{ 219 Type: addrs.NewLegacyProvider("test"), 220 }.Absolute(addrs.RootModuleInstance), 221 ) 222 })) 223 b.CLI = cli.NewMockUi() 224 outDir := testTempDir(t) 225 defer os.RemoveAll(outDir) 226 planPath := filepath.Join(outDir, "plan.tfplan") 227 op, configCleanup := testOperationPlan(t, "./testdata/plan") 228 defer configCleanup() 229 op.PlanRefresh = true 230 op.PlanOutPath = planPath 231 cfg := cty.ObjectVal(map[string]cty.Value{ 232 "path": cty.StringVal(b.StatePath), 233 }) 234 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 235 if err != nil { 236 t.Fatal(err) 237 } 238 op.PlanOutBackend = &plans.Backend{ 239 // Just a placeholder so that we can generate a valid plan file. 240 Type: "local", 241 Config: cfgRaw, 242 } 243 run, err := b.Operation(context.Background(), op) 244 if err != nil { 245 t.Fatalf("bad: %s", err) 246 } 247 <-run.Done() 248 if run.Result != backend.OperationSuccess { 249 t.Fatalf("plan operation failed") 250 } 251 if !p.ReadResourceCalled { 252 t.Fatal("ReadResource should be called") 253 } 254 if run.PlanEmpty { 255 t.Fatal("plan should not be empty") 256 } 257 258 // The deposed object and the current object are distinct, so our 259 // plan includes separate actions for each of them. This strange situation 260 // is not common: it should arise only if Terraform fails during 261 // a create-before-destroy when the create hasn't completed yet but 262 // in a severe way that prevents the previous object from being restored 263 // as "current". 264 // 265 // However, that situation was more common in some earlier Terraform 266 // versions where deposed objects were not managed properly, so this 267 // can arise when upgrading from an older version with deposed objects 268 // already in the state. 269 // 270 // This is one of the few cases where we expose the idea of "deposed" in 271 // the UI, including the user-unfriendly "deposed key" (00000000 in this 272 // case) just so that users can correlate this with what they might 273 // see in `terraform show` and in the subsequent apply output, because 274 // it's also possible for there to be _multiple_ deposed objects, in the 275 // unlikely event that create_before_destroy _keeps_ crashing across 276 // subsequent runs. 277 expectedOutput := `An execution plan has been generated and is shown below. 278 Resource actions are indicated with the following symbols: 279 + create 280 - destroy 281 282 Terraform will perform the following actions: 283 284 # test_instance.foo will be created 285 + resource "test_instance" "foo" { 286 + ami = "bar" 287 288 + network_interface { 289 + description = "Main network interface" 290 + device_index = 0 291 } 292 } 293 294 # test_instance.foo (deposed object 00000000) will be destroyed 295 - resource "test_instance" "foo" { 296 - ami = "bar" -> null 297 298 - network_interface { 299 - description = "Main network interface" -> null 300 - device_index = 0 -> null 301 } 302 } 303 304 Plan: 1 to add, 0 to change, 1 to destroy.` 305 output := b.CLI.(*cli.MockUi).OutputWriter.String() 306 if !strings.Contains(output, expectedOutput) { 307 t.Fatalf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput) 308 } 309 } 310 311 func TestLocal_planTainted_createBeforeDestroy(t *testing.T) { 312 b, cleanup := TestLocal(t) 313 defer cleanup() 314 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 315 testStateFile(t, b.StatePath, testPlanState_tainted()) 316 b.CLI = cli.NewMockUi() 317 outDir := testTempDir(t) 318 defer os.RemoveAll(outDir) 319 planPath := filepath.Join(outDir, "plan.tfplan") 320 op, configCleanup := testOperationPlan(t, "./testdata/plan-cbd") 321 defer configCleanup() 322 op.PlanRefresh = true 323 op.PlanOutPath = planPath 324 cfg := cty.ObjectVal(map[string]cty.Value{ 325 "path": cty.StringVal(b.StatePath), 326 }) 327 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 328 if err != nil { 329 t.Fatal(err) 330 } 331 op.PlanOutBackend = &plans.Backend{ 332 // Just a placeholder so that we can generate a valid plan file. 333 Type: "local", 334 Config: cfgRaw, 335 } 336 run, err := b.Operation(context.Background(), op) 337 if err != nil { 338 t.Fatalf("bad: %s", err) 339 } 340 <-run.Done() 341 if run.Result != backend.OperationSuccess { 342 t.Fatalf("plan operation failed") 343 } 344 if !p.ReadResourceCalled { 345 t.Fatal("ReadResource should be called") 346 } 347 if run.PlanEmpty { 348 t.Fatal("plan should not be empty") 349 } 350 351 expectedOutput := `An execution plan has been generated and is shown below. 352 Resource actions are indicated with the following symbols: 353 +/- create replacement and then destroy 354 355 Terraform will perform the following actions: 356 357 # test_instance.foo is tainted, so must be replaced 358 +/- resource "test_instance" "foo" { 359 ami = "bar" 360 361 network_interface { 362 description = "Main network interface" 363 device_index = 0 364 } 365 } 366 367 Plan: 1 to add, 0 to change, 1 to destroy.` 368 output := b.CLI.(*cli.MockUi).OutputWriter.String() 369 if !strings.Contains(output, expectedOutput) { 370 t.Fatalf("Unexpected output:\n%s", output) 371 } 372 } 373 374 func TestLocal_planRefreshFalse(t *testing.T) { 375 b, cleanup := TestLocal(t) 376 defer cleanup() 377 378 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 379 testStateFile(t, b.StatePath, testPlanState()) 380 381 op, configCleanup := testOperationPlan(t, "./testdata/plan") 382 defer configCleanup() 383 384 run, err := b.Operation(context.Background(), op) 385 if err != nil { 386 t.Fatalf("bad: %s", err) 387 } 388 <-run.Done() 389 if run.Result != backend.OperationSuccess { 390 t.Fatalf("plan operation failed") 391 } 392 393 if p.ReadResourceCalled { 394 t.Fatal("ReadResource should not be called") 395 } 396 397 if !run.PlanEmpty { 398 t.Fatal("plan should be empty") 399 } 400 } 401 402 func TestLocal_planDestroy(t *testing.T) { 403 b, cleanup := TestLocal(t) 404 defer cleanup() 405 406 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 407 testStateFile(t, b.StatePath, testPlanState()) 408 409 outDir := testTempDir(t) 410 defer os.RemoveAll(outDir) 411 planPath := filepath.Join(outDir, "plan.tfplan") 412 413 op, configCleanup := testOperationPlan(t, "./testdata/plan") 414 defer configCleanup() 415 op.Destroy = true 416 op.PlanRefresh = true 417 op.PlanOutPath = planPath 418 cfg := cty.ObjectVal(map[string]cty.Value{ 419 "path": cty.StringVal(b.StatePath), 420 }) 421 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 422 if err != nil { 423 t.Fatal(err) 424 } 425 op.PlanOutBackend = &plans.Backend{ 426 // Just a placeholder so that we can generate a valid plan file. 427 Type: "local", 428 Config: cfgRaw, 429 } 430 431 run, err := b.Operation(context.Background(), op) 432 if err != nil { 433 t.Fatalf("bad: %s", err) 434 } 435 <-run.Done() 436 if run.Result != backend.OperationSuccess { 437 t.Fatalf("plan operation failed") 438 } 439 440 if !p.ReadResourceCalled { 441 t.Fatal("ReadResource should be called") 442 } 443 444 if run.PlanEmpty { 445 t.Fatal("plan should not be empty") 446 } 447 448 plan := testReadPlan(t, planPath) 449 for _, r := range plan.Changes.Resources { 450 if r.Action.String() != "Delete" { 451 t.Fatalf("bad: %#v", r.Action.String()) 452 } 453 } 454 } 455 456 func TestLocal_planDestroy_withDataSources(t *testing.T) { 457 b, cleanup := TestLocal(t) 458 defer cleanup() 459 460 p := TestLocalProvider(t, b, "test", planFixtureSchema()) 461 testStateFile(t, b.StatePath, testPlanState_withDataSource()) 462 463 b.CLI = cli.NewMockUi() 464 465 outDir := testTempDir(t) 466 defer os.RemoveAll(outDir) 467 planPath := filepath.Join(outDir, "plan.tfplan") 468 469 op, configCleanup := testOperationPlan(t, "./testdata/destroy-with-ds") 470 defer configCleanup() 471 op.Destroy = true 472 op.PlanRefresh = true 473 op.PlanOutPath = planPath 474 cfg := cty.ObjectVal(map[string]cty.Value{ 475 "path": cty.StringVal(b.StatePath), 476 }) 477 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 478 if err != nil { 479 t.Fatal(err) 480 } 481 op.PlanOutBackend = &plans.Backend{ 482 // Just a placeholder so that we can generate a valid plan file. 483 Type: "local", 484 Config: cfgRaw, 485 } 486 487 run, err := b.Operation(context.Background(), op) 488 if err != nil { 489 t.Fatalf("bad: %s", err) 490 } 491 <-run.Done() 492 if run.Result != backend.OperationSuccess { 493 t.Fatalf("plan operation failed") 494 } 495 496 if !p.ReadResourceCalled { 497 t.Fatal("ReadResource should be called") 498 } 499 500 if !p.ReadDataSourceCalled { 501 t.Fatal("ReadDataSourceCalled should be called") 502 } 503 504 if run.PlanEmpty { 505 t.Fatal("plan should not be empty") 506 } 507 508 // Data source should still exist in the the plan file 509 plan := testReadPlan(t, planPath) 510 if len(plan.Changes.Resources) != 2 { 511 t.Fatalf("Expected exactly 1 resource for destruction, %d given: %q", 512 len(plan.Changes.Resources), getAddrs(plan.Changes.Resources)) 513 } 514 515 // Data source should not be rendered in the output 516 expectedOutput := `Terraform will perform the following actions: 517 518 # test_instance.foo will be destroyed 519 - resource "test_instance" "foo" { 520 - ami = "bar" -> null 521 522 - network_interface { 523 - description = "Main network interface" -> null 524 - device_index = 0 -> null 525 } 526 } 527 528 Plan: 0 to add, 0 to change, 1 to destroy.` 529 530 output := b.CLI.(*cli.MockUi).OutputWriter.String() 531 if !strings.Contains(output, expectedOutput) { 532 t.Fatalf("Unexpected output (expected no data source):\n%s", output) 533 } 534 } 535 536 func getAddrs(resources []*plans.ResourceInstanceChangeSrc) []string { 537 addrs := make([]string, len(resources), len(resources)) 538 for i, r := range resources { 539 addrs[i] = r.Addr.String() 540 } 541 return addrs 542 } 543 544 func TestLocal_planOutPathNoChange(t *testing.T) { 545 b, cleanup := TestLocal(t) 546 defer cleanup() 547 TestLocalProvider(t, b, "test", planFixtureSchema()) 548 testStateFile(t, b.StatePath, testPlanState()) 549 550 outDir := testTempDir(t) 551 defer os.RemoveAll(outDir) 552 planPath := filepath.Join(outDir, "plan.tfplan") 553 554 op, configCleanup := testOperationPlan(t, "./testdata/plan") 555 defer configCleanup() 556 op.PlanOutPath = planPath 557 cfg := cty.ObjectVal(map[string]cty.Value{ 558 "path": cty.StringVal(b.StatePath), 559 }) 560 cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type()) 561 if err != nil { 562 t.Fatal(err) 563 } 564 op.PlanOutBackend = &plans.Backend{ 565 // Just a placeholder so that we can generate a valid plan file. 566 Type: "local", 567 Config: cfgRaw, 568 } 569 570 run, err := b.Operation(context.Background(), op) 571 if err != nil { 572 t.Fatalf("bad: %s", err) 573 } 574 <-run.Done() 575 if run.Result != backend.OperationSuccess { 576 t.Fatalf("plan operation failed") 577 } 578 579 plan := testReadPlan(t, planPath) 580 581 if !plan.Changes.Empty() { 582 t.Fatalf("expected empty plan to be written") 583 } 584 } 585 586 // TestLocal_planScaleOutNoDupeCount tests a Refresh/Plan sequence when a 587 // resource count is scaled out. The scaled out node needs to exist in the 588 // graph and run through a plan-style sequence during the refresh phase, but 589 // can conflate the count if its post-diff count hooks are not skipped. This 590 // checks to make sure the correct resource count is ultimately given to the 591 // UI. 592 func TestLocal_planScaleOutNoDupeCount(t *testing.T) { 593 b, cleanup := TestLocal(t) 594 defer cleanup() 595 TestLocalProvider(t, b, "test", planFixtureSchema()) 596 testStateFile(t, b.StatePath, testPlanState()) 597 598 actual := new(CountHook) 599 b.ContextOpts.Hooks = append(b.ContextOpts.Hooks, actual) 600 601 outDir := testTempDir(t) 602 defer os.RemoveAll(outDir) 603 604 op, configCleanup := testOperationPlan(t, "./testdata/plan-scaleout") 605 defer configCleanup() 606 op.PlanRefresh = true 607 608 run, err := b.Operation(context.Background(), op) 609 if err != nil { 610 t.Fatalf("bad: %s", err) 611 } 612 <-run.Done() 613 if run.Result != backend.OperationSuccess { 614 t.Fatalf("plan operation failed") 615 } 616 617 expected := new(CountHook) 618 expected.ToAdd = 1 619 expected.ToChange = 0 620 expected.ToRemoveAndAdd = 0 621 expected.ToRemove = 0 622 623 if !reflect.DeepEqual(expected, actual) { 624 t.Fatalf("Expected %#v, got %#v instead.", 625 expected, actual) 626 } 627 } 628 629 func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func()) { 630 t.Helper() 631 632 _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) 633 634 return &backend.Operation{ 635 Type: backend.OperationTypePlan, 636 ConfigDir: configDir, 637 ConfigLoader: configLoader, 638 }, configCleanup 639 } 640 641 // testPlanState is just a common state that we use for testing plan. 642 func testPlanState() *states.State { 643 state := states.NewState() 644 rootModule := state.RootModule() 645 rootModule.SetResourceInstanceCurrent( 646 addrs.Resource{ 647 Mode: addrs.ManagedResourceMode, 648 Type: "test_instance", 649 Name: "foo", 650 }.Instance(addrs.IntKey(0)), 651 &states.ResourceInstanceObjectSrc{ 652 Status: states.ObjectReady, 653 AttrsJSON: []byte(`{ 654 "ami": "bar", 655 "network_interface": [{ 656 "device_index": 0, 657 "description": "Main network interface" 658 }] 659 }`), 660 }, 661 addrs.ProviderConfig{ 662 Type: addrs.NewLegacyProvider("test"), 663 }.Absolute(addrs.RootModuleInstance), 664 ) 665 return state 666 } 667 668 func testPlanState_withDataSource() *states.State { 669 state := states.NewState() 670 rootModule := state.RootModule() 671 rootModule.SetResourceInstanceCurrent( 672 addrs.Resource{ 673 Mode: addrs.ManagedResourceMode, 674 Type: "test_instance", 675 Name: "foo", 676 }.Instance(addrs.IntKey(0)), 677 &states.ResourceInstanceObjectSrc{ 678 Status: states.ObjectReady, 679 AttrsJSON: []byte(`{ 680 "ami": "bar", 681 "network_interface": [{ 682 "device_index": 0, 683 "description": "Main network interface" 684 }] 685 }`), 686 }, 687 addrs.ProviderConfig{ 688 Type: addrs.NewLegacyProvider("test"), 689 }.Absolute(addrs.RootModuleInstance), 690 ) 691 rootModule.SetResourceInstanceCurrent( 692 addrs.Resource{ 693 Mode: addrs.DataResourceMode, 694 Type: "test_ds", 695 Name: "bar", 696 }.Instance(addrs.IntKey(0)), 697 &states.ResourceInstanceObjectSrc{ 698 Status: states.ObjectReady, 699 AttrsJSON: []byte(`{ 700 "filter": "foo" 701 }`), 702 }, 703 addrs.ProviderConfig{ 704 Type: addrs.NewLegacyProvider("test"), 705 }.Absolute(addrs.RootModuleInstance), 706 ) 707 return state 708 } 709 710 func testPlanState_tainted() *states.State { 711 state := states.NewState() 712 rootModule := state.RootModule() 713 rootModule.SetResourceInstanceCurrent( 714 addrs.Resource{ 715 Mode: addrs.ManagedResourceMode, 716 Type: "test_instance", 717 Name: "foo", 718 }.Instance(addrs.IntKey(0)), 719 &states.ResourceInstanceObjectSrc{ 720 Status: states.ObjectTainted, 721 AttrsJSON: []byte(`{ 722 "ami": "bar", 723 "network_interface": [{ 724 "device_index": 0, 725 "description": "Main network interface" 726 }] 727 }`), 728 }, 729 addrs.ProviderConfig{ 730 Type: addrs.NewLegacyProvider("test"), 731 }.Absolute(addrs.RootModuleInstance), 732 ) 733 return state 734 } 735 736 func testReadPlan(t *testing.T, path string) *plans.Plan { 737 t.Helper() 738 739 p, err := planfile.Open(path) 740 if err != nil { 741 t.Fatalf("err: %s", err) 742 } 743 defer p.Close() 744 745 plan, err := p.ReadPlan() 746 if err != nil { 747 t.Fatalf("err: %s", err) 748 } 749 750 return plan 751 } 752 753 // planFixtureSchema returns a schema suitable for processing the 754 // configuration in testdata/plan . This schema should be 755 // assigned to a mock provider named "test". 756 func planFixtureSchema() *terraform.ProviderSchema { 757 return &terraform.ProviderSchema{ 758 ResourceTypes: map[string]*configschema.Block{ 759 "test_instance": { 760 Attributes: map[string]*configschema.Attribute{ 761 "ami": {Type: cty.String, Optional: true}, 762 }, 763 BlockTypes: map[string]*configschema.NestedBlock{ 764 "network_interface": { 765 Nesting: configschema.NestingList, 766 Block: configschema.Block{ 767 Attributes: map[string]*configschema.Attribute{ 768 "device_index": {Type: cty.Number, Optional: true}, 769 "description": {Type: cty.String, Optional: true}, 770 }, 771 }, 772 }, 773 }, 774 }, 775 }, 776 DataSources: map[string]*configschema.Block{ 777 "test_ds": { 778 Attributes: map[string]*configschema.Attribute{ 779 "filter": {Type: cty.String, Required: true}, 780 }, 781 }, 782 }, 783 } 784 }