kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/backend/local/backend_plan_test.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  	"testing"
     9  
    10  	"kubeform.dev/terraform-backend-sdk/addrs"
    11  	"kubeform.dev/terraform-backend-sdk/backend"
    12  	"kubeform.dev/terraform-backend-sdk/command/arguments"
    13  	"kubeform.dev/terraform-backend-sdk/command/clistate"
    14  	"kubeform.dev/terraform-backend-sdk/command/views"
    15  	"kubeform.dev/terraform-backend-sdk/configs/configschema"
    16  	"kubeform.dev/terraform-backend-sdk/initwd"
    17  	"kubeform.dev/terraform-backend-sdk/plans"
    18  	"kubeform.dev/terraform-backend-sdk/plans/planfile"
    19  	"kubeform.dev/terraform-backend-sdk/states"
    20  	"kubeform.dev/terraform-backend-sdk/terminal"
    21  	"kubeform.dev/terraform-backend-sdk/terraform"
    22  	"github.com/zclconf/go-cty/cty"
    23  )
    24  
    25  func TestLocal_planBasic(t *testing.T) {
    26  	b, cleanup := TestLocal(t)
    27  	defer cleanup()
    28  	p := TestLocalProvider(t, b, "test", planFixtureSchema())
    29  
    30  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
    31  	defer configCleanup()
    32  	op.PlanRefresh = true
    33  
    34  	run, err := b.Operation(context.Background(), op)
    35  	if err != nil {
    36  		t.Fatalf("bad: %s", err)
    37  	}
    38  	<-run.Done()
    39  	if run.Result != backend.OperationSuccess {
    40  		t.Fatalf("plan operation failed")
    41  	}
    42  
    43  	if !p.PlanResourceChangeCalled {
    44  		t.Fatal("PlanResourceChange should be called")
    45  	}
    46  
    47  	// the backend should be unlocked after a run
    48  	assertBackendStateUnlocked(t, b)
    49  
    50  	if errOutput := done(t).Stderr(); errOutput != "" {
    51  		t.Fatalf("unexpected error output:\n%s", errOutput)
    52  	}
    53  }
    54  
    55  func TestLocal_planInAutomation(t *testing.T) {
    56  	b, cleanup := TestLocal(t)
    57  	defer cleanup()
    58  	TestLocalProvider(t, b, "test", planFixtureSchema())
    59  
    60  	const msg = `You didn't use the -out option`
    61  
    62  	// When we're "in automation" we omit certain text from the plan output.
    63  	// However, the responsibility for this omission is in the view, so here we
    64  	// test for its presence while the "in automation" setting is false, to
    65  	// validate that we are calling the correct view method.
    66  	//
    67  	// Ideally this test would be replaced by a call-logging mock view, but
    68  	// that's future work.
    69  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
    70  	defer configCleanup()
    71  	op.PlanRefresh = true
    72  
    73  	run, err := b.Operation(context.Background(), op)
    74  	if err != nil {
    75  		t.Fatalf("unexpected error: %s", err)
    76  	}
    77  	<-run.Done()
    78  	if run.Result != backend.OperationSuccess {
    79  		t.Fatalf("plan operation failed")
    80  	}
    81  
    82  	if output := done(t).Stdout(); !strings.Contains(output, msg) {
    83  		t.Fatalf("missing next-steps message when not in automation\nwant: %s\noutput:\n%s", msg, output)
    84  	}
    85  }
    86  
    87  func TestLocal_planNoConfig(t *testing.T) {
    88  	b, cleanup := TestLocal(t)
    89  	defer cleanup()
    90  	TestLocalProvider(t, b, "test", &terraform.ProviderSchema{})
    91  
    92  	op, configCleanup, done := testOperationPlan(t, "./testdata/empty")
    93  	defer configCleanup()
    94  	op.PlanRefresh = true
    95  
    96  	run, err := b.Operation(context.Background(), op)
    97  	if err != nil {
    98  		t.Fatalf("bad: %s", err)
    99  	}
   100  	<-run.Done()
   101  
   102  	output := done(t)
   103  
   104  	if run.Result == backend.OperationSuccess {
   105  		t.Fatal("plan operation succeeded; want failure")
   106  	}
   107  
   108  	if stderr := output.Stderr(); !strings.Contains(stderr, "No configuration files") {
   109  		t.Fatalf("bad: %s", stderr)
   110  	}
   111  
   112  	// the backend should be unlocked after a run
   113  	assertBackendStateUnlocked(t, b)
   114  }
   115  
   116  // This test validates the state lacking behavior when the inner call to
   117  // Context() fails
   118  func TestLocal_plan_context_error(t *testing.T) {
   119  	b, cleanup := TestLocal(t)
   120  	defer cleanup()
   121  
   122  	// This is an intentionally-invalid value to make terraform.NewContext fail
   123  	// when b.Operation calls it.
   124  	// NOTE: This test was originally using a provider initialization failure
   125  	// as its forced error condition, but terraform.NewContext is no longer
   126  	// responsible for checking that. Invalid parallelism is the last situation
   127  	// where terraform.NewContext can return error diagnostics, and arguably
   128  	// we should be validating this argument at the UI layer anyway, so perhaps
   129  	// in future we'll make terraform.NewContext never return errors and then
   130  	// this test will become redundant, because its purpose is specifically
   131  	// to test that we properly unlock the state if terraform.NewContext
   132  	// returns an error.
   133  	if b.ContextOpts == nil {
   134  		b.ContextOpts = &terraform.ContextOpts{}
   135  	}
   136  	b.ContextOpts.Parallelism = -1
   137  
   138  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   139  	defer configCleanup()
   140  
   141  	// we coerce a failure in Context() by omitting the provider schema
   142  	run, err := b.Operation(context.Background(), op)
   143  	if err != nil {
   144  		t.Fatalf("bad: %s", err)
   145  	}
   146  	<-run.Done()
   147  	if run.Result != backend.OperationFailure {
   148  		t.Fatalf("plan operation succeeded")
   149  	}
   150  
   151  	// the backend should be unlocked after a run
   152  	assertBackendStateUnlocked(t, b)
   153  
   154  	if got, want := done(t).Stderr(), "Error: Invalid parallelism value"; !strings.Contains(got, want) {
   155  		t.Fatalf("unexpected error output:\n%s\nwant: %s", got, want)
   156  	}
   157  }
   158  
   159  func TestLocal_planOutputsChanged(t *testing.T) {
   160  	b, cleanup := TestLocal(t)
   161  	defer cleanup()
   162  	testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) {
   163  		ss.SetOutputValue(addrs.AbsOutputValue{
   164  			Module:      addrs.RootModuleInstance,
   165  			OutputValue: addrs.OutputValue{Name: "changed"},
   166  		}, cty.StringVal("before"), false)
   167  		ss.SetOutputValue(addrs.AbsOutputValue{
   168  			Module:      addrs.RootModuleInstance,
   169  			OutputValue: addrs.OutputValue{Name: "sensitive_before"},
   170  		}, cty.StringVal("before"), true)
   171  		ss.SetOutputValue(addrs.AbsOutputValue{
   172  			Module:      addrs.RootModuleInstance,
   173  			OutputValue: addrs.OutputValue{Name: "sensitive_after"},
   174  		}, cty.StringVal("before"), false)
   175  		ss.SetOutputValue(addrs.AbsOutputValue{
   176  			Module:      addrs.RootModuleInstance,
   177  			OutputValue: addrs.OutputValue{Name: "removed"}, // not present in the config fixture
   178  		}, cty.StringVal("before"), false)
   179  		ss.SetOutputValue(addrs.AbsOutputValue{
   180  			Module:      addrs.RootModuleInstance,
   181  			OutputValue: addrs.OutputValue{Name: "unchanged"},
   182  		}, cty.StringVal("before"), false)
   183  		// NOTE: This isn't currently testing the situation where the new
   184  		// value of an output is unknown, because to do that requires there to
   185  		// be at least one managed resource Create action in the plan and that
   186  		// would defeat the point of this test, which is to ensure that a
   187  		// plan containing only output changes is considered "non-empty".
   188  		// For now we're not too worried about testing the "new value is
   189  		// unknown" situation because that's already common for printing out
   190  		// resource changes and we already have many tests for that.
   191  	}))
   192  	outDir := t.TempDir()
   193  	defer os.RemoveAll(outDir)
   194  	planPath := filepath.Join(outDir, "plan.tfplan")
   195  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-outputs-changed")
   196  	defer configCleanup()
   197  	op.PlanRefresh = true
   198  	op.PlanOutPath = planPath
   199  	cfg := cty.ObjectVal(map[string]cty.Value{
   200  		"path": cty.StringVal(b.StatePath),
   201  	})
   202  	cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
   203  	if err != nil {
   204  		t.Fatal(err)
   205  	}
   206  	op.PlanOutBackend = &plans.Backend{
   207  		// Just a placeholder so that we can generate a valid plan file.
   208  		Type:   "local",
   209  		Config: cfgRaw,
   210  	}
   211  	run, err := b.Operation(context.Background(), op)
   212  	if err != nil {
   213  		t.Fatalf("bad: %s", err)
   214  	}
   215  	<-run.Done()
   216  	if run.Result != backend.OperationSuccess {
   217  		t.Fatalf("plan operation failed")
   218  	}
   219  	if run.PlanEmpty {
   220  		t.Error("plan should not be empty")
   221  	}
   222  
   223  	expectedOutput := strings.TrimSpace(`
   224  Changes to Outputs:
   225    + added            = "after"
   226    ~ changed          = "before" -> "after"
   227    - removed          = "before" -> null
   228    ~ sensitive_after  = (sensitive value)
   229    ~ sensitive_before = (sensitive value)
   230  
   231  You can apply this plan to save these new output values to the Terraform
   232  state, without changing any real infrastructure.
   233  `)
   234  
   235  	if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
   236  		t.Errorf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput)
   237  	}
   238  }
   239  
   240  // Module outputs should not cause the plan to be rendered
   241  func TestLocal_planModuleOutputsChanged(t *testing.T) {
   242  	b, cleanup := TestLocal(t)
   243  	defer cleanup()
   244  	testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) {
   245  		ss.SetOutputValue(addrs.AbsOutputValue{
   246  			Module:      addrs.RootModuleInstance.Child("mod", addrs.NoKey),
   247  			OutputValue: addrs.OutputValue{Name: "changed"},
   248  		}, cty.StringVal("before"), false)
   249  	}))
   250  	outDir := t.TempDir()
   251  	defer os.RemoveAll(outDir)
   252  	planPath := filepath.Join(outDir, "plan.tfplan")
   253  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-module-outputs-changed")
   254  	defer configCleanup()
   255  	op.PlanRefresh = true
   256  	op.PlanOutPath = planPath
   257  	cfg := cty.ObjectVal(map[string]cty.Value{
   258  		"path": cty.StringVal(b.StatePath),
   259  	})
   260  	cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
   261  	if err != nil {
   262  		t.Fatal(err)
   263  	}
   264  	op.PlanOutBackend = &plans.Backend{
   265  		Type:   "local",
   266  		Config: cfgRaw,
   267  	}
   268  	run, err := b.Operation(context.Background(), op)
   269  	if err != nil {
   270  		t.Fatalf("bad: %s", err)
   271  	}
   272  	<-run.Done()
   273  	if run.Result != backend.OperationSuccess {
   274  		t.Fatalf("plan operation failed")
   275  	}
   276  	if !run.PlanEmpty {
   277  		t.Fatal("plan should be empty")
   278  	}
   279  
   280  	expectedOutput := strings.TrimSpace(`
   281  No changes. Your infrastructure matches the configuration.
   282  `)
   283  	if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
   284  		t.Fatalf("Unexpected output:\n%s\n\nwant output containing:\n%s", output, expectedOutput)
   285  	}
   286  }
   287  
   288  func TestLocal_planTainted(t *testing.T) {
   289  	b, cleanup := TestLocal(t)
   290  	defer cleanup()
   291  	p := TestLocalProvider(t, b, "test", planFixtureSchema())
   292  	testStateFile(t, b.StatePath, testPlanState_tainted())
   293  	outDir := t.TempDir()
   294  	planPath := filepath.Join(outDir, "plan.tfplan")
   295  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   296  	defer configCleanup()
   297  	op.PlanRefresh = true
   298  	op.PlanOutPath = planPath
   299  	cfg := cty.ObjectVal(map[string]cty.Value{
   300  		"path": cty.StringVal(b.StatePath),
   301  	})
   302  	cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
   303  	if err != nil {
   304  		t.Fatal(err)
   305  	}
   306  	op.PlanOutBackend = &plans.Backend{
   307  		// Just a placeholder so that we can generate a valid plan file.
   308  		Type:   "local",
   309  		Config: cfgRaw,
   310  	}
   311  	run, err := b.Operation(context.Background(), op)
   312  	if err != nil {
   313  		t.Fatalf("bad: %s", err)
   314  	}
   315  	<-run.Done()
   316  	if run.Result != backend.OperationSuccess {
   317  		t.Fatalf("plan operation failed")
   318  	}
   319  	if !p.ReadResourceCalled {
   320  		t.Fatal("ReadResource should be called")
   321  	}
   322  	if run.PlanEmpty {
   323  		t.Fatal("plan should not be empty")
   324  	}
   325  
   326  	expectedOutput := `Terraform used the selected providers to generate the following execution
   327  plan. Resource actions are indicated with the following symbols:
   328  -/+ destroy and then create replacement
   329  
   330  Terraform will perform the following actions:
   331  
   332    # test_instance.foo is tainted, so must be replaced
   333  -/+ resource "test_instance" "foo" {
   334          # (1 unchanged attribute hidden)
   335  
   336          # (1 unchanged block hidden)
   337      }
   338  
   339  Plan: 1 to add, 0 to change, 1 to destroy.`
   340  	if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
   341  		t.Fatalf("Unexpected output\ngot\n%s\n\nwant:\n%s", output, expectedOutput)
   342  	}
   343  }
   344  
   345  func TestLocal_planDeposedOnly(t *testing.T) {
   346  	b, cleanup := TestLocal(t)
   347  	defer cleanup()
   348  	p := TestLocalProvider(t, b, "test", planFixtureSchema())
   349  	testStateFile(t, b.StatePath, states.BuildState(func(ss *states.SyncState) {
   350  		ss.SetResourceInstanceDeposed(
   351  			addrs.Resource{
   352  				Mode: addrs.ManagedResourceMode,
   353  				Type: "test_instance",
   354  				Name: "foo",
   355  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   356  			states.DeposedKey("00000000"),
   357  			&states.ResourceInstanceObjectSrc{
   358  				Status: states.ObjectReady,
   359  				AttrsJSON: []byte(`{
   360  				"ami": "bar",
   361  				"network_interface": [{
   362  					"device_index": 0,
   363  					"description": "Main network interface"
   364  				}]
   365  			}`),
   366  			},
   367  			addrs.AbsProviderConfig{
   368  				Provider: addrs.NewDefaultProvider("test"),
   369  				Module:   addrs.RootModule,
   370  			},
   371  		)
   372  	}))
   373  	outDir := t.TempDir()
   374  	planPath := filepath.Join(outDir, "plan.tfplan")
   375  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   376  	defer configCleanup()
   377  	op.PlanRefresh = true
   378  	op.PlanOutPath = planPath
   379  	cfg := cty.ObjectVal(map[string]cty.Value{
   380  		"path": cty.StringVal(b.StatePath),
   381  	})
   382  	cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
   383  	if err != nil {
   384  		t.Fatal(err)
   385  	}
   386  	op.PlanOutBackend = &plans.Backend{
   387  		// Just a placeholder so that we can generate a valid plan file.
   388  		Type:   "local",
   389  		Config: cfgRaw,
   390  	}
   391  	run, err := b.Operation(context.Background(), op)
   392  	if err != nil {
   393  		t.Fatalf("bad: %s", err)
   394  	}
   395  	<-run.Done()
   396  	if run.Result != backend.OperationSuccess {
   397  		t.Fatalf("plan operation failed")
   398  	}
   399  	if !p.ReadResourceCalled {
   400  		t.Fatal("ReadResource should've been called to refresh the deposed object")
   401  	}
   402  	if run.PlanEmpty {
   403  		t.Fatal("plan should not be empty")
   404  	}
   405  
   406  	// The deposed object and the current object are distinct, so our
   407  	// plan includes separate actions for each of them. This strange situation
   408  	// is not common: it should arise only if Terraform fails during
   409  	// a create-before-destroy when the create hasn't completed yet but
   410  	// in a severe way that prevents the previous object from being restored
   411  	// as "current".
   412  	//
   413  	// However, that situation was more common in some earlier Terraform
   414  	// versions where deposed objects were not managed properly, so this
   415  	// can arise when upgrading from an older version with deposed objects
   416  	// already in the state.
   417  	//
   418  	// This is one of the few cases where we expose the idea of "deposed" in
   419  	// the UI, including the user-unfriendly "deposed key" (00000000 in this
   420  	// case) just so that users can correlate this with what they might
   421  	// see in `terraform show` and in the subsequent apply output, because
   422  	// it's also possible for there to be _multiple_ deposed objects, in the
   423  	// unlikely event that create_before_destroy _keeps_ crashing across
   424  	// subsequent runs.
   425  	expectedOutput := `Terraform used the selected providers to generate the following execution
   426  plan. Resource actions are indicated with the following symbols:
   427    + create
   428    - destroy
   429  
   430  Terraform will perform the following actions:
   431  
   432    # test_instance.foo will be created
   433    + resource "test_instance" "foo" {
   434        + ami = "bar"
   435  
   436        + network_interface {
   437            + description  = "Main network interface"
   438            + device_index = 0
   439          }
   440      }
   441  
   442    # test_instance.foo (deposed object 00000000) will be destroyed
   443    # (left over from a partially-failed replacement of this instance)
   444    - resource "test_instance" "foo" {
   445        - ami = "bar" -> null
   446  
   447        - network_interface {
   448            - description  = "Main network interface" -> null
   449            - device_index = 0 -> null
   450          }
   451      }
   452  
   453  Plan: 1 to add, 0 to change, 1 to destroy.`
   454  	if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
   455  		t.Fatalf("Unexpected output:\n%s", output)
   456  	}
   457  }
   458  
   459  func TestLocal_planTainted_createBeforeDestroy(t *testing.T) {
   460  	b, cleanup := TestLocal(t)
   461  	defer cleanup()
   462  	p := TestLocalProvider(t, b, "test", planFixtureSchema())
   463  	testStateFile(t, b.StatePath, testPlanState_tainted())
   464  	outDir := t.TempDir()
   465  	planPath := filepath.Join(outDir, "plan.tfplan")
   466  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-cbd")
   467  	defer configCleanup()
   468  	op.PlanRefresh = true
   469  	op.PlanOutPath = planPath
   470  	cfg := cty.ObjectVal(map[string]cty.Value{
   471  		"path": cty.StringVal(b.StatePath),
   472  	})
   473  	cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
   474  	if err != nil {
   475  		t.Fatal(err)
   476  	}
   477  	op.PlanOutBackend = &plans.Backend{
   478  		// Just a placeholder so that we can generate a valid plan file.
   479  		Type:   "local",
   480  		Config: cfgRaw,
   481  	}
   482  	run, err := b.Operation(context.Background(), op)
   483  	if err != nil {
   484  		t.Fatalf("bad: %s", err)
   485  	}
   486  	<-run.Done()
   487  	if run.Result != backend.OperationSuccess {
   488  		t.Fatalf("plan operation failed")
   489  	}
   490  	if !p.ReadResourceCalled {
   491  		t.Fatal("ReadResource should be called")
   492  	}
   493  	if run.PlanEmpty {
   494  		t.Fatal("plan should not be empty")
   495  	}
   496  
   497  	expectedOutput := `Terraform used the selected providers to generate the following execution
   498  plan. Resource actions are indicated with the following symbols:
   499  +/- create replacement and then destroy
   500  
   501  Terraform will perform the following actions:
   502  
   503    # test_instance.foo is tainted, so must be replaced
   504  +/- resource "test_instance" "foo" {
   505          # (1 unchanged attribute hidden)
   506  
   507          # (1 unchanged block hidden)
   508      }
   509  
   510  Plan: 1 to add, 0 to change, 1 to destroy.`
   511  	if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
   512  		t.Fatalf("Unexpected output:\n%s", output)
   513  	}
   514  }
   515  
   516  func TestLocal_planRefreshFalse(t *testing.T) {
   517  	b, cleanup := TestLocal(t)
   518  	defer cleanup()
   519  
   520  	p := TestLocalProvider(t, b, "test", planFixtureSchema())
   521  	testStateFile(t, b.StatePath, testPlanState())
   522  
   523  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   524  	defer configCleanup()
   525  
   526  	run, err := b.Operation(context.Background(), op)
   527  	if err != nil {
   528  		t.Fatalf("bad: %s", err)
   529  	}
   530  	<-run.Done()
   531  	if run.Result != backend.OperationSuccess {
   532  		t.Fatalf("plan operation failed")
   533  	}
   534  
   535  	if p.ReadResourceCalled {
   536  		t.Fatal("ReadResource should not be called")
   537  	}
   538  
   539  	if !run.PlanEmpty {
   540  		t.Fatal("plan should be empty")
   541  	}
   542  
   543  	if errOutput := done(t).Stderr(); errOutput != "" {
   544  		t.Fatalf("unexpected error output:\n%s", errOutput)
   545  	}
   546  }
   547  
   548  func TestLocal_planDestroy(t *testing.T) {
   549  	b, cleanup := TestLocal(t)
   550  	defer cleanup()
   551  
   552  	TestLocalProvider(t, b, "test", planFixtureSchema())
   553  	testStateFile(t, b.StatePath, testPlanState())
   554  
   555  	outDir := t.TempDir()
   556  	planPath := filepath.Join(outDir, "plan.tfplan")
   557  
   558  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   559  	defer configCleanup()
   560  	op.PlanMode = plans.DestroyMode
   561  	op.PlanRefresh = true
   562  	op.PlanOutPath = planPath
   563  	cfg := cty.ObjectVal(map[string]cty.Value{
   564  		"path": cty.StringVal(b.StatePath),
   565  	})
   566  	cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
   567  	if err != nil {
   568  		t.Fatal(err)
   569  	}
   570  	op.PlanOutBackend = &plans.Backend{
   571  		// Just a placeholder so that we can generate a valid plan file.
   572  		Type:   "local",
   573  		Config: cfgRaw,
   574  	}
   575  
   576  	run, err := b.Operation(context.Background(), op)
   577  	if err != nil {
   578  		t.Fatalf("bad: %s", err)
   579  	}
   580  	<-run.Done()
   581  	if run.Result != backend.OperationSuccess {
   582  		t.Fatalf("plan operation failed")
   583  	}
   584  
   585  	if run.PlanEmpty {
   586  		t.Fatal("plan should not be empty")
   587  	}
   588  
   589  	plan := testReadPlan(t, planPath)
   590  	for _, r := range plan.Changes.Resources {
   591  		if r.Action.String() != "Delete" {
   592  			t.Fatalf("bad: %#v", r.Action.String())
   593  		}
   594  	}
   595  
   596  	if errOutput := done(t).Stderr(); errOutput != "" {
   597  		t.Fatalf("unexpected error output:\n%s", errOutput)
   598  	}
   599  }
   600  
   601  func TestLocal_planDestroy_withDataSources(t *testing.T) {
   602  	b, cleanup := TestLocal(t)
   603  	defer cleanup()
   604  
   605  	TestLocalProvider(t, b, "test", planFixtureSchema())
   606  	testStateFile(t, b.StatePath, testPlanState_withDataSource())
   607  
   608  	outDir := t.TempDir()
   609  	planPath := filepath.Join(outDir, "plan.tfplan")
   610  
   611  	op, configCleanup, done := testOperationPlan(t, "./testdata/destroy-with-ds")
   612  	defer configCleanup()
   613  	op.PlanMode = plans.DestroyMode
   614  	op.PlanRefresh = true
   615  	op.PlanOutPath = planPath
   616  	cfg := cty.ObjectVal(map[string]cty.Value{
   617  		"path": cty.StringVal(b.StatePath),
   618  	})
   619  	cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
   620  	if err != nil {
   621  		t.Fatal(err)
   622  	}
   623  	op.PlanOutBackend = &plans.Backend{
   624  		// Just a placeholder so that we can generate a valid plan file.
   625  		Type:   "local",
   626  		Config: cfgRaw,
   627  	}
   628  
   629  	run, err := b.Operation(context.Background(), op)
   630  	if err != nil {
   631  		t.Fatalf("bad: %s", err)
   632  	}
   633  	<-run.Done()
   634  	if run.Result != backend.OperationSuccess {
   635  		t.Fatalf("plan operation failed")
   636  	}
   637  
   638  	if run.PlanEmpty {
   639  		t.Fatal("plan should not be empty")
   640  	}
   641  
   642  	// Data source should still exist in the the plan file
   643  	plan := testReadPlan(t, planPath)
   644  	if len(plan.Changes.Resources) != 2 {
   645  		t.Fatalf("Expected exactly 1 resource for destruction, %d given: %q",
   646  			len(plan.Changes.Resources), getAddrs(plan.Changes.Resources))
   647  	}
   648  
   649  	// Data source should not be rendered in the output
   650  	expectedOutput := `Terraform will perform the following actions:
   651  
   652    # test_instance.foo[0] will be destroyed
   653    - resource "test_instance" "foo" {
   654        - ami = "bar" -> null
   655  
   656        - network_interface {
   657            - description  = "Main network interface" -> null
   658            - device_index = 0 -> null
   659          }
   660      }
   661  
   662  Plan: 0 to add, 0 to change, 1 to destroy.`
   663  
   664  	if output := done(t).Stdout(); !strings.Contains(output, expectedOutput) {
   665  		t.Fatalf("Unexpected output:\n%s", output)
   666  	}
   667  }
   668  
   669  func getAddrs(resources []*plans.ResourceInstanceChangeSrc) []string {
   670  	addrs := make([]string, len(resources))
   671  	for i, r := range resources {
   672  		addrs[i] = r.Addr.String()
   673  	}
   674  	return addrs
   675  }
   676  
   677  func TestLocal_planOutPathNoChange(t *testing.T) {
   678  	b, cleanup := TestLocal(t)
   679  	defer cleanup()
   680  	TestLocalProvider(t, b, "test", planFixtureSchema())
   681  	testStateFile(t, b.StatePath, testPlanState())
   682  
   683  	outDir := t.TempDir()
   684  	planPath := filepath.Join(outDir, "plan.tfplan")
   685  
   686  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   687  	defer configCleanup()
   688  	op.PlanOutPath = planPath
   689  	cfg := cty.ObjectVal(map[string]cty.Value{
   690  		"path": cty.StringVal(b.StatePath),
   691  	})
   692  	cfgRaw, err := plans.NewDynamicValue(cfg, cfg.Type())
   693  	if err != nil {
   694  		t.Fatal(err)
   695  	}
   696  	op.PlanOutBackend = &plans.Backend{
   697  		// Just a placeholder so that we can generate a valid plan file.
   698  		Type:   "local",
   699  		Config: cfgRaw,
   700  	}
   701  	op.PlanRefresh = true
   702  
   703  	run, err := b.Operation(context.Background(), op)
   704  	if err != nil {
   705  		t.Fatalf("bad: %s", err)
   706  	}
   707  	<-run.Done()
   708  	if run.Result != backend.OperationSuccess {
   709  		t.Fatalf("plan operation failed")
   710  	}
   711  
   712  	plan := testReadPlan(t, planPath)
   713  
   714  	if !plan.Changes.Empty() {
   715  		t.Fatalf("expected empty plan to be written")
   716  	}
   717  
   718  	if errOutput := done(t).Stderr(); errOutput != "" {
   719  		t.Fatalf("unexpected error output:\n%s", errOutput)
   720  	}
   721  }
   722  
   723  func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
   724  	t.Helper()
   725  
   726  	_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
   727  
   728  	streams, done := terminal.StreamsForTesting(t)
   729  	view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
   730  
   731  	return &backend.Operation{
   732  		Type:         backend.OperationTypePlan,
   733  		ConfigDir:    configDir,
   734  		ConfigLoader: configLoader,
   735  		StateLocker:  clistate.NewNoopLocker(),
   736  		View:         view,
   737  	}, configCleanup, done
   738  }
   739  
   740  // testPlanState is just a common state that we use for testing plan.
   741  func testPlanState() *states.State {
   742  	state := states.NewState()
   743  	rootModule := state.RootModule()
   744  	rootModule.SetResourceInstanceCurrent(
   745  		addrs.Resource{
   746  			Mode: addrs.ManagedResourceMode,
   747  			Type: "test_instance",
   748  			Name: "foo",
   749  		}.Instance(addrs.NoKey),
   750  		&states.ResourceInstanceObjectSrc{
   751  			Status: states.ObjectReady,
   752  			AttrsJSON: []byte(`{
   753  				"ami": "bar",
   754  				"network_interface": [{
   755  					"device_index": 0,
   756  					"description": "Main network interface"
   757  				}]
   758  			}`),
   759  		},
   760  		addrs.AbsProviderConfig{
   761  			Provider: addrs.NewDefaultProvider("test"),
   762  			Module:   addrs.RootModule,
   763  		},
   764  	)
   765  	return state
   766  }
   767  
   768  func testPlanState_withDataSource() *states.State {
   769  	state := states.NewState()
   770  	rootModule := state.RootModule()
   771  	rootModule.SetResourceInstanceCurrent(
   772  		addrs.Resource{
   773  			Mode: addrs.ManagedResourceMode,
   774  			Type: "test_instance",
   775  			Name: "foo",
   776  		}.Instance(addrs.IntKey(0)),
   777  		&states.ResourceInstanceObjectSrc{
   778  			Status: states.ObjectReady,
   779  			AttrsJSON: []byte(`{
   780  				"ami": "bar",
   781  				"network_interface": [{
   782  					"device_index": 0,
   783  					"description": "Main network interface"
   784  				}]
   785  			}`),
   786  		},
   787  		addrs.AbsProviderConfig{
   788  			Provider: addrs.NewDefaultProvider("test"),
   789  			Module:   addrs.RootModule,
   790  		},
   791  	)
   792  	rootModule.SetResourceInstanceCurrent(
   793  		addrs.Resource{
   794  			Mode: addrs.DataResourceMode,
   795  			Type: "test_ds",
   796  			Name: "bar",
   797  		}.Instance(addrs.IntKey(0)),
   798  		&states.ResourceInstanceObjectSrc{
   799  			Status: states.ObjectReady,
   800  			AttrsJSON: []byte(`{
   801  				"filter": "foo"
   802  			}`),
   803  		},
   804  		addrs.AbsProviderConfig{
   805  			Provider: addrs.NewDefaultProvider("test"),
   806  			Module:   addrs.RootModule,
   807  		},
   808  	)
   809  	return state
   810  }
   811  
   812  func testPlanState_tainted() *states.State {
   813  	state := states.NewState()
   814  	rootModule := state.RootModule()
   815  	rootModule.SetResourceInstanceCurrent(
   816  		addrs.Resource{
   817  			Mode: addrs.ManagedResourceMode,
   818  			Type: "test_instance",
   819  			Name: "foo",
   820  		}.Instance(addrs.NoKey),
   821  		&states.ResourceInstanceObjectSrc{
   822  			Status: states.ObjectTainted,
   823  			AttrsJSON: []byte(`{
   824  				"ami": "bar",
   825  				"network_interface": [{
   826  					"device_index": 0,
   827  					"description": "Main network interface"
   828  				}]
   829  			}`),
   830  		},
   831  		addrs.AbsProviderConfig{
   832  			Provider: addrs.NewDefaultProvider("test"),
   833  			Module:   addrs.RootModule,
   834  		},
   835  	)
   836  	return state
   837  }
   838  
   839  func testReadPlan(t *testing.T, path string) *plans.Plan {
   840  	t.Helper()
   841  
   842  	p, err := planfile.Open(path)
   843  	if err != nil {
   844  		t.Fatalf("err: %s", err)
   845  	}
   846  	defer p.Close()
   847  
   848  	plan, err := p.ReadPlan()
   849  	if err != nil {
   850  		t.Fatalf("err: %s", err)
   851  	}
   852  
   853  	return plan
   854  }
   855  
   856  // planFixtureSchema returns a schema suitable for processing the
   857  // configuration in testdata/plan . This schema should be
   858  // assigned to a mock provider named "test".
   859  func planFixtureSchema() *terraform.ProviderSchema {
   860  	return &terraform.ProviderSchema{
   861  		ResourceTypes: map[string]*configschema.Block{
   862  			"test_instance": {
   863  				Attributes: map[string]*configschema.Attribute{
   864  					"ami": {Type: cty.String, Optional: true},
   865  				},
   866  				BlockTypes: map[string]*configschema.NestedBlock{
   867  					"network_interface": {
   868  						Nesting: configschema.NestingList,
   869  						Block: configschema.Block{
   870  							Attributes: map[string]*configschema.Attribute{
   871  								"device_index": {Type: cty.Number, Optional: true},
   872  								"description":  {Type: cty.String, Optional: true},
   873  							},
   874  						},
   875  					},
   876  				},
   877  			},
   878  		},
   879  		DataSources: map[string]*configschema.Block{
   880  			"test_ds": {
   881  				Attributes: map[string]*configschema.Attribute{
   882  					"filter": {Type: cty.String, Required: true},
   883  				},
   884  			},
   885  		},
   886  	}
   887  }