github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/local/backend_plan_test.go (about)

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