github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/plan_test.go (about)

     1  package command
     2  
     3  import (
     4  	"bytes"
     5  	"io/ioutil"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"strings"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/davecgh/go-spew/spew"
    15  	"github.com/zclconf/go-cty/cty"
    16  
    17  	"github.com/iaas-resource-provision/iaas-rpc/internal/addrs"
    18  	backendinit "github.com/iaas-resource-provision/iaas-rpc/internal/backend/init"
    19  	"github.com/iaas-resource-provision/iaas-rpc/internal/configs/configschema"
    20  	"github.com/iaas-resource-provision/iaas-rpc/internal/plans"
    21  	"github.com/iaas-resource-provision/iaas-rpc/internal/providers"
    22  	"github.com/iaas-resource-provision/iaas-rpc/internal/states"
    23  	"github.com/iaas-resource-provision/iaas-rpc/internal/terraform"
    24  )
    25  
    26  func TestPlan(t *testing.T) {
    27  	td := tempDir(t)
    28  	testCopyDir(t, testFixturePath("plan"), td)
    29  	defer os.RemoveAll(td)
    30  	defer testChdir(t, td)()
    31  
    32  	p := planFixtureProvider()
    33  	view, done := testView(t)
    34  	c := &PlanCommand{
    35  		Meta: Meta{
    36  			testingOverrides: metaOverridesForProvider(p),
    37  			View:             view,
    38  		},
    39  	}
    40  
    41  	args := []string{}
    42  	code := c.Run(args)
    43  	output := done(t)
    44  	if code != 0 {
    45  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
    46  	}
    47  }
    48  
    49  func TestPlan_lockedState(t *testing.T) {
    50  	td := tempDir(t)
    51  	testCopyDir(t, testFixturePath("plan"), td)
    52  	defer os.RemoveAll(td)
    53  	defer testChdir(t, td)()
    54  
    55  	unlock, err := testLockState(testDataDir, filepath.Join(td, DefaultStateFilename))
    56  	if err != nil {
    57  		t.Fatal(err)
    58  	}
    59  	defer unlock()
    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  	if code == 0 {
    73  		t.Fatal("expected error", done(t).Stdout())
    74  	}
    75  
    76  	output := done(t).Stderr()
    77  	if !strings.Contains(output, "lock") {
    78  		t.Fatal("command output does not look like a lock error:", output)
    79  	}
    80  }
    81  
    82  func TestPlan_plan(t *testing.T) {
    83  	tmp, cwd := testCwd(t)
    84  	defer testFixCwd(t, tmp, cwd)
    85  
    86  	planPath := testPlanFileNoop(t)
    87  
    88  	p := testProvider()
    89  	view, done := testView(t)
    90  	c := &PlanCommand{
    91  		Meta: Meta{
    92  			testingOverrides: metaOverridesForProvider(p),
    93  			View:             view,
    94  		},
    95  	}
    96  
    97  	args := []string{planPath}
    98  	code := c.Run(args)
    99  	output := done(t)
   100  	if code != 1 {
   101  		t.Fatalf("wrong exit status %d; want 1\nstderr: %s", code, output.Stderr())
   102  	}
   103  }
   104  
   105  func TestPlan_destroy(t *testing.T) {
   106  	td := tempDir(t)
   107  	testCopyDir(t, testFixturePath("plan"), td)
   108  	defer os.RemoveAll(td)
   109  	defer testChdir(t, td)()
   110  
   111  	originalState := states.BuildState(func(s *states.SyncState) {
   112  		s.SetResourceInstanceCurrent(
   113  			addrs.Resource{
   114  				Mode: addrs.ManagedResourceMode,
   115  				Type: "test_instance",
   116  				Name: "foo",
   117  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   118  			&states.ResourceInstanceObjectSrc{
   119  				AttrsJSON: []byte(`{"id":"bar"}`),
   120  				Status:    states.ObjectReady,
   121  			},
   122  			addrs.AbsProviderConfig{
   123  				Provider: addrs.NewDefaultProvider("test"),
   124  				Module:   addrs.RootModule,
   125  			},
   126  		)
   127  	})
   128  	outPath := testTempFile(t)
   129  	statePath := testStateFile(t, originalState)
   130  
   131  	p := planFixtureProvider()
   132  	view, done := testView(t)
   133  	c := &PlanCommand{
   134  		Meta: Meta{
   135  			testingOverrides: metaOverridesForProvider(p),
   136  			View:             view,
   137  		},
   138  	}
   139  
   140  	args := []string{
   141  		"-destroy",
   142  		"-out", outPath,
   143  		"-state", statePath,
   144  	}
   145  	code := c.Run(args)
   146  	output := done(t)
   147  	if code != 0 {
   148  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   149  	}
   150  
   151  	plan := testReadPlan(t, outPath)
   152  	for _, rc := range plan.Changes.Resources {
   153  		if got, want := rc.Action, plans.Delete; got != want {
   154  			t.Fatalf("wrong action %s for %s; want %s\nplanned change: %s", got, rc.Addr, want, spew.Sdump(rc))
   155  		}
   156  	}
   157  }
   158  
   159  func TestPlan_noState(t *testing.T) {
   160  	td := tempDir(t)
   161  	testCopyDir(t, testFixturePath("plan"), td)
   162  	defer os.RemoveAll(td)
   163  	defer testChdir(t, td)()
   164  
   165  	p := planFixtureProvider()
   166  	view, done := testView(t)
   167  	c := &PlanCommand{
   168  		Meta: Meta{
   169  			testingOverrides: metaOverridesForProvider(p),
   170  			View:             view,
   171  		},
   172  	}
   173  
   174  	args := []string{}
   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  	// Verify that refresh was called
   182  	if p.ReadResourceCalled {
   183  		t.Fatal("ReadResource should not be called")
   184  	}
   185  
   186  	// Verify that the provider was called with the existing state
   187  	actual := p.PlanResourceChangeRequest.PriorState
   188  	expected := cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_instance"].Block.ImpliedType())
   189  	if !expected.RawEquals(actual) {
   190  		t.Fatalf("wrong prior state\ngot:  %#v\nwant: %#v", actual, expected)
   191  	}
   192  }
   193  
   194  func TestPlan_outPath(t *testing.T) {
   195  	td := tempDir(t)
   196  	testCopyDir(t, testFixturePath("plan"), td)
   197  	defer os.RemoveAll(td)
   198  	defer testChdir(t, td)()
   199  
   200  	outPath := filepath.Join(td, "test.plan")
   201  
   202  	p := planFixtureProvider()
   203  	view, done := testView(t)
   204  	c := &PlanCommand{
   205  		Meta: Meta{
   206  			testingOverrides: metaOverridesForProvider(p),
   207  			View:             view,
   208  		},
   209  	}
   210  
   211  	p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{
   212  		PlannedState: cty.NullVal(cty.EmptyObject),
   213  	}
   214  
   215  	args := []string{
   216  		"-out", outPath,
   217  	}
   218  	code := c.Run(args)
   219  	output := done(t)
   220  	if code != 0 {
   221  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   222  	}
   223  
   224  	testReadPlan(t, outPath) // will call t.Fatal itself if the file cannot be read
   225  }
   226  
   227  func TestPlan_outPathNoChange(t *testing.T) {
   228  	td := tempDir(t)
   229  	testCopyDir(t, testFixturePath("plan"), td)
   230  	defer os.RemoveAll(td)
   231  	defer testChdir(t, td)()
   232  
   233  	originalState := states.BuildState(func(s *states.SyncState) {
   234  		s.SetResourceInstanceCurrent(
   235  			addrs.Resource{
   236  				Mode: addrs.ManagedResourceMode,
   237  				Type: "test_instance",
   238  				Name: "foo",
   239  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   240  			&states.ResourceInstanceObjectSrc{
   241  				// Aside from "id" (which is computed) the values here must
   242  				// exactly match the values in the "plan" test fixture in order
   243  				// to produce the empty plan we need for this test.
   244  				AttrsJSON: []byte(`{"id":"bar","ami":"bar","network_interface":[{"description":"Main network interface","device_index":"0"}]}`),
   245  				Status:    states.ObjectReady,
   246  			},
   247  			addrs.AbsProviderConfig{
   248  				Provider: addrs.NewDefaultProvider("test"),
   249  				Module:   addrs.RootModule,
   250  			},
   251  		)
   252  	})
   253  	statePath := testStateFile(t, originalState)
   254  
   255  	outPath := filepath.Join(td, "test.plan")
   256  
   257  	p := planFixtureProvider()
   258  	view, done := testView(t)
   259  	c := &PlanCommand{
   260  		Meta: Meta{
   261  			testingOverrides: metaOverridesForProvider(p),
   262  			View:             view,
   263  		},
   264  	}
   265  
   266  	args := []string{
   267  		"-out", outPath,
   268  		"-state", statePath,
   269  	}
   270  	code := c.Run(args)
   271  	output := done(t)
   272  	if code != 0 {
   273  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   274  	}
   275  
   276  	plan := testReadPlan(t, outPath)
   277  	if !plan.Changes.Empty() {
   278  		t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan))
   279  	}
   280  }
   281  
   282  // When using "-out" with a backend, the plan should encode the backend config
   283  func TestPlan_outBackend(t *testing.T) {
   284  	// Create a temporary working directory that is empty
   285  	td := tempDir(t)
   286  	testCopyDir(t, testFixturePath("plan-out-backend"), td)
   287  	defer os.RemoveAll(td)
   288  	defer testChdir(t, td)()
   289  
   290  	originalState := states.BuildState(func(s *states.SyncState) {
   291  		s.SetResourceInstanceCurrent(
   292  			addrs.Resource{
   293  				Mode: addrs.ManagedResourceMode,
   294  				Type: "test_instance",
   295  				Name: "foo",
   296  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   297  			&states.ResourceInstanceObjectSrc{
   298  				AttrsJSON: []byte(`{"id":"bar","ami":"bar"}`),
   299  				Status:    states.ObjectReady,
   300  			},
   301  			addrs.AbsProviderConfig{
   302  				Provider: addrs.NewDefaultProvider("test"),
   303  				Module:   addrs.RootModule,
   304  			},
   305  		)
   306  	})
   307  
   308  	// Set up our backend state
   309  	dataState, srv := testBackendState(t, originalState, 200)
   310  	defer srv.Close()
   311  	testStateFileRemote(t, dataState)
   312  
   313  	outPath := "foo"
   314  	p := testProvider()
   315  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   316  		ResourceTypes: map[string]providers.Schema{
   317  			"test_instance": {
   318  				Block: &configschema.Block{
   319  					Attributes: map[string]*configschema.Attribute{
   320  						"id": {
   321  							Type:     cty.String,
   322  							Computed: true,
   323  						},
   324  						"ami": {
   325  							Type:     cty.String,
   326  							Optional: true,
   327  						},
   328  					},
   329  				},
   330  			},
   331  		},
   332  	}
   333  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
   334  		return providers.PlanResourceChangeResponse{
   335  			PlannedState: req.ProposedNewState,
   336  		}
   337  	}
   338  	view, done := testView(t)
   339  	c := &PlanCommand{
   340  		Meta: Meta{
   341  			testingOverrides: metaOverridesForProvider(p),
   342  			View:             view,
   343  		},
   344  	}
   345  
   346  	args := []string{
   347  		"-out", outPath,
   348  	}
   349  	code := c.Run(args)
   350  	output := done(t)
   351  	if code != 0 {
   352  		t.Logf("stdout: %s", output.Stdout())
   353  		t.Fatalf("plan command failed with exit code %d\n\n%s", code, output.Stderr())
   354  	}
   355  
   356  	plan := testReadPlan(t, outPath)
   357  	if !plan.Changes.Empty() {
   358  		t.Fatalf("Expected empty plan to be written to plan file, got: %s", spew.Sdump(plan))
   359  	}
   360  
   361  	if got, want := plan.Backend.Type, "http"; got != want {
   362  		t.Errorf("wrong backend type %q; want %q", got, want)
   363  	}
   364  	if got, want := plan.Backend.Workspace, "default"; got != want {
   365  		t.Errorf("wrong backend workspace %q; want %q", got, want)
   366  	}
   367  	{
   368  		httpBackend := backendinit.Backend("http")()
   369  		schema := httpBackend.ConfigSchema()
   370  		got, err := plan.Backend.Config.Decode(schema.ImpliedType())
   371  		if err != nil {
   372  			t.Fatalf("failed to decode backend config in plan: %s", err)
   373  		}
   374  		want, err := dataState.Backend.Config(schema)
   375  		if err != nil {
   376  			t.Fatalf("failed to decode cached config: %s", err)
   377  		}
   378  		if !want.RawEquals(got) {
   379  			t.Errorf("wrong backend config\ngot:  %#v\nwant: %#v", got, want)
   380  		}
   381  	}
   382  }
   383  
   384  func TestPlan_refreshFalse(t *testing.T) {
   385  	// Create a temporary working directory that is empty
   386  	td := tempDir(t)
   387  	testCopyDir(t, testFixturePath("plan"), td)
   388  	defer os.RemoveAll(td)
   389  	defer testChdir(t, td)()
   390  
   391  	p := planFixtureProvider()
   392  	view, done := testView(t)
   393  	c := &PlanCommand{
   394  		Meta: Meta{
   395  			testingOverrides: metaOverridesForProvider(p),
   396  			View:             view,
   397  		},
   398  	}
   399  
   400  	args := []string{
   401  		"-refresh=false",
   402  	}
   403  	code := c.Run(args)
   404  	output := done(t)
   405  	if code != 0 {
   406  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   407  	}
   408  
   409  	if p.ReadResourceCalled {
   410  		t.Fatal("ReadResource should not have been called")
   411  	}
   412  }
   413  
   414  func TestPlan_state(t *testing.T) {
   415  	// Create a temporary working directory that is empty
   416  	td := tempDir(t)
   417  	testCopyDir(t, testFixturePath("plan"), td)
   418  	defer os.RemoveAll(td)
   419  	defer testChdir(t, td)()
   420  
   421  	originalState := testState()
   422  	statePath := testStateFile(t, originalState)
   423  
   424  	p := planFixtureProvider()
   425  	view, done := testView(t)
   426  	c := &PlanCommand{
   427  		Meta: Meta{
   428  			testingOverrides: metaOverridesForProvider(p),
   429  			View:             view,
   430  		},
   431  	}
   432  
   433  	args := []string{
   434  		"-state", statePath,
   435  	}
   436  	code := c.Run(args)
   437  	output := done(t)
   438  	if code != 0 {
   439  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   440  	}
   441  
   442  	// Verify that the provider was called with the existing state
   443  	actual := p.PlanResourceChangeRequest.PriorState
   444  	expected := cty.ObjectVal(map[string]cty.Value{
   445  		"id":  cty.StringVal("bar"),
   446  		"ami": cty.NullVal(cty.String),
   447  		"network_interface": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
   448  			"device_index": cty.String,
   449  			"description":  cty.String,
   450  		}))),
   451  	})
   452  	if !expected.RawEquals(actual) {
   453  		t.Fatalf("wrong prior state\ngot:  %#v\nwant: %#v", actual, expected)
   454  	}
   455  }
   456  
   457  func TestPlan_stateDefault(t *testing.T) {
   458  	// Create a temporary working directory that is empty
   459  	td := tempDir(t)
   460  	testCopyDir(t, testFixturePath("plan"), td)
   461  	defer os.RemoveAll(td)
   462  	defer testChdir(t, td)()
   463  
   464  	// Generate state and move it to the default path
   465  	originalState := testState()
   466  	statePath := testStateFile(t, originalState)
   467  	os.Rename(statePath, path.Join(td, "resource_state.json"))
   468  
   469  	p := planFixtureProvider()
   470  	view, done := testView(t)
   471  	c := &PlanCommand{
   472  		Meta: Meta{
   473  			testingOverrides: metaOverridesForProvider(p),
   474  			View:             view,
   475  		},
   476  	}
   477  
   478  	args := []string{}
   479  	code := c.Run(args)
   480  	output := done(t)
   481  	if code != 0 {
   482  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   483  	}
   484  
   485  	// Verify that the provider was called with the existing state
   486  	actual := p.PlanResourceChangeRequest.PriorState
   487  	expected := cty.ObjectVal(map[string]cty.Value{
   488  		"id":  cty.StringVal("bar"),
   489  		"ami": cty.NullVal(cty.String),
   490  		"network_interface": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
   491  			"device_index": cty.String,
   492  			"description":  cty.String,
   493  		}))),
   494  	})
   495  	if !expected.RawEquals(actual) {
   496  		t.Fatalf("wrong prior state\ngot:  %#v\nwant: %#v", actual, expected)
   497  	}
   498  }
   499  
   500  func TestPlan_validate(t *testing.T) {
   501  	// This is triggered by not asking for input so we have to set this to false
   502  	test = false
   503  	defer func() { test = true }()
   504  
   505  	td := tempDir(t)
   506  	testCopyDir(t, testFixturePath("plan-invalid"), td)
   507  	defer os.RemoveAll(td)
   508  	defer testChdir(t, td)()
   509  
   510  	p := testProvider()
   511  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   512  		ResourceTypes: map[string]providers.Schema{
   513  			"test_instance": {
   514  				Block: &configschema.Block{
   515  					Attributes: map[string]*configschema.Attribute{
   516  						"id": {Type: cty.String, Optional: true, Computed: true},
   517  					},
   518  				},
   519  			},
   520  		},
   521  	}
   522  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
   523  		return providers.PlanResourceChangeResponse{
   524  			PlannedState: req.ProposedNewState,
   525  		}
   526  	}
   527  	view, done := testView(t)
   528  	c := &PlanCommand{
   529  		Meta: Meta{
   530  			testingOverrides: metaOverridesForProvider(p),
   531  			View:             view,
   532  		},
   533  	}
   534  
   535  	args := []string{"-no-color"}
   536  	code := c.Run(args)
   537  	output := done(t)
   538  	if code != 1 {
   539  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   540  	}
   541  
   542  	actual := output.Stderr()
   543  	if want := "Error: Invalid count argument"; !strings.Contains(actual, want) {
   544  		t.Fatalf("unexpected error output\ngot:\n%s\n\nshould contain: %s", actual, want)
   545  	}
   546  	if want := "9:   count = timestamp()"; !strings.Contains(actual, want) {
   547  		t.Fatalf("unexpected error output\ngot:\n%s\n\nshould contain: %s", actual, want)
   548  	}
   549  }
   550  
   551  func TestPlan_vars(t *testing.T) {
   552  	// Create a temporary working directory that is empty
   553  	td := tempDir(t)
   554  	testCopyDir(t, testFixturePath("plan-vars"), td)
   555  	defer os.RemoveAll(td)
   556  	defer testChdir(t, td)()
   557  
   558  	p := planVarsFixtureProvider()
   559  	view, done := testView(t)
   560  	c := &PlanCommand{
   561  		Meta: Meta{
   562  			testingOverrides: metaOverridesForProvider(p),
   563  			View:             view,
   564  		},
   565  	}
   566  
   567  	actual := ""
   568  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
   569  		actual = req.ProposedNewState.GetAttr("value").AsString()
   570  		resp.PlannedState = req.ProposedNewState
   571  		return
   572  	}
   573  
   574  	args := []string{
   575  		"-var", "foo=bar",
   576  	}
   577  	code := c.Run(args)
   578  	output := done(t)
   579  	if code != 0 {
   580  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   581  	}
   582  
   583  	if actual != "bar" {
   584  		t.Fatal("didn't work")
   585  	}
   586  }
   587  
   588  func TestPlan_varsUnset(t *testing.T) {
   589  	// Create a temporary working directory that is empty
   590  	td := tempDir(t)
   591  	testCopyDir(t, testFixturePath("plan-vars"), td)
   592  	defer os.RemoveAll(td)
   593  	defer testChdir(t, td)()
   594  
   595  	// The plan command will prompt for interactive input of var.foo.
   596  	// We'll answer "bar" to that prompt, which should then allow this
   597  	// configuration to apply even though var.foo doesn't have a
   598  	// default value and there are no -var arguments on our command line.
   599  
   600  	// This will (helpfully) panic if more than one variable is requested during plan:
   601  	// https://github.com/iaas-resource-provision/iaas-rpc/issues/26027
   602  	close := testInteractiveInput(t, []string{"bar"})
   603  	defer close()
   604  
   605  	p := planVarsFixtureProvider()
   606  	view, done := testView(t)
   607  	c := &PlanCommand{
   608  		Meta: Meta{
   609  			testingOverrides: metaOverridesForProvider(p),
   610  			View:             view,
   611  		},
   612  	}
   613  
   614  	args := []string{}
   615  	code := c.Run(args)
   616  	output := done(t)
   617  	if code != 0 {
   618  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   619  	}
   620  }
   621  
   622  // This test adds a required argument to the test provider to validate
   623  // processing of user input:
   624  // https://github.com/iaas-resource-provision/iaas-rpc/issues/26035
   625  func TestPlan_providerArgumentUnset(t *testing.T) {
   626  	// Create a temporary working directory that is empty
   627  	td := tempDir(t)
   628  	testCopyDir(t, testFixturePath("plan"), td)
   629  	defer os.RemoveAll(td)
   630  	defer testChdir(t, td)()
   631  
   632  	// Disable test mode so input would be asked
   633  	test = false
   634  	defer func() { test = true }()
   635  
   636  	// The plan command will prompt for interactive input of provider.test.region
   637  	defaultInputReader = bytes.NewBufferString("us-east-1\n")
   638  
   639  	p := planFixtureProvider()
   640  	// override the planFixtureProvider schema to include a required provider argument
   641  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   642  		Provider: providers.Schema{
   643  			Block: &configschema.Block{
   644  				Attributes: map[string]*configschema.Attribute{
   645  					"region": {Type: cty.String, Required: true},
   646  				},
   647  			},
   648  		},
   649  		ResourceTypes: map[string]providers.Schema{
   650  			"test_instance": {
   651  				Block: &configschema.Block{
   652  					Attributes: map[string]*configschema.Attribute{
   653  						"id":  {Type: cty.String, Optional: true, Computed: true},
   654  						"ami": {Type: cty.String, Optional: true, Computed: true},
   655  					},
   656  					BlockTypes: map[string]*configschema.NestedBlock{
   657  						"network_interface": {
   658  							Nesting: configschema.NestingList,
   659  							Block: configschema.Block{
   660  								Attributes: map[string]*configschema.Attribute{
   661  									"device_index": {Type: cty.String, Optional: true},
   662  									"description":  {Type: cty.String, Optional: true},
   663  								},
   664  							},
   665  						},
   666  					},
   667  				},
   668  			},
   669  		},
   670  	}
   671  	view, done := testView(t)
   672  	c := &PlanCommand{
   673  		Meta: Meta{
   674  			testingOverrides: metaOverridesForProvider(p),
   675  			View:             view,
   676  		},
   677  	}
   678  
   679  	args := []string{}
   680  	code := c.Run(args)
   681  	output := done(t)
   682  	if code != 0 {
   683  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   684  	}
   685  }
   686  
   687  func TestPlan_varFile(t *testing.T) {
   688  	// Create a temporary working directory that is empty
   689  	td := tempDir(t)
   690  	testCopyDir(t, testFixturePath("plan-vars"), td)
   691  	defer os.RemoveAll(td)
   692  	defer testChdir(t, td)()
   693  
   694  	varFilePath := testTempFile(t)
   695  	if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil {
   696  		t.Fatalf("err: %s", err)
   697  	}
   698  
   699  	p := planVarsFixtureProvider()
   700  	view, done := testView(t)
   701  	c := &PlanCommand{
   702  		Meta: Meta{
   703  			testingOverrides: metaOverridesForProvider(p),
   704  			View:             view,
   705  		},
   706  	}
   707  
   708  	actual := ""
   709  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
   710  		actual = req.ProposedNewState.GetAttr("value").AsString()
   711  		resp.PlannedState = req.ProposedNewState
   712  		return
   713  	}
   714  
   715  	args := []string{
   716  		"-var-file", varFilePath,
   717  	}
   718  	code := c.Run(args)
   719  	output := done(t)
   720  	if code != 0 {
   721  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   722  	}
   723  
   724  	if actual != "bar" {
   725  		t.Fatal("didn't work")
   726  	}
   727  }
   728  
   729  func TestPlan_varFileDefault(t *testing.T) {
   730  	// Create a temporary working directory that is empty
   731  	td := tempDir(t)
   732  	testCopyDir(t, testFixturePath("plan-vars"), td)
   733  	defer os.RemoveAll(td)
   734  	defer testChdir(t, td)()
   735  
   736  	varFilePath := filepath.Join(td, "terraform.tfvars")
   737  	if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil {
   738  		t.Fatalf("err: %s", err)
   739  	}
   740  
   741  	p := planVarsFixtureProvider()
   742  	view, done := testView(t)
   743  	c := &PlanCommand{
   744  		Meta: Meta{
   745  			testingOverrides: metaOverridesForProvider(p),
   746  			View:             view,
   747  		},
   748  	}
   749  
   750  	actual := ""
   751  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
   752  		actual = req.ProposedNewState.GetAttr("value").AsString()
   753  		resp.PlannedState = req.ProposedNewState
   754  		return
   755  	}
   756  
   757  	args := []string{}
   758  	code := c.Run(args)
   759  	output := done(t)
   760  	if code != 0 {
   761  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   762  	}
   763  
   764  	if actual != "bar" {
   765  		t.Fatal("didn't work")
   766  	}
   767  }
   768  
   769  func TestPlan_varFileWithDecls(t *testing.T) {
   770  	// Create a temporary working directory that is empty
   771  	td := tempDir(t)
   772  	testCopyDir(t, testFixturePath("plan-vars"), td)
   773  	defer os.RemoveAll(td)
   774  	defer testChdir(t, td)()
   775  
   776  	varFilePath := testTempFile(t)
   777  	if err := ioutil.WriteFile(varFilePath, []byte(planVarFileWithDecl), 0644); err != nil {
   778  		t.Fatalf("err: %s", err)
   779  	}
   780  
   781  	p := planVarsFixtureProvider()
   782  	view, done := testView(t)
   783  	c := &PlanCommand{
   784  		Meta: Meta{
   785  			testingOverrides: metaOverridesForProvider(p),
   786  			View:             view,
   787  		},
   788  	}
   789  
   790  	args := []string{
   791  		"-var-file", varFilePath,
   792  	}
   793  	code := c.Run(args)
   794  	output := done(t)
   795  	if code == 0 {
   796  		t.Fatalf("succeeded; want failure\n\n%s", output.Stdout())
   797  	}
   798  
   799  	msg := output.Stderr()
   800  	if got, want := msg, "Variable declaration in .tfvars file"; !strings.Contains(got, want) {
   801  		t.Fatalf("missing expected error message\nwant message containing %q\ngot:\n%s", want, got)
   802  	}
   803  }
   804  
   805  func TestPlan_detailedExitcode(t *testing.T) {
   806  	td := tempDir(t)
   807  	testCopyDir(t, testFixturePath("plan"), td)
   808  	defer os.RemoveAll(td)
   809  	defer testChdir(t, td)()
   810  
   811  	p := planFixtureProvider()
   812  	view, done := testView(t)
   813  	c := &PlanCommand{
   814  		Meta: Meta{
   815  			testingOverrides: metaOverridesForProvider(p),
   816  			View:             view,
   817  		},
   818  	}
   819  
   820  	args := []string{"-detailed-exitcode"}
   821  	code := c.Run(args)
   822  	output := done(t)
   823  	if code != 2 {
   824  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   825  	}
   826  }
   827  
   828  func TestPlan_detailedExitcode_emptyDiff(t *testing.T) {
   829  	td := tempDir(t)
   830  	testCopyDir(t, testFixturePath("plan-emptydiff"), td)
   831  	defer os.RemoveAll(td)
   832  	defer testChdir(t, td)()
   833  
   834  	p := testProvider()
   835  	view, done := testView(t)
   836  	c := &PlanCommand{
   837  		Meta: Meta{
   838  			testingOverrides: metaOverridesForProvider(p),
   839  			View:             view,
   840  		},
   841  	}
   842  
   843  	args := []string{"-detailed-exitcode"}
   844  	code := c.Run(args)
   845  	output := done(t)
   846  	if code != 0 {
   847  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   848  	}
   849  }
   850  
   851  func TestPlan_shutdown(t *testing.T) {
   852  	// Create a temporary working directory that is empty
   853  	td := tempDir(t)
   854  	testCopyDir(t, testFixturePath("apply-shutdown"), td)
   855  	defer os.RemoveAll(td)
   856  	defer testChdir(t, td)()
   857  
   858  	cancelled := make(chan struct{})
   859  	shutdownCh := make(chan struct{})
   860  
   861  	p := testProvider()
   862  	view, done := testView(t)
   863  	c := &PlanCommand{
   864  		Meta: Meta{
   865  			testingOverrides: metaOverridesForProvider(p),
   866  			View:             view,
   867  			ShutdownCh:       shutdownCh,
   868  		},
   869  	}
   870  
   871  	p.StopFn = func() error {
   872  		close(cancelled)
   873  		return nil
   874  	}
   875  
   876  	var once sync.Once
   877  
   878  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
   879  		once.Do(func() {
   880  			shutdownCh <- struct{}{}
   881  		})
   882  
   883  		// Because of the internal lock in the MockProvider, we can't
   884  		// coordinate directly with the calling of Stop, and making the
   885  		// MockProvider concurrent is disruptive to a lot of existing tests.
   886  		// Wait here a moment to help make sure the main goroutine gets to the
   887  		// Stop call before we exit, or the plan may finish before it can be
   888  		// canceled.
   889  		time.Sleep(200 * time.Millisecond)
   890  
   891  		s := req.ProposedNewState.AsValueMap()
   892  		s["ami"] = cty.StringVal("bar")
   893  		resp.PlannedState = cty.ObjectVal(s)
   894  		return
   895  	}
   896  
   897  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   898  		ResourceTypes: map[string]providers.Schema{
   899  			"test_instance": {
   900  				Block: &configschema.Block{
   901  					Attributes: map[string]*configschema.Attribute{
   902  						"ami": {Type: cty.String, Optional: true},
   903  					},
   904  				},
   905  			},
   906  		},
   907  	}
   908  
   909  	code := c.Run([]string{})
   910  	output := done(t)
   911  	if code != 1 {
   912  		t.Errorf("wrong exit code %d; want 1\noutput:\n%s", code, output.Stdout())
   913  	}
   914  
   915  	select {
   916  	case <-cancelled:
   917  	default:
   918  		t.Error("command not cancelled")
   919  	}
   920  }
   921  
   922  func TestPlan_init_required(t *testing.T) {
   923  	td := tempDir(t)
   924  	testCopyDir(t, testFixturePath("plan"), td)
   925  	defer os.RemoveAll(td)
   926  	defer testChdir(t, td)()
   927  
   928  	view, done := testView(t)
   929  	c := &PlanCommand{
   930  		Meta: Meta{
   931  			// Running plan without setting testingOverrides is similar to plan without init
   932  			View: view,
   933  		},
   934  	}
   935  
   936  	args := []string{}
   937  	code := c.Run(args)
   938  	output := done(t)
   939  	if code != 1 {
   940  		t.Fatalf("expected error, got success")
   941  	}
   942  	got := output.Stderr()
   943  	if !strings.Contains(got, `Plugin reinitialization required. Please run "terraform init".`) {
   944  		t.Fatal("wrong error message in output:", got)
   945  	}
   946  }
   947  
   948  // Config with multiple resources, targeting plan of a subset
   949  func TestPlan_targeted(t *testing.T) {
   950  	td := tempDir(t)
   951  	testCopyDir(t, testFixturePath("apply-targeted"), td)
   952  	defer os.RemoveAll(td)
   953  	defer testChdir(t, td)()
   954  
   955  	p := testProvider()
   956  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
   957  		ResourceTypes: map[string]providers.Schema{
   958  			"test_instance": {
   959  				Block: &configschema.Block{
   960  					Attributes: map[string]*configschema.Attribute{
   961  						"id": {Type: cty.String, Computed: true},
   962  					},
   963  				},
   964  			},
   965  		},
   966  	}
   967  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
   968  		return providers.PlanResourceChangeResponse{
   969  			PlannedState: req.ProposedNewState,
   970  		}
   971  	}
   972  
   973  	view, done := testView(t)
   974  	c := &PlanCommand{
   975  		Meta: Meta{
   976  			testingOverrides: metaOverridesForProvider(p),
   977  			View:             view,
   978  		},
   979  	}
   980  
   981  	args := []string{
   982  		"-target", "test_instance.foo",
   983  		"-target", "test_instance.baz",
   984  	}
   985  	code := c.Run(args)
   986  	output := done(t)
   987  	if code != 0 {
   988  		t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
   989  	}
   990  
   991  	if got, want := output.Stdout(), "3 to add, 0 to change, 0 to destroy"; !strings.Contains(got, want) {
   992  		t.Fatalf("bad change summary, want %q, got:\n%s", want, got)
   993  	}
   994  }
   995  
   996  // Diagnostics for invalid -target flags
   997  func TestPlan_targetFlagsDiags(t *testing.T) {
   998  	testCases := map[string]string{
   999  		"test_instance.": "Dot must be followed by attribute name.",
  1000  		"test_instance":  "Resource specification must include a resource type and name.",
  1001  	}
  1002  
  1003  	for target, wantDiag := range testCases {
  1004  		t.Run(target, func(t *testing.T) {
  1005  			td := testTempDir(t)
  1006  			defer os.RemoveAll(td)
  1007  			defer testChdir(t, td)()
  1008  
  1009  			view, done := testView(t)
  1010  			c := &PlanCommand{
  1011  				Meta: Meta{
  1012  					View: view,
  1013  				},
  1014  			}
  1015  
  1016  			args := []string{
  1017  				"-target", target,
  1018  			}
  1019  			code := c.Run(args)
  1020  			output := done(t)
  1021  			if code != 1 {
  1022  				t.Fatalf("bad: %d\n\n%s", code, output.Stdout())
  1023  			}
  1024  
  1025  			got := output.Stderr()
  1026  			if !strings.Contains(got, target) {
  1027  				t.Fatalf("bad error output, want %q, got:\n%s", target, got)
  1028  			}
  1029  			if !strings.Contains(got, wantDiag) {
  1030  				t.Fatalf("bad error output, want %q, got:\n%s", wantDiag, got)
  1031  			}
  1032  		})
  1033  	}
  1034  }
  1035  
  1036  func TestPlan_replace(t *testing.T) {
  1037  	td := tempDir(t)
  1038  	testCopyDir(t, testFixturePath("plan-replace"), td)
  1039  	defer os.RemoveAll(td)
  1040  	defer testChdir(t, td)()
  1041  
  1042  	originalState := states.BuildState(func(s *states.SyncState) {
  1043  		s.SetResourceInstanceCurrent(
  1044  			addrs.Resource{
  1045  				Mode: addrs.ManagedResourceMode,
  1046  				Type: "test_instance",
  1047  				Name: "a",
  1048  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
  1049  			&states.ResourceInstanceObjectSrc{
  1050  				AttrsJSON: []byte(`{"id":"hello"}`),
  1051  				Status:    states.ObjectReady,
  1052  			},
  1053  			addrs.AbsProviderConfig{
  1054  				Provider: addrs.NewDefaultProvider("test"),
  1055  				Module:   addrs.RootModule,
  1056  			},
  1057  		)
  1058  	})
  1059  	statePath := testStateFile(t, originalState)
  1060  
  1061  	p := testProvider()
  1062  	p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
  1063  		ResourceTypes: map[string]providers.Schema{
  1064  			"test_instance": {
  1065  				Block: &configschema.Block{
  1066  					Attributes: map[string]*configschema.Attribute{
  1067  						"id": {Type: cty.String, Computed: true},
  1068  					},
  1069  				},
  1070  			},
  1071  		},
  1072  	}
  1073  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
  1074  		return providers.PlanResourceChangeResponse{
  1075  			PlannedState: req.ProposedNewState,
  1076  		}
  1077  	}
  1078  
  1079  	view, done := testView(t)
  1080  	c := &PlanCommand{
  1081  		Meta: Meta{
  1082  			testingOverrides: metaOverridesForProvider(p),
  1083  			View:             view,
  1084  		},
  1085  	}
  1086  
  1087  	args := []string{
  1088  		"-state", statePath,
  1089  		"-no-color",
  1090  		"-replace", "test_instance.a",
  1091  	}
  1092  	code := c.Run(args)
  1093  	output := done(t)
  1094  	if code != 0 {
  1095  		t.Fatalf("wrong exit code %d\n\n%s", code, output.Stderr())
  1096  	}
  1097  
  1098  	stdout := output.Stdout()
  1099  	if got, want := stdout, "1 to add, 0 to change, 1 to destroy"; !strings.Contains(got, want) {
  1100  		t.Errorf("wrong plan summary\ngot output:\n%s\n\nwant substring: %s", got, want)
  1101  	}
  1102  	if got, want := stdout, "test_instance.a will be replaced, as requested"; !strings.Contains(got, want) {
  1103  		t.Errorf("missing replace explanation\ngot output:\n%s\n\nwant substring: %s", got, want)
  1104  	}
  1105  
  1106  }
  1107  
  1108  // planFixtureSchema returns a schema suitable for processing the
  1109  // configuration in testdata/plan . This schema should be
  1110  // assigned to a mock provider named "test".
  1111  func planFixtureSchema() *providers.GetProviderSchemaResponse {
  1112  	return &providers.GetProviderSchemaResponse{
  1113  		ResourceTypes: map[string]providers.Schema{
  1114  			"test_instance": {
  1115  				Block: &configschema.Block{
  1116  					Attributes: map[string]*configschema.Attribute{
  1117  						"id":  {Type: cty.String, Optional: true, Computed: true},
  1118  						"ami": {Type: cty.String, Optional: true},
  1119  					},
  1120  					BlockTypes: map[string]*configschema.NestedBlock{
  1121  						"network_interface": {
  1122  							Nesting: configschema.NestingList,
  1123  							Block: configschema.Block{
  1124  								Attributes: map[string]*configschema.Attribute{
  1125  									"device_index": {Type: cty.String, Optional: true},
  1126  									"description":  {Type: cty.String, Optional: true},
  1127  								},
  1128  							},
  1129  						},
  1130  					},
  1131  				},
  1132  			},
  1133  		},
  1134  	}
  1135  }
  1136  
  1137  // planFixtureProvider returns a mock provider that is configured for basic
  1138  // operation with the configuration in testdata/plan. This mock has
  1139  // GetSchemaResponse and PlanResourceChangeFn populated, with the plan
  1140  // step just passing through the new object proposed by Terraform Core.
  1141  func planFixtureProvider() *terraform.MockProvider {
  1142  	p := testProvider()
  1143  	p.GetProviderSchemaResponse = planFixtureSchema()
  1144  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
  1145  		return providers.PlanResourceChangeResponse{
  1146  			PlannedState: req.ProposedNewState,
  1147  		}
  1148  	}
  1149  	return p
  1150  }
  1151  
  1152  // planVarsFixtureSchema returns a schema suitable for processing the
  1153  // configuration in testdata/plan-vars . This schema should be
  1154  // assigned to a mock provider named "test".
  1155  func planVarsFixtureSchema() *providers.GetProviderSchemaResponse {
  1156  	return &providers.GetProviderSchemaResponse{
  1157  		ResourceTypes: map[string]providers.Schema{
  1158  			"test_instance": {
  1159  				Block: &configschema.Block{
  1160  					Attributes: map[string]*configschema.Attribute{
  1161  						"id":    {Type: cty.String, Optional: true, Computed: true},
  1162  						"value": {Type: cty.String, Optional: true},
  1163  					},
  1164  				},
  1165  			},
  1166  		},
  1167  	}
  1168  }
  1169  
  1170  // planVarsFixtureProvider returns a mock provider that is configured for basic
  1171  // operation with the configuration in testdata/plan-vars. This mock has
  1172  // GetSchemaResponse and PlanResourceChangeFn populated, with the plan
  1173  // step just passing through the new object proposed by Terraform Core.
  1174  func planVarsFixtureProvider() *terraform.MockProvider {
  1175  	p := testProvider()
  1176  	p.GetProviderSchemaResponse = planVarsFixtureSchema()
  1177  	p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
  1178  		return providers.PlanResourceChangeResponse{
  1179  			PlannedState: req.ProposedNewState,
  1180  		}
  1181  	}
  1182  	return p
  1183  }
  1184  
  1185  const planVarFile = `
  1186  foo = "bar"
  1187  `
  1188  
  1189  const planVarFileWithDecl = `
  1190  foo = "bar"
  1191  
  1192  variable "nope" {
  1193  }
  1194  `