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