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