github.com/opentofu/opentofu@v1.7.1/internal/command/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 command
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"fmt"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"strings"
    16  	"sync"
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/davecgh/go-spew/spew"
    21  	"github.com/zclconf/go-cty/cty"
    22  
    23  	"github.com/opentofu/opentofu/internal/addrs"
    24  	backendinit "github.com/opentofu/opentofu/internal/backend/init"
    25  	"github.com/opentofu/opentofu/internal/checks"
    26  	"github.com/opentofu/opentofu/internal/configs/configschema"
    27  	"github.com/opentofu/opentofu/internal/encryption"
    28  	"github.com/opentofu/opentofu/internal/plans"
    29  	"github.com/opentofu/opentofu/internal/providers"
    30  	"github.com/opentofu/opentofu/internal/states"
    31  	"github.com/opentofu/opentofu/internal/tfdiags"
    32  	"github.com/opentofu/opentofu/internal/tofu"
    33  )
    34  
    35  func TestPlan(t *testing.T) {
    36  	td := t.TempDir()
    37  	testCopyDir(t, testFixturePath("plan"), td)
    38  	defer testChdir(t, td)()
    39  
    40  	p := planFixtureProvider()
    41  	view, done := testView(t)
    42  	c := &PlanCommand{
    43  		Meta: Meta{
    44  			testingOverrides: metaOverridesForProvider(p),
    45  			View:             view,
    46  		},
    47  	}
    48  
    49  	args := []string{}
    50  	code := c.Run(args)
    51  	output := done(t)
    52  	if code != 0 {
    53  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
    54  	}
    55  }
    56  func TestPlan_conditionalSensitive(t *testing.T) {
    57  	td := t.TempDir()
    58  	testCopyDir(t, testFixturePath("apply-plan-conditional-sensitive"), td)
    59  	defer testChdir(t, td)()
    60  
    61  	p := planFixtureProvider()
    62  	view, done := testView(t)
    63  	c := &PlanCommand{
    64  		Meta: Meta{
    65  			testingOverrides: metaOverridesForProvider(p),
    66  			View:             view,
    67  		},
    68  	}
    69  
    70  	args := []string{}
    71  	code := c.Run(args)
    72  	output := done(t).Stderr()
    73  	if code != 1 {
    74  		t.Fatalf("bad status code: %d\n\n%s", code, output)
    75  	}
    76  
    77  	if strings.Count(output, "Output refers to sensitive values") != 9 {
    78  		t.Fatal("Not all outputs have issue with refer to sensitive value", output)
    79  	}
    80  }
    81  
    82  func TestPlan_lockedState(t *testing.T) {
    83  	td := t.TempDir()
    84  	testCopyDir(t, testFixturePath("plan"), td)
    85  	defer testChdir(t, td)()
    86  
    87  	unlock, err := testLockState(t, testDataDir, filepath.Join(td, DefaultStateFilename))
    88  	if err != nil {
    89  		t.Fatal(err)
    90  	}
    91  	defer unlock()
    92  
    93  	p := planFixtureProvider()
    94  	view, done := testView(t)
    95  	c := &PlanCommand{
    96  		Meta: Meta{
    97  			testingOverrides: metaOverridesForProvider(p),
    98  			View:             view,
    99  		},
   100  	}
   101  
   102  	args := []string{}
   103  	code := c.Run(args)
   104  	if code == 0 {
   105  		t.Fatal("expected error", done(t).Stdout())
   106  	}
   107  
   108  	output := done(t).Stderr()
   109  	if !strings.Contains(output, "lock") {
   110  		t.Fatal("command output does not look like a lock error:", output)
   111  	}
   112  }
   113  
   114  func TestPlan_plan(t *testing.T) {
   115  	testCwd(t)
   116  
   117  	planPath := testPlanFileNoop(t)
   118  
   119  	p := testProvider()
   120  	view, done := testView(t)
   121  	c := &PlanCommand{
   122  		Meta: Meta{
   123  			testingOverrides: metaOverridesForProvider(p),
   124  			View:             view,
   125  		},
   126  	}
   127  
   128  	args := []string{planPath}
   129  	code := c.Run(args)
   130  	output := done(t)
   131  	if code != 1 {
   132  		t.Fatalf("wrong exit status %d; want 1\nstderr: %s", code, output.Stderr())
   133  	}
   134  }
   135  
   136  func TestPlan_destroy(t *testing.T) {
   137  	td := t.TempDir()
   138  	testCopyDir(t, testFixturePath("plan"), td)
   139  	defer testChdir(t, td)()
   140  
   141  	originalState := states.BuildState(func(s *states.SyncState) {
   142  		s.SetResourceInstanceCurrent(
   143  			addrs.Resource{
   144  				Mode: addrs.ManagedResourceMode,
   145  				Type: "test_instance",
   146  				Name: "foo",
   147  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   148  			&states.ResourceInstanceObjectSrc{
   149  				AttrsJSON: []byte(`{"id":"bar"}`),
   150  				Status:    states.ObjectReady,
   151  			},
   152  			addrs.AbsProviderConfig{
   153  				Provider: addrs.NewDefaultProvider("test"),
   154  				Module:   addrs.RootModule,
   155  			},
   156  		)
   157  	})
   158  	outPath := testTempFile(t)
   159  	statePath := testStateFile(t, originalState)
   160  
   161  	p := planFixtureProvider()
   162  	view, done := testView(t)
   163  	c := &PlanCommand{
   164  		Meta: Meta{
   165  			testingOverrides: metaOverridesForProvider(p),
   166  			View:             view,
   167  		},
   168  	}
   169  
   170  	args := []string{
   171  		"-destroy",
   172  		"-out", outPath,
   173  		"-state", statePath,
   174  	}
   175  	code := c.Run(args)
   176  	output := done(t)
   177  	if code != 0 {
   178  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   179  	}
   180  
   181  	plan := testReadPlan(t, outPath)
   182  	for _, rc := range plan.Changes.Resources {
   183  		if got, want := rc.Action, plans.Delete; got != want {
   184  			t.Fatalf("wrong action %s for %s; want %s\nplanned change: %s", got, rc.Addr, want, spew.Sdump(rc))
   185  		}
   186  	}
   187  }
   188  
   189  func TestPlan_noState(t *testing.T) {
   190  	td := t.TempDir()
   191  	testCopyDir(t, testFixturePath("plan"), td)
   192  	defer testChdir(t, td)()
   193  
   194  	p := planFixtureProvider()
   195  	view, done := testView(t)
   196  	c := &PlanCommand{
   197  		Meta: Meta{
   198  			testingOverrides: metaOverridesForProvider(p),
   199  			View:             view,
   200  		},
   201  	}
   202  
   203  	args := []string{}
   204  	code := c.Run(args)
   205  	output := done(t)
   206  	if code != 0 {
   207  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   208  	}
   209  
   210  	// Verify that refresh was called
   211  	if p.ReadResourceCalled {
   212  		t.Fatal("ReadResource should not be called")
   213  	}
   214  
   215  	// Verify that the provider was called with the existing state
   216  	actual := p.PlanResourceChangeRequest.PriorState
   217  	expected := cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_instance"].Block.ImpliedType())
   218  	if !expected.RawEquals(actual) {
   219  		t.Fatalf("wrong prior state\ngot:  %#v\nwant: %#v", actual, expected)
   220  	}
   221  }
   222  
   223  func TestPlan_generatedConfigPath(t *testing.T) {
   224  	td := t.TempDir()
   225  	testCopyDir(t, testFixturePath("plan-import-config-gen"), td)
   226  	defer testChdir(t, td)()
   227  
   228  	genPath := filepath.Join(td, "generated.tf")
   229  
   230  	p := planFixtureProvider()
   231  	view, done := testView(t)
   232  
   233  	c := &PlanCommand{
   234  		Meta: Meta{
   235  			testingOverrides: metaOverridesForProvider(p),
   236  			View:             view,
   237  		},
   238  	}
   239  
   240  	p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
   241  		ImportedResources: []providers.ImportedResource{
   242  			{
   243  				TypeName: "test_instance",
   244  				State: cty.ObjectVal(map[string]cty.Value{
   245  					"id": cty.StringVal("bar"),
   246  				}),
   247  				Private: nil,
   248  			},
   249  		},
   250  	}
   251  
   252  	args := []string{
   253  		"-generate-config-out", genPath,
   254  	}
   255  	code := c.Run(args)
   256  	output := done(t)
   257  	if code != 0 {
   258  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   259  	}
   260  
   261  	testFileEquals(t, genPath, filepath.Join(td, "generated.tf.expected"))
   262  }
   263  
   264  func TestPlan_outPath(t *testing.T) {
   265  	td := t.TempDir()
   266  	testCopyDir(t, testFixturePath("plan"), td)
   267  	defer testChdir(t, td)()
   268  
   269  	outPath := filepath.Join(td, "test.plan")
   270  
   271  	p := planFixtureProvider()
   272  	view, done := testView(t)
   273  	c := &PlanCommand{
   274  		Meta: Meta{
   275  			testingOverrides: metaOverridesForProvider(p),
   276  			View:             view,
   277  		},
   278  	}
   279  
   280  	p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{
   281  		PlannedState: cty.NullVal(cty.EmptyObject),
   282  	}
   283  
   284  	args := []string{
   285  		"-out", outPath,
   286  	}
   287  	code := c.Run(args)
   288  	output := done(t)
   289  	if code != 0 {
   290  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   291  	}
   292  
   293  	testReadPlan(t, outPath) // will call t.Fatal itself if the file cannot be read
   294  }
   295  
   296  func TestPlan_outPathNoChange(t *testing.T) {
   297  	td := t.TempDir()
   298  	testCopyDir(t, testFixturePath("plan"), td)
   299  	defer testChdir(t, td)()
   300  
   301  	originalState := states.BuildState(func(s *states.SyncState) {
   302  		s.SetResourceInstanceCurrent(
   303  			addrs.Resource{
   304  				Mode: addrs.ManagedResourceMode,
   305  				Type: "test_instance",
   306  				Name: "foo",
   307  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   308  			&states.ResourceInstanceObjectSrc{
   309  				// Aside from "id" (which is computed) the values here must
   310  				// exactly match the values in the "plan" test fixture in order
   311  				// to produce the empty plan we need for this test.
   312  				AttrsJSON: []byte(`{"id":"bar","ami":"bar","network_interface":[{"description":"Main network interface","device_index":"0"}]}`),
   313  				Status:    states.ObjectReady,
   314  			},
   315  			addrs.AbsProviderConfig{
   316  				Provider: addrs.NewDefaultProvider("test"),
   317  				Module:   addrs.RootModule,
   318  			},
   319  		)
   320  	})
   321  	statePath := testStateFile(t, originalState)
   322  
   323  	outPath := filepath.Join(td, "test.plan")
   324  
   325  	p := planFixtureProvider()
   326  	view, done := testView(t)
   327  	c := &PlanCommand{
   328  		Meta: Meta{
   329  			testingOverrides: metaOverridesForProvider(p),
   330  			View:             view,
   331  		},
   332  	}
   333  
   334  	args := []string{
   335  		"-out", outPath,
   336  		"-state", statePath,
   337  	}
   338  	code := c.Run(args)
   339  	output := done(t)
   340  	if code != 0 {
   341  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   342  	}
   343  
   344  	plan := testReadPlan(t, outPath)
   345  	if !plan.Changes.Empty() {
   346  		t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan))
   347  	}
   348  }
   349  
   350  func TestPlan_outPathWithError(t *testing.T) {
   351  	td := t.TempDir()
   352  	testCopyDir(t, testFixturePath("plan-fail-condition"), td)
   353  	defer testChdir(t, td)()
   354  
   355  	outPath := filepath.Join(td, "test.plan")
   356  
   357  	p := planFixtureProvider()
   358  	view, done := testView(t)
   359  	c := &PlanCommand{
   360  		Meta: Meta{
   361  			testingOverrides: metaOverridesForProvider(p),
   362  			View:             view,
   363  		},
   364  	}
   365  
   366  	p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{
   367  		PlannedState: cty.NullVal(cty.EmptyObject),
   368  	}
   369  
   370  	args := []string{
   371  		"-out", outPath,
   372  	}
   373  	code := c.Run(args)
   374  	output := done(t)
   375  	if code == 0 {
   376  		t.Fatal("expected non-zero exit status", output)
   377  	}
   378  
   379  	plan := testReadPlan(t, outPath) // will call t.Fatal itself if the file cannot be read
   380  	if !plan.Errored {
   381  		t.Fatal("plan should be marked with Errored")
   382  	}
   383  
   384  	if plan.Checks == nil {
   385  		t.Fatal("plan contains no checks")
   386  	}
   387  
   388  	// the checks should only contain one failure
   389  	results := plan.Checks.ConfigResults.Elements()
   390  	if len(results) != 1 {
   391  		t.Fatal("incorrect number of check results", len(results))
   392  	}
   393  	if results[0].Value.Status != checks.StatusFail {
   394  		t.Errorf("incorrect status, got %s", results[0].Value.Status)
   395  	}
   396  }
   397  
   398  // When using "-out" with a backend, the plan should encode the backend config
   399  func TestPlan_outBackend(t *testing.T) {
   400  	// Create a temporary working directory that is empty
   401  	td := t.TempDir()
   402  	testCopyDir(t, testFixturePath("plan-out-backend"), td)
   403  	defer testChdir(t, td)()
   404  
   405  	originalState := states.BuildState(func(s *states.SyncState) {
   406  		s.SetResourceInstanceCurrent(
   407  			addrs.Resource{
   408  				Mode: addrs.ManagedResourceMode,
   409  				Type: "test_instance",
   410  				Name: "foo",
   411  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   412  			&states.ResourceInstanceObjectSrc{
   413  				AttrsJSON: []byte(`{"id":"bar","ami":"bar"}`),
   414  				Status:    states.ObjectReady,
   415  			},
   416  			addrs.AbsProviderConfig{
   417  				Provider: addrs.NewDefaultProvider("test"),
   418  				Module:   addrs.RootModule,
   419  			},
   420  		)
   421  	})
   422  
   423  	// Set up our backend state
   424  	dataState, srv := testBackendState(t, originalState, 200)
   425  	defer srv.Close()
   426  	testStateFileRemote(t, dataState)
   427  
   428  	outPath := "foo"
   429  	p := testProvider()
   430  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   431  		ResourceTypes: map[string]providers.Schema{
   432  			"test_instance": {
   433  				Block: &configschema.Block{
   434  					Attributes: map[string]*configschema.Attribute{
   435  						"id": {
   436  							Type:     cty.String,
   437  							Computed: true,
   438  						},
   439  						"ami": {
   440  							Type:     cty.String,
   441  							Optional: true,
   442  						},
   443  					},
   444  				},
   445  			},
   446  		},
   447  	}
   448  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
   449  		return providers.PlanResourceChangeResponse{
   450  			PlannedState: req.ProposedNewState,
   451  		}
   452  	}
   453  	view, done := testView(t)
   454  	c := &PlanCommand{
   455  		Meta: Meta{
   456  			testingOverrides: metaOverridesForProvider(p),
   457  			View:             view,
   458  		},
   459  	}
   460  
   461  	args := []string{
   462  		"-out", outPath,
   463  	}
   464  	code := c.Run(args)
   465  	output := done(t)
   466  	if code != 0 {
   467  		t.Logf("stdout: %s", output.Stdout())
   468  		t.Fatalf("plan command failed with exit code %d\n\n%s", code, output.Stderr())
   469  	}
   470  
   471  	plan := testReadPlan(t, outPath)
   472  	if !plan.Changes.Empty() {
   473  		t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan))
   474  	}
   475  
   476  	if got, want := plan.Backend.Type, "http"; got != want {
   477  		t.Errorf("wrong backend type %q; want %q", got, want)
   478  	}
   479  	if got, want := plan.Backend.Workspace, "default"; got != want {
   480  		t.Errorf("wrong backend workspace %q; want %q", got, want)
   481  	}
   482  	{
   483  		httpBackend := backendinit.Backend("http")(encryption.StateEncryptionDisabled())
   484  		schema := httpBackend.ConfigSchema()
   485  		got, err := plan.Backend.Config.Decode(schema.ImpliedType())
   486  		if err != nil {
   487  			t.Fatalf("failed to decode backend config in plan: %s", err)
   488  		}
   489  		want, err := dataState.Backend.Config(schema)
   490  		if err != nil {
   491  			t.Fatalf("failed to decode cached config: %s", err)
   492  		}
   493  		if !want.RawEquals(got) {
   494  			t.Errorf("wrong backend config\ngot:  %#v\nwant: %#v", got, want)
   495  		}
   496  	}
   497  }
   498  
   499  func TestPlan_refreshFalse(t *testing.T) {
   500  	// Create a temporary working directory that is empty
   501  	td := t.TempDir()
   502  	testCopyDir(t, testFixturePath("plan-existing-state"), td)
   503  	defer testChdir(t, td)()
   504  
   505  	p := planFixtureProvider()
   506  	view, done := testView(t)
   507  	c := &PlanCommand{
   508  		Meta: Meta{
   509  			testingOverrides: metaOverridesForProvider(p),
   510  			View:             view,
   511  		},
   512  	}
   513  
   514  	args := []string{
   515  		"-refresh=false",
   516  	}
   517  	code := c.Run(args)
   518  	output := done(t)
   519  	if code != 0 {
   520  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   521  	}
   522  
   523  	if p.ReadResourceCalled {
   524  		t.Fatal("ReadResource should not have been called")
   525  	}
   526  }
   527  
   528  func TestPlan_refreshTrue(t *testing.T) {
   529  	// Create a temporary working directory that is empty
   530  	td := t.TempDir()
   531  	testCopyDir(t, testFixturePath("plan-existing-state"), td)
   532  	defer testChdir(t, td)()
   533  
   534  	p := planFixtureProvider()
   535  	view, done := testView(t)
   536  	c := &PlanCommand{
   537  		Meta: Meta{
   538  			testingOverrides: metaOverridesForProvider(p),
   539  			View:             view,
   540  		},
   541  	}
   542  
   543  	args := []string{
   544  		"-refresh=true",
   545  	}
   546  	code := c.Run(args)
   547  	output := done(t)
   548  	if code != 0 {
   549  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   550  	}
   551  
   552  	if !p.ReadResourceCalled {
   553  		t.Fatalf("ReadResource should have been called")
   554  	}
   555  }
   556  
   557  // A consumer relies on the fact that running
   558  // tofu plan -refresh=false -refresh=true gives the same result as
   559  // tofu plan -refresh=true.
   560  // While the flag logic itself is handled by the stdlib flags package (and code
   561  // in main() that is tested elsewhere), we verify the overall plan command
   562  // behaviour here in case we accidentally break this with additional logic.
   563  func TestPlan_refreshFalseRefreshTrue(t *testing.T) {
   564  	// Create a temporary working directory that is empty
   565  	td := t.TempDir()
   566  	testCopyDir(t, testFixturePath("plan-existing-state"), td)
   567  	defer testChdir(t, td)()
   568  
   569  	p := planFixtureProvider()
   570  	view, done := testView(t)
   571  	c := &PlanCommand{
   572  		Meta: Meta{
   573  			testingOverrides: metaOverridesForProvider(p),
   574  			View:             view,
   575  		},
   576  	}
   577  
   578  	args := []string{
   579  		"-refresh=false",
   580  		"-refresh=true",
   581  	}
   582  	code := c.Run(args)
   583  	output := done(t)
   584  	if code != 0 {
   585  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   586  	}
   587  
   588  	if !p.ReadResourceCalled {
   589  		t.Fatal("ReadResource should have been called")
   590  	}
   591  }
   592  
   593  func TestPlan_state(t *testing.T) {
   594  	// Create a temporary working directory that is empty
   595  	td := t.TempDir()
   596  	testCopyDir(t, testFixturePath("plan"), td)
   597  	defer testChdir(t, td)()
   598  
   599  	originalState := testState()
   600  	statePath := testStateFile(t, originalState)
   601  
   602  	p := planFixtureProvider()
   603  	view, done := testView(t)
   604  	c := &PlanCommand{
   605  		Meta: Meta{
   606  			testingOverrides: metaOverridesForProvider(p),
   607  			View:             view,
   608  		},
   609  	}
   610  
   611  	args := []string{
   612  		"-state", statePath,
   613  	}
   614  	code := c.Run(args)
   615  	output := done(t)
   616  	if code != 0 {
   617  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   618  	}
   619  
   620  	// Verify that the provider was called with the existing state
   621  	actual := p.PlanResourceChangeRequest.PriorState
   622  	expected := cty.ObjectVal(map[string]cty.Value{
   623  		"id":  cty.StringVal("bar"),
   624  		"ami": cty.NullVal(cty.String),
   625  		"network_interface": cty.ListValEmpty(cty.Object(map[string]cty.Type{
   626  			"device_index": cty.String,
   627  			"description":  cty.String,
   628  		})),
   629  	})
   630  	if !expected.RawEquals(actual) {
   631  		t.Fatalf("wrong prior state\ngot:  %#v\nwant: %#v", actual, expected)
   632  	}
   633  }
   634  
   635  func TestPlan_stateDefault(t *testing.T) {
   636  	// Create a temporary working directory that is empty
   637  	td := t.TempDir()
   638  	testCopyDir(t, testFixturePath("plan"), td)
   639  	defer testChdir(t, td)()
   640  
   641  	// Generate state and move it to the default path
   642  	originalState := testState()
   643  	statePath := testStateFile(t, originalState)
   644  	os.Rename(statePath, path.Join(td, "terraform.tfstate"))
   645  
   646  	p := planFixtureProvider()
   647  	view, done := testView(t)
   648  	c := &PlanCommand{
   649  		Meta: Meta{
   650  			testingOverrides: metaOverridesForProvider(p),
   651  			View:             view,
   652  		},
   653  	}
   654  
   655  	args := []string{}
   656  	code := c.Run(args)
   657  	output := done(t)
   658  	if code != 0 {
   659  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   660  	}
   661  
   662  	// Verify that the provider was called with the existing state
   663  	actual := p.PlanResourceChangeRequest.PriorState
   664  	expected := cty.ObjectVal(map[string]cty.Value{
   665  		"id":  cty.StringVal("bar"),
   666  		"ami": cty.NullVal(cty.String),
   667  		"network_interface": cty.ListValEmpty(cty.Object(map[string]cty.Type{
   668  			"device_index": cty.String,
   669  			"description":  cty.String,
   670  		})),
   671  	})
   672  	if !expected.RawEquals(actual) {
   673  		t.Fatalf("wrong prior state\ngot:  %#v\nwant: %#v", actual, expected)
   674  	}
   675  }
   676  
   677  func TestPlan_validate(t *testing.T) {
   678  	// This is triggered by not asking for input so we have to set this to false
   679  	test = false
   680  	defer func() { test = true }()
   681  
   682  	td := t.TempDir()
   683  	testCopyDir(t, testFixturePath("plan-invalid"), td)
   684  	defer testChdir(t, td)()
   685  
   686  	p := testProvider()
   687  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   688  		ResourceTypes: map[string]providers.Schema{
   689  			"test_instance": {
   690  				Block: &configschema.Block{
   691  					Attributes: map[string]*configschema.Attribute{
   692  						"id": {Type: cty.String, Optional: true, Computed: true},
   693  					},
   694  				},
   695  			},
   696  		},
   697  	}
   698  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
   699  		return providers.PlanResourceChangeResponse{
   700  			PlannedState: req.ProposedNewState,
   701  		}
   702  	}
   703  	view, done := testView(t)
   704  	c := &PlanCommand{
   705  		Meta: Meta{
   706  			testingOverrides: metaOverridesForProvider(p),
   707  			View:             view,
   708  		},
   709  	}
   710  
   711  	args := []string{"-no-color"}
   712  	code := c.Run(args)
   713  	output := done(t)
   714  	if code != 1 {
   715  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   716  	}
   717  
   718  	actual := output.Stderr()
   719  	if want := "Error: Invalid count argument"; !strings.Contains(actual, want) {
   720  		t.Fatalf("unexpected error output\ngot:\n%s\n\nshould contain: %s", actual, want)
   721  	}
   722  	if want := "9:   count = timestamp()"; !strings.Contains(actual, want) {
   723  		t.Fatalf("unexpected error output\ngot:\n%s\n\nshould contain: %s", actual, want)
   724  	}
   725  }
   726  
   727  func TestPlan_vars(t *testing.T) {
   728  	// Create a temporary working directory that is empty
   729  	td := t.TempDir()
   730  	testCopyDir(t, testFixturePath("plan-vars"), td)
   731  	defer testChdir(t, td)()
   732  
   733  	p := planVarsFixtureProvider()
   734  	view, done := testView(t)
   735  	c := &PlanCommand{
   736  		Meta: Meta{
   737  			testingOverrides: metaOverridesForProvider(p),
   738  			View:             view,
   739  		},
   740  	}
   741  
   742  	actual := ""
   743  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
   744  		actual = req.ProposedNewState.GetAttr("value").AsString()
   745  		resp.PlannedState = req.ProposedNewState
   746  		return
   747  	}
   748  
   749  	args := []string{
   750  		"-var", "foo=bar",
   751  	}
   752  	code := c.Run(args)
   753  	output := done(t)
   754  	if code != 0 {
   755  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   756  	}
   757  
   758  	if actual != "bar" {
   759  		t.Fatal("didn't work")
   760  	}
   761  }
   762  
   763  func TestPlan_varsInvalid(t *testing.T) {
   764  	testCases := []struct {
   765  		args    []string
   766  		wantErr string
   767  	}{
   768  		{
   769  			[]string{"-var", "foo"},
   770  			`The given -var option "foo" is not correctly specified.`,
   771  		},
   772  		{
   773  			[]string{"-var", "foo = bar"},
   774  			`Variable name "foo " is invalid due to trailing space.`,
   775  		},
   776  	}
   777  
   778  	// Create a temporary working directory that is empty
   779  	td := t.TempDir()
   780  	testCopyDir(t, testFixturePath("plan-vars"), td)
   781  	defer testChdir(t, td)()
   782  
   783  	for _, tc := range testCases {
   784  		t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
   785  			p := planVarsFixtureProvider()
   786  			view, done := testView(t)
   787  			c := &PlanCommand{
   788  				Meta: Meta{
   789  					testingOverrides: metaOverridesForProvider(p),
   790  					View:             view,
   791  				},
   792  			}
   793  
   794  			code := c.Run(tc.args)
   795  			output := done(t)
   796  			if code != 1 {
   797  				t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
   798  			}
   799  
   800  			got := output.Stderr()
   801  			if !strings.Contains(got, tc.wantErr) {
   802  				t.Fatalf("bad error output, want %q, got:\n%s", tc.wantErr, got)
   803  			}
   804  		})
   805  	}
   806  }
   807  
   808  func TestPlan_varsUnset(t *testing.T) {
   809  	// Create a temporary working directory that is empty
   810  	td := t.TempDir()
   811  	testCopyDir(t, testFixturePath("plan-vars"), td)
   812  	defer testChdir(t, td)()
   813  
   814  	// The plan command will prompt for interactive input of var.foo.
   815  	// We'll answer "bar" to that prompt, which should then allow this
   816  	// configuration to apply even though var.foo doesn't have a
   817  	// default value and there are no -var arguments on our command line.
   818  
   819  	// This will (helpfully) panic if more than one variable is requested during plan:
   820  	// https://github.com/hashicorp/terraform/issues/26027
   821  	close := testInteractiveInput(t, []string{"bar"})
   822  	defer close()
   823  
   824  	p := planVarsFixtureProvider()
   825  	view, done := testView(t)
   826  	c := &PlanCommand{
   827  		Meta: Meta{
   828  			testingOverrides: metaOverridesForProvider(p),
   829  			View:             view,
   830  		},
   831  	}
   832  
   833  	args := []string{}
   834  	code := c.Run(args)
   835  	output := done(t)
   836  	if code != 0 {
   837  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   838  	}
   839  }
   840  
   841  // This test adds a required argument to the test provider to validate
   842  // processing of user input:
   843  // https://github.com/hashicorp/terraform/issues/26035
   844  func TestPlan_providerArgumentUnset(t *testing.T) {
   845  	// Create a temporary working directory that is empty
   846  	td := t.TempDir()
   847  	testCopyDir(t, testFixturePath("plan"), td)
   848  	defer testChdir(t, td)()
   849  
   850  	// Disable test mode so input would be asked
   851  	test = false
   852  	defer func() { test = true }()
   853  
   854  	// The plan command will prompt for interactive input of provider.test.region
   855  	defaultInputReader = bytes.NewBufferString("us-east-1\n")
   856  
   857  	p := planFixtureProvider()
   858  	// override the planFixtureProvider schema to include a required provider argument
   859  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   860  		Provider: providers.Schema{
   861  			Block: &configschema.Block{
   862  				Attributes: map[string]*configschema.Attribute{
   863  					"region": {Type: cty.String, Required: true},
   864  				},
   865  			},
   866  		},
   867  		ResourceTypes: map[string]providers.Schema{
   868  			"test_instance": {
   869  				Block: &configschema.Block{
   870  					Attributes: map[string]*configschema.Attribute{
   871  						"id":  {Type: cty.String, Optional: true, Computed: true},
   872  						"ami": {Type: cty.String, Optional: true, Computed: true},
   873  					},
   874  					BlockTypes: map[string]*configschema.NestedBlock{
   875  						"network_interface": {
   876  							Nesting: configschema.NestingList,
   877  							Block: configschema.Block{
   878  								Attributes: map[string]*configschema.Attribute{
   879  									"device_index": {Type: cty.String, Optional: true},
   880  									"description":  {Type: cty.String, Optional: true},
   881  								},
   882  							},
   883  						},
   884  					},
   885  				},
   886  			},
   887  		},
   888  		DataSources: map[string]providers.Schema{
   889  			"test_data_source": {
   890  				Block: &configschema.Block{
   891  					Attributes: map[string]*configschema.Attribute{
   892  						"id": {
   893  							Type:     cty.String,
   894  							Required: true,
   895  						},
   896  						"valid": {
   897  							Type:     cty.Bool,
   898  							Computed: true,
   899  						},
   900  					},
   901  				},
   902  			},
   903  		},
   904  	}
   905  	view, done := testView(t)
   906  	c := &PlanCommand{
   907  		Meta: Meta{
   908  			testingOverrides: metaOverridesForProvider(p),
   909  			View:             view,
   910  		},
   911  	}
   912  
   913  	args := []string{}
   914  	code := c.Run(args)
   915  	output := done(t)
   916  	if code != 0 {
   917  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   918  	}
   919  }
   920  
   921  // Test that tofu properly merges provider configuration that's split
   922  // between config files and interactive input variables.
   923  // https://github.com/hashicorp/terraform/issues/28956
   924  func TestPlan_providerConfigMerge(t *testing.T) {
   925  	td := t.TempDir()
   926  	testCopyDir(t, testFixturePath("plan-provider-input"), td)
   927  	defer testChdir(t, td)()
   928  
   929  	// Disable test mode so input would be asked
   930  	test = false
   931  	defer func() { test = true }()
   932  
   933  	// The plan command will prompt for interactive input of provider.test.region
   934  	defaultInputReader = bytes.NewBufferString("us-east-1\n")
   935  
   936  	p := planFixtureProvider()
   937  	// override the planFixtureProvider schema to include a required provider argument and a nested block
   938  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   939  		Provider: providers.Schema{
   940  			Block: &configschema.Block{
   941  				Attributes: map[string]*configschema.Attribute{
   942  					"region": {Type: cty.String, Required: true},
   943  					"url":    {Type: cty.String, Required: true},
   944  				},
   945  				BlockTypes: map[string]*configschema.NestedBlock{
   946  					"auth": {
   947  						Nesting: configschema.NestingList,
   948  						Block: configschema.Block{
   949  							Attributes: map[string]*configschema.Attribute{
   950  								"user":     {Type: cty.String, Required: true},
   951  								"password": {Type: cty.String, Required: true},
   952  							},
   953  						},
   954  					},
   955  				},
   956  			},
   957  		},
   958  		ResourceTypes: map[string]providers.Schema{
   959  			"test_instance": {
   960  				Block: &configschema.Block{
   961  					Attributes: map[string]*configschema.Attribute{
   962  						"id": {Type: cty.String, Optional: true, Computed: true},
   963  					},
   964  				},
   965  			},
   966  		},
   967  	}
   968  
   969  	view, done := testView(t)
   970  	c := &PlanCommand{
   971  		Meta: Meta{
   972  			testingOverrides: metaOverridesForProvider(p),
   973  			View:             view,
   974  		},
   975  	}
   976  
   977  	args := []string{}
   978  	code := c.Run(args)
   979  	output := done(t)
   980  	if code != 0 {
   981  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   982  	}
   983  
   984  	if !p.ConfigureProviderCalled {
   985  		t.Fatal("configure provider not called")
   986  	}
   987  
   988  	// For this test, we want to confirm that we've sent the expected config
   989  	// value *to* the provider.
   990  	got := p.ConfigureProviderRequest.Config
   991  	want := cty.ObjectVal(map[string]cty.Value{
   992  		"auth": cty.ListVal([]cty.Value{
   993  			cty.ObjectVal(map[string]cty.Value{
   994  				"user":     cty.StringVal("one"),
   995  				"password": cty.StringVal("onepw"),
   996  			}),
   997  			cty.ObjectVal(map[string]cty.Value{
   998  				"user":     cty.StringVal("two"),
   999  				"password": cty.StringVal("twopw"),
  1000  			}),
  1001  		}),
  1002  		"region": cty.StringVal("us-east-1"),
  1003  		"url":    cty.StringVal("example.com"),
  1004  	})
  1005  
  1006  	if !got.RawEquals(want) {
  1007  		t.Fatal("wrong provider config")
  1008  	}
  1009  
  1010  }
  1011  
  1012  func TestPlan_varFile(t *testing.T) {
  1013  	// Create a temporary working directory that is empty
  1014  	td := t.TempDir()
  1015  	testCopyDir(t, testFixturePath("plan-vars"), td)
  1016  	defer testChdir(t, td)()
  1017  
  1018  	varFilePath := testTempFile(t)
  1019  	if err := os.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil {
  1020  		t.Fatalf("err: %s", err)
  1021  	}
  1022  
  1023  	p := planVarsFixtureProvider()
  1024  	view, done := testView(t)
  1025  	c := &PlanCommand{
  1026  		Meta: Meta{
  1027  			testingOverrides: metaOverridesForProvider(p),
  1028  			View:             view,
  1029  		},
  1030  	}
  1031  
  1032  	actual := ""
  1033  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
  1034  		actual = req.ProposedNewState.GetAttr("value").AsString()
  1035  		resp.PlannedState = req.ProposedNewState
  1036  		return
  1037  	}
  1038  
  1039  	args := []string{
  1040  		"-var-file", varFilePath,
  1041  	}
  1042  	code := c.Run(args)
  1043  	output := done(t)
  1044  	if code != 0 {
  1045  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
  1046  	}
  1047  
  1048  	if actual != "bar" {
  1049  		t.Fatal("didn't work")
  1050  	}
  1051  }
  1052  
  1053  func TestPlan_varFileDefault(t *testing.T) {
  1054  	// Create a temporary working directory that is empty
  1055  	td := t.TempDir()
  1056  	testCopyDir(t, testFixturePath("plan-vars"), td)
  1057  	defer testChdir(t, td)()
  1058  
  1059  	varFilePath := filepath.Join(td, "terraform.tfvars")
  1060  	if err := os.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil {
  1061  		t.Fatalf("err: %s", err)
  1062  	}
  1063  
  1064  	p := planVarsFixtureProvider()
  1065  	view, done := testView(t)
  1066  	c := &PlanCommand{
  1067  		Meta: Meta{
  1068  			testingOverrides: metaOverridesForProvider(p),
  1069  			View:             view,
  1070  		},
  1071  	}
  1072  
  1073  	actual := ""
  1074  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
  1075  		actual = req.ProposedNewState.GetAttr("value").AsString()
  1076  		resp.PlannedState = req.ProposedNewState
  1077  		return
  1078  	}
  1079  
  1080  	args := []string{}
  1081  	code := c.Run(args)
  1082  	output := done(t)
  1083  	if code != 0 {
  1084  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
  1085  	}
  1086  
  1087  	if actual != "bar" {
  1088  		t.Fatal("didn't work")
  1089  	}
  1090  }
  1091  
  1092  func TestPlan_varFileWithDecls(t *testing.T) {
  1093  	// Create a temporary working directory that is empty
  1094  	td := t.TempDir()
  1095  	testCopyDir(t, testFixturePath("plan-vars"), td)
  1096  	defer testChdir(t, td)()
  1097  
  1098  	varFilePath := testTempFile(t)
  1099  	if err := os.WriteFile(varFilePath, []byte(planVarFileWithDecl), 0644); err != nil {
  1100  		t.Fatalf("err: %s", err)
  1101  	}
  1102  
  1103  	p := planVarsFixtureProvider()
  1104  	view, done := testView(t)
  1105  	c := &PlanCommand{
  1106  		Meta: Meta{
  1107  			testingOverrides: metaOverridesForProvider(p),
  1108  			View:             view,
  1109  		},
  1110  	}
  1111  
  1112  	args := []string{
  1113  		"-var-file", varFilePath,
  1114  	}
  1115  	code := c.Run(args)
  1116  	output := done(t)
  1117  	if code == 0 {
  1118  		t.Fatalf("succeeded; want failure\n\n%s", output.Stdout())
  1119  	}
  1120  
  1121  	msg := output.Stderr()
  1122  	if got, want := msg, "Variable declaration in .tfvars file"; !strings.Contains(got, want) {
  1123  		t.Fatalf("missing expected error message\nwant message containing %q\ngot:\n%s", want, got)
  1124  	}
  1125  }
  1126  
  1127  func TestPlan_detailedExitcode(t *testing.T) {
  1128  	td := t.TempDir()
  1129  	testCopyDir(t, testFixturePath("plan"), td)
  1130  	defer testChdir(t, td)()
  1131  
  1132  	t.Run("return 1", func(t *testing.T) {
  1133  		view, done := testView(t)
  1134  		c := &PlanCommand{
  1135  			Meta: Meta{
  1136  				// Running plan without setting testingOverrides is similar to plan without init
  1137  				View: view,
  1138  			},
  1139  		}
  1140  		code := c.Run([]string{"-detailed-exitcode"})
  1141  		output := done(t)
  1142  		if code != 1 {
  1143  			t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
  1144  		}
  1145  	})
  1146  
  1147  	t.Run("return 2", func(t *testing.T) {
  1148  		p := planFixtureProvider()
  1149  		view, done := testView(t)
  1150  		c := &PlanCommand{
  1151  			Meta: Meta{
  1152  				testingOverrides: metaOverridesForProvider(p),
  1153  				View:             view,
  1154  			},
  1155  		}
  1156  
  1157  		code := c.Run([]string{"-detailed-exitcode"})
  1158  		output := done(t)
  1159  		if code != 2 {
  1160  			t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
  1161  		}
  1162  	})
  1163  }
  1164  
  1165  func TestPlan_detailedExitcode_emptyDiff(t *testing.T) {
  1166  	td := t.TempDir()
  1167  	testCopyDir(t, testFixturePath("plan-emptydiff"), td)
  1168  	defer testChdir(t, td)()
  1169  
  1170  	p := testProvider()
  1171  	view, done := testView(t)
  1172  	c := &PlanCommand{
  1173  		Meta: Meta{
  1174  			testingOverrides: metaOverridesForProvider(p),
  1175  			View:             view,
  1176  		},
  1177  	}
  1178  
  1179  	args := []string{"-detailed-exitcode"}
  1180  	code := c.Run(args)
  1181  	output := done(t)
  1182  	if code != 0 {
  1183  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
  1184  	}
  1185  }
  1186  
  1187  func TestPlan_shutdown(t *testing.T) {
  1188  	// Create a temporary working directory that is empty
  1189  	td := t.TempDir()
  1190  	testCopyDir(t, testFixturePath("apply-shutdown"), td)
  1191  	defer testChdir(t, td)()
  1192  
  1193  	cancelled := make(chan struct{})
  1194  	shutdownCh := make(chan struct{})
  1195  
  1196  	p := testProvider()
  1197  	view, done := testView(t)
  1198  	c := &PlanCommand{
  1199  		Meta: Meta{
  1200  			testingOverrides: metaOverridesForProvider(p),
  1201  			View:             view,
  1202  			ShutdownCh:       shutdownCh,
  1203  		},
  1204  	}
  1205  
  1206  	p.StopFn = func() error {
  1207  		close(cancelled)
  1208  		return nil
  1209  	}
  1210  
  1211  	var once sync.Once
  1212  
  1213  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
  1214  		once.Do(func() {
  1215  			shutdownCh <- struct{}{}
  1216  		})
  1217  
  1218  		// Because of the internal lock in the MockProvider, we can't
  1219  		// coordinate directly with the calling of Stop, and making the
  1220  		// MockProvider concurrent is disruptive to a lot of existing tests.
  1221  		// Wait here a moment to help make sure the main goroutine gets to the
  1222  		// Stop call before we exit, or the plan may finish before it can be
  1223  		// canceled.
  1224  		time.Sleep(200 * time.Millisecond)
  1225  
  1226  		s := req.ProposedNewState.AsValueMap()
  1227  		s["ami"] = cty.StringVal("bar")
  1228  		resp.PlannedState = cty.ObjectVal(s)
  1229  		return
  1230  	}
  1231  
  1232  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
  1233  		ResourceTypes: map[string]providers.Schema{
  1234  			"test_instance": {
  1235  				Block: &configschema.Block{
  1236  					Attributes: map[string]*configschema.Attribute{
  1237  						"ami": {Type: cty.String, Optional: true},
  1238  					},
  1239  				},
  1240  			},
  1241  		},
  1242  	}
  1243  
  1244  	code := c.Run([]string{})
  1245  	output := done(t)
  1246  	if code != 1 {
  1247  		t.Errorf("wrong exit code %d; want 1\noutput:\n%s", code, output.Stdout())
  1248  	}
  1249  
  1250  	select {
  1251  	case <-cancelled:
  1252  	default:
  1253  		t.Error("command not cancelled")
  1254  	}
  1255  }
  1256  
  1257  func TestPlan_init_required(t *testing.T) {
  1258  	td := t.TempDir()
  1259  	testCopyDir(t, testFixturePath("plan"), td)
  1260  	defer testChdir(t, td)()
  1261  
  1262  	view, done := testView(t)
  1263  	c := &PlanCommand{
  1264  		Meta: Meta{
  1265  			// Running plan without setting testingOverrides is similar to plan without init
  1266  			View: view,
  1267  		},
  1268  	}
  1269  
  1270  	args := []string{"-no-color"}
  1271  	code := c.Run(args)
  1272  	output := done(t)
  1273  	if code != 1 {
  1274  		t.Fatalf("expected error, got success")
  1275  	}
  1276  	got := output.Stderr()
  1277  	if !(strings.Contains(got, "tofu init") && strings.Contains(got, "provider registry.opentofu.org/hashicorp/test: required by this configuration but no version is selected")) {
  1278  		t.Fatal("wrong error message in output:", got)
  1279  	}
  1280  }
  1281  
  1282  // Config with multiple resources, targeting plan of a subset
  1283  func TestPlan_targeted(t *testing.T) {
  1284  	td := t.TempDir()
  1285  	testCopyDir(t, testFixturePath("apply-targeted"), td)
  1286  	defer testChdir(t, td)()
  1287  
  1288  	p := testProvider()
  1289  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
  1290  		ResourceTypes: map[string]providers.Schema{
  1291  			"test_instance": {
  1292  				Block: &configschema.Block{
  1293  					Attributes: map[string]*configschema.Attribute{
  1294  						"id": {Type: cty.String, Computed: true},
  1295  					},
  1296  				},
  1297  			},
  1298  		},
  1299  	}
  1300  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
  1301  		return providers.PlanResourceChangeResponse{
  1302  			PlannedState: req.ProposedNewState,
  1303  		}
  1304  	}
  1305  
  1306  	view, done := testView(t)
  1307  	c := &PlanCommand{
  1308  		Meta: Meta{
  1309  			testingOverrides: metaOverridesForProvider(p),
  1310  			View:             view,
  1311  		},
  1312  	}
  1313  
  1314  	args := []string{
  1315  		"-target", "test_instance.foo",
  1316  		"-target", "test_instance.baz",
  1317  	}
  1318  	code := c.Run(args)
  1319  	output := done(t)
  1320  	if code != 0 {
  1321  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
  1322  	}
  1323  
  1324  	if got, want := output.Stdout(), "3 to add, 0 to change, 0 to destroy"; !strings.Contains(got, want) {
  1325  		t.Fatalf("bad change summary, want %q, got:\n%s", want, got)
  1326  	}
  1327  }
  1328  
  1329  // Diagnostics for invalid -target flags
  1330  func TestPlan_targetFlagsDiags(t *testing.T) {
  1331  	testCases := map[string]string{
  1332  		"test_instance.": "Dot must be followed by attribute name.",
  1333  		"test_instance":  "Resource specification must include a resource type and name.",
  1334  	}
  1335  
  1336  	for target, wantDiag := range testCases {
  1337  		t.Run(target, func(t *testing.T) {
  1338  			td := testTempDir(t)
  1339  			defer os.RemoveAll(td)
  1340  			defer testChdir(t, td)()
  1341  
  1342  			view, done := testView(t)
  1343  			c := &PlanCommand{
  1344  				Meta: Meta{
  1345  					View: view,
  1346  				},
  1347  			}
  1348  
  1349  			args := []string{
  1350  				"-target", target,
  1351  			}
  1352  			code := c.Run(args)
  1353  			output := done(t)
  1354  			if code != 1 {
  1355  				t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
  1356  			}
  1357  
  1358  			got := output.Stderr()
  1359  			if !strings.Contains(got, target) {
  1360  				t.Fatalf("bad error output, want %q, got:\n%s", target, got)
  1361  			}
  1362  			if !strings.Contains(got, wantDiag) {
  1363  				t.Fatalf("bad error output, want %q, got:\n%s", wantDiag, got)
  1364  			}
  1365  		})
  1366  	}
  1367  }
  1368  
  1369  func TestPlan_replace(t *testing.T) {
  1370  	td := t.TempDir()
  1371  	testCopyDir(t, testFixturePath("plan-replace"), td)
  1372  	defer testChdir(t, td)()
  1373  
  1374  	originalState := states.BuildState(func(s *states.SyncState) {
  1375  		s.SetResourceInstanceCurrent(
  1376  			addrs.Resource{
  1377  				Mode: addrs.ManagedResourceMode,
  1378  				Type: "test_instance",
  1379  				Name: "a",
  1380  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
  1381  			&states.ResourceInstanceObjectSrc{
  1382  				AttrsJSON: []byte(`{"id":"hello"}`),
  1383  				Status:    states.ObjectReady,
  1384  			},
  1385  			addrs.AbsProviderConfig{
  1386  				Provider: addrs.NewDefaultProvider("test"),
  1387  				Module:   addrs.RootModule,
  1388  			},
  1389  		)
  1390  	})
  1391  	statePath := testStateFile(t, originalState)
  1392  
  1393  	p := testProvider()
  1394  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
  1395  		ResourceTypes: map[string]providers.Schema{
  1396  			"test_instance": {
  1397  				Block: &configschema.Block{
  1398  					Attributes: map[string]*configschema.Attribute{
  1399  						"id": {Type: cty.String, Computed: true},
  1400  					},
  1401  				},
  1402  			},
  1403  		},
  1404  	}
  1405  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
  1406  		return providers.PlanResourceChangeResponse{
  1407  			PlannedState: req.ProposedNewState,
  1408  		}
  1409  	}
  1410  
  1411  	view, done := testView(t)
  1412  	c := &PlanCommand{
  1413  		Meta: Meta{
  1414  			testingOverrides: metaOverridesForProvider(p),
  1415  			View:             view,
  1416  		},
  1417  	}
  1418  
  1419  	args := []string{
  1420  		"-state", statePath,
  1421  		"-no-color",
  1422  		"-replace", "test_instance.a",
  1423  	}
  1424  	code := c.Run(args)
  1425  	output := done(t)
  1426  	if code != 0 {
  1427  		t.Fatalf("wrong exit code %d\n\n%s", code, output.Stderr())
  1428  	}
  1429  
  1430  	stdout := output.Stdout()
  1431  	if got, want := stdout, "1 to add, 0 to change, 1 to destroy"; !strings.Contains(got, want) {
  1432  		t.Errorf("wrong plan summary\ngot output:\n%s\n\nwant substring: %s", got, want)
  1433  	}
  1434  	if got, want := stdout, "test_instance.a will be replaced, as requested"; !strings.Contains(got, want) {
  1435  		t.Errorf("missing replace explanation\ngot output:\n%s\n\nwant substring: %s", got, want)
  1436  	}
  1437  }
  1438  
  1439  // Verify that the parallelism flag allows no more than the desired number of
  1440  // concurrent calls to PlanResourceChange.
  1441  func TestPlan_parallelism(t *testing.T) {
  1442  	// Create a temporary working directory that is empty
  1443  	td := t.TempDir()
  1444  	testCopyDir(t, testFixturePath("parallelism"), td)
  1445  	defer testChdir(t, td)()
  1446  
  1447  	par := 4
  1448  
  1449  	// started is a semaphore that we use to ensure that we never have more
  1450  	// than "par" plan operations happening concurrently
  1451  	started := make(chan struct{}, par)
  1452  
  1453  	// beginCtx is used as a starting gate to hold back PlanResourceChange
  1454  	// calls until we reach the desired concurrency. The cancel func "begin" is
  1455  	// called once we reach the desired concurrency, allowing all apply calls
  1456  	// to proceed in unison.
  1457  	beginCtx, begin := context.WithCancel(context.Background())
  1458  
  1459  	// Since our mock provider has its own mutex preventing concurrent calls
  1460  	// to ApplyResourceChange, we need to use a number of separate providers
  1461  	// here. They will all have the same mock implementation function assigned
  1462  	// but crucially they will each have their own mutex.
  1463  	providerFactories := map[addrs.Provider]providers.Factory{}
  1464  	for i := 0; i < 10; i++ {
  1465  		name := fmt.Sprintf("test%d", i)
  1466  		provider := &tofu.MockProvider{}
  1467  		provider.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
  1468  			ResourceTypes: map[string]providers.Schema{
  1469  				name + "_instance": {Block: &configschema.Block{}},
  1470  			},
  1471  		}
  1472  		provider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
  1473  			// If we ever have more than our intended parallelism number of
  1474  			// plan operations running concurrently, the semaphore will fail.
  1475  			select {
  1476  			case started <- struct{}{}:
  1477  				defer func() {
  1478  					<-started
  1479  				}()
  1480  			default:
  1481  				t.Fatal("too many concurrent apply operations")
  1482  			}
  1483  
  1484  			// If we never reach our intended parallelism, the context will
  1485  			// never be canceled and the test will time out.
  1486  			if len(started) >= par {
  1487  				begin()
  1488  			}
  1489  			<-beginCtx.Done()
  1490  
  1491  			// do some "work"
  1492  			// Not required for correctness, but makes it easier to spot a
  1493  			// failure when there is more overlap.
  1494  			time.Sleep(10 * time.Millisecond)
  1495  			return providers.PlanResourceChangeResponse{
  1496  				PlannedState: req.ProposedNewState,
  1497  			}
  1498  		}
  1499  		providerFactories[addrs.NewDefaultProvider(name)] = providers.FactoryFixed(provider)
  1500  	}
  1501  	testingOverrides := &testingOverrides{
  1502  		Providers: providerFactories,
  1503  	}
  1504  
  1505  	view, done := testView(t)
  1506  	c := &PlanCommand{
  1507  		Meta: Meta{
  1508  			testingOverrides: testingOverrides,
  1509  			View:             view,
  1510  		},
  1511  	}
  1512  
  1513  	args := []string{
  1514  		fmt.Sprintf("-parallelism=%d", par),
  1515  	}
  1516  
  1517  	res := c.Run(args)
  1518  	output := done(t)
  1519  	if res != 0 {
  1520  		t.Fatal(output.Stdout())
  1521  	}
  1522  }
  1523  
  1524  func TestPlan_warnings(t *testing.T) {
  1525  	td := t.TempDir()
  1526  	testCopyDir(t, testFixturePath("plan"), td)
  1527  	defer testChdir(t, td)()
  1528  
  1529  	t.Run("full warnings", func(t *testing.T) {
  1530  		p := planWarningsFixtureProvider()
  1531  		view, done := testView(t)
  1532  		c := &PlanCommand{
  1533  			Meta: Meta{
  1534  				testingOverrides: metaOverridesForProvider(p),
  1535  				View:             view,
  1536  			},
  1537  		}
  1538  		code := c.Run([]string{})
  1539  		output := done(t)
  1540  		if code != 0 {
  1541  			t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
  1542  		}
  1543  		// the output should contain 3 warnings (returned by planWarningsFixtureProvider())
  1544  		wantWarnings := []string{
  1545  			"warning 1",
  1546  			"warning 2",
  1547  			"warning 3",
  1548  		}
  1549  		for _, want := range wantWarnings {
  1550  			if !strings.Contains(output.Stdout(), want) {
  1551  				t.Errorf("missing warning %s", want)
  1552  			}
  1553  		}
  1554  	})
  1555  
  1556  	t.Run("compact warnings", func(t *testing.T) {
  1557  		p := planWarningsFixtureProvider()
  1558  		view, done := testView(t)
  1559  		c := &PlanCommand{
  1560  			Meta: Meta{
  1561  				testingOverrides: metaOverridesForProvider(p),
  1562  				View:             view,
  1563  			},
  1564  		}
  1565  		code := c.Run([]string{"-compact-warnings"})
  1566  		output := done(t)
  1567  		if code != 0 {
  1568  			t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
  1569  		}
  1570  		// the output should contain 3 warnings (returned by planWarningsFixtureProvider())
  1571  		// and the message that plan was run with -compact-warnings
  1572  		wantWarnings := []string{
  1573  			"warning 1",
  1574  			"warning 2",
  1575  			"warning 3",
  1576  			"To see the full warning notes, run OpenTofu without -compact-warnings.",
  1577  		}
  1578  		for _, want := range wantWarnings {
  1579  			if !strings.Contains(output.Stdout(), want) {
  1580  				t.Errorf("missing warning %s", want)
  1581  			}
  1582  		}
  1583  	})
  1584  }
  1585  
  1586  func TestPlan_jsonGoldenReference(t *testing.T) {
  1587  	// Create a temporary working directory that is empty
  1588  	td := t.TempDir()
  1589  	testCopyDir(t, testFixturePath("plan"), td)
  1590  	defer testChdir(t, td)()
  1591  
  1592  	p := planFixtureProvider()
  1593  	view, done := testView(t)
  1594  	c := &PlanCommand{
  1595  		Meta: Meta{
  1596  			testingOverrides: metaOverridesForProvider(p),
  1597  			View:             view,
  1598  		},
  1599  	}
  1600  
  1601  	args := []string{
  1602  		"-json",
  1603  	}
  1604  	code := c.Run(args)
  1605  	output := done(t)
  1606  	if code != 0 {
  1607  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
  1608  	}
  1609  
  1610  	checkGoldenReference(t, output, "plan")
  1611  }
  1612  
  1613  // planFixtureSchema returns a schema suitable for processing the
  1614  // configuration in testdata/plan . This schema should be
  1615  // assigned to a mock provider named "test".
  1616  func planFixtureSchema() *providers.GetProviderSchemaResponse {
  1617  	return &providers.GetProviderSchemaResponse{
  1618  		ResourceTypes: map[string]providers.Schema{
  1619  			"test_instance": {
  1620  				Block: &configschema.Block{
  1621  					Attributes: map[string]*configschema.Attribute{
  1622  						"id":  {Type: cty.String, Optional: true, Computed: true},
  1623  						"ami": {Type: cty.String, Optional: true},
  1624  					},
  1625  					BlockTypes: map[string]*configschema.NestedBlock{
  1626  						"network_interface": {
  1627  							Nesting: configschema.NestingList,
  1628  							Block: configschema.Block{
  1629  								Attributes: map[string]*configschema.Attribute{
  1630  									"device_index": {Type: cty.String, Optional: true},
  1631  									"description":  {Type: cty.String, Optional: true},
  1632  								},
  1633  							},
  1634  						},
  1635  					},
  1636  				},
  1637  			},
  1638  		},
  1639  		DataSources: map[string]providers.Schema{
  1640  			"test_data_source": {
  1641  				Block: &configschema.Block{
  1642  					Attributes: map[string]*configschema.Attribute{
  1643  						"id": {
  1644  							Type:     cty.String,
  1645  							Required: true,
  1646  						},
  1647  						"valid": {
  1648  							Type:     cty.Bool,
  1649  							Computed: true,
  1650  						},
  1651  					},
  1652  				},
  1653  			},
  1654  		},
  1655  	}
  1656  }
  1657  
  1658  // planFixtureProvider returns a mock provider that is configured for basic
  1659  // operation with the configuration in testdata/plan. This mock has
  1660  // GetSchemaResponse and PlanResourceChangeFn populated, with the plan
  1661  // step just passing through the new object proposed by OpenTofu Core.
  1662  func planFixtureProvider() *tofu.MockProvider {
  1663  	p := testProvider()
  1664  	p.GetProviderSchemaResponse = planFixtureSchema()
  1665  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
  1666  		return providers.PlanResourceChangeResponse{
  1667  			PlannedState: req.ProposedNewState,
  1668  		}
  1669  	}
  1670  	p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
  1671  		return providers.ReadDataSourceResponse{
  1672  			State: cty.ObjectVal(map[string]cty.Value{
  1673  				"id":    cty.StringVal("zzzzz"),
  1674  				"valid": cty.BoolVal(true),
  1675  			}),
  1676  		}
  1677  	}
  1678  	return p
  1679  }
  1680  
  1681  // planVarsFixtureSchema returns a schema suitable for processing the
  1682  // configuration in testdata/plan-vars . This schema should be
  1683  // assigned to a mock provider named "test".
  1684  func planVarsFixtureSchema() *providers.GetProviderSchemaResponse {
  1685  	return &providers.GetProviderSchemaResponse{
  1686  		ResourceTypes: map[string]providers.Schema{
  1687  			"test_instance": {
  1688  				Block: &configschema.Block{
  1689  					Attributes: map[string]*configschema.Attribute{
  1690  						"id":    {Type: cty.String, Optional: true, Computed: true},
  1691  						"value": {Type: cty.String, Optional: true},
  1692  					},
  1693  				},
  1694  			},
  1695  		},
  1696  	}
  1697  }
  1698  
  1699  // planVarsFixtureProvider returns a mock provider that is configured for basic
  1700  // operation with the configuration in testdata/plan-vars. This mock has
  1701  // GetSchemaResponse and PlanResourceChangeFn populated, with the plan
  1702  // step just passing through the new object proposed by OpenTofu Core.
  1703  func planVarsFixtureProvider() *tofu.MockProvider {
  1704  	p := testProvider()
  1705  	p.GetProviderSchemaResponse = planVarsFixtureSchema()
  1706  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
  1707  		return providers.PlanResourceChangeResponse{
  1708  			PlannedState: req.ProposedNewState,
  1709  		}
  1710  	}
  1711  	p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
  1712  		return providers.ReadDataSourceResponse{
  1713  			State: cty.ObjectVal(map[string]cty.Value{
  1714  				"id":    cty.StringVal("zzzzz"),
  1715  				"valid": cty.BoolVal(true),
  1716  			}),
  1717  		}
  1718  	}
  1719  	return p
  1720  }
  1721  
  1722  // planFixtureProvider returns a mock provider that is configured for basic
  1723  // operation with the configuration in testdata/plan. This mock has
  1724  // GetSchemaResponse and PlanResourceChangeFn populated, returning 3 warnings.
  1725  func planWarningsFixtureProvider() *tofu.MockProvider {
  1726  	p := testProvider()
  1727  	p.GetProviderSchemaResponse = planFixtureSchema()
  1728  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
  1729  		return providers.PlanResourceChangeResponse{
  1730  			Diagnostics: tfdiags.Diagnostics{
  1731  				tfdiags.SimpleWarning("warning 1"),
  1732  				tfdiags.SimpleWarning("warning 2"),
  1733  				tfdiags.SimpleWarning("warning 3"),
  1734  			},
  1735  			PlannedState: req.ProposedNewState,
  1736  		}
  1737  	}
  1738  	p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
  1739  		return providers.ReadDataSourceResponse{
  1740  			State: cty.ObjectVal(map[string]cty.Value{
  1741  				"id":    cty.StringVal("zzzzz"),
  1742  				"valid": cty.BoolVal(true),
  1743  			}),
  1744  		}
  1745  	}
  1746  	return p
  1747  }
  1748  
  1749  const planVarFile = `
  1750  foo = "bar"
  1751  `
  1752  
  1753  const planVarFileWithDecl = `
  1754  foo = "bar"
  1755  
  1756  variable "nope" {
  1757  }
  1758  `