github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/plan_test.go (about)

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