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  }