github.com/opentofu/opentofu@v1.7.1/internal/backend/local/backend_plan_test.go (about)

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