github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/cloud/backend_plan_test.go (about)

     1  package cloud
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"os/signal"
     7  	"strings"
     8  	"syscall"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  	tfe "github.com/hashicorp/go-tfe"
    14  	"github.com/hashicorp/terraform/internal/addrs"
    15  	"github.com/hashicorp/terraform/internal/backend"
    16  	"github.com/hashicorp/terraform/internal/command/arguments"
    17  	"github.com/hashicorp/terraform/internal/command/clistate"
    18  	"github.com/hashicorp/terraform/internal/command/views"
    19  	"github.com/hashicorp/terraform/internal/depsfile"
    20  	"github.com/hashicorp/terraform/internal/initwd"
    21  	"github.com/hashicorp/terraform/internal/plans"
    22  	"github.com/hashicorp/terraform/internal/plans/planfile"
    23  	"github.com/hashicorp/terraform/internal/states/statemgr"
    24  	"github.com/hashicorp/terraform/internal/terminal"
    25  	"github.com/hashicorp/terraform/internal/terraform"
    26  	"github.com/mitchellh/cli"
    27  )
    28  
    29  func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
    30  	t.Helper()
    31  
    32  	return testOperationPlanWithTimeout(t, configDir, 0)
    33  }
    34  
    35  func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
    36  	t.Helper()
    37  
    38  	_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
    39  
    40  	streams, done := terminal.StreamsForTesting(t)
    41  	view := views.NewView(streams)
    42  	stateLockerView := views.NewStateLocker(arguments.ViewHuman, view)
    43  	operationView := views.NewOperation(arguments.ViewHuman, false, view)
    44  
    45  	// Many of our tests use an overridden "null" provider that's just in-memory
    46  	// inside the test process, not a separate plugin on disk.
    47  	depLocks := depsfile.NewLocks()
    48  	depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null"))
    49  
    50  	return &backend.Operation{
    51  		ConfigDir:       configDir,
    52  		ConfigLoader:    configLoader,
    53  		PlanRefresh:     true,
    54  		StateLocker:     clistate.NewLocker(timeout, stateLockerView),
    55  		Type:            backend.OperationTypePlan,
    56  		View:            operationView,
    57  		DependencyLocks: depLocks,
    58  	}, configCleanup, done
    59  }
    60  
    61  func TestCloud_planBasic(t *testing.T) {
    62  	b, bCleanup := testBackendWithName(t)
    63  	defer bCleanup()
    64  
    65  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
    66  	defer configCleanup()
    67  	defer done(t)
    68  
    69  	op.Workspace = testBackendSingleWorkspaceName
    70  
    71  	run, err := b.Operation(context.Background(), op)
    72  	if err != nil {
    73  		t.Fatalf("error starting operation: %v", err)
    74  	}
    75  
    76  	<-run.Done()
    77  	if run.Result != backend.OperationSuccess {
    78  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
    79  	}
    80  	if run.PlanEmpty {
    81  		t.Fatal("expected a non-empty plan")
    82  	}
    83  
    84  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
    85  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
    86  		t.Fatalf("expected TFC header in output: %s", output)
    87  	}
    88  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
    89  		t.Fatalf("expected plan summary in output: %s", output)
    90  	}
    91  
    92  	stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
    93  	// An error suggests that the state was not unlocked after the operation finished
    94  	if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
    95  		t.Fatalf("unexpected error locking state after successful plan: %s", err.Error())
    96  	}
    97  }
    98  
    99  func TestCloud_planCanceled(t *testing.T) {
   100  	b, bCleanup := testBackendWithName(t)
   101  	defer bCleanup()
   102  
   103  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   104  	defer configCleanup()
   105  	defer done(t)
   106  
   107  	op.Workspace = testBackendSingleWorkspaceName
   108  
   109  	run, err := b.Operation(context.Background(), op)
   110  	if err != nil {
   111  		t.Fatalf("error starting operation: %v", err)
   112  	}
   113  
   114  	// Stop the run to simulate a Ctrl-C.
   115  	run.Stop()
   116  
   117  	<-run.Done()
   118  	if run.Result == backend.OperationSuccess {
   119  		t.Fatal("expected plan operation to fail")
   120  	}
   121  
   122  	stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
   123  	// An error suggests that the state was not unlocked after the operation finished
   124  	if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
   125  		t.Fatalf("unexpected error locking state after cancelled plan: %s", err.Error())
   126  	}
   127  }
   128  
   129  func TestCloud_planLongLine(t *testing.T) {
   130  	b, bCleanup := testBackendWithName(t)
   131  	defer bCleanup()
   132  
   133  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-long-line")
   134  	defer configCleanup()
   135  	defer done(t)
   136  
   137  	op.Workspace = testBackendSingleWorkspaceName
   138  
   139  	run, err := b.Operation(context.Background(), op)
   140  	if err != nil {
   141  		t.Fatalf("error starting operation: %v", err)
   142  	}
   143  
   144  	<-run.Done()
   145  	if run.Result != backend.OperationSuccess {
   146  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   147  	}
   148  	if run.PlanEmpty {
   149  		t.Fatal("expected a non-empty plan")
   150  	}
   151  
   152  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   153  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
   154  		t.Fatalf("expected TFC header in output: %s", output)
   155  	}
   156  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   157  		t.Fatalf("expected plan summary in output: %s", output)
   158  	}
   159  }
   160  
   161  func TestCloud_planWithoutPermissions(t *testing.T) {
   162  	b, bCleanup := testBackendWithTags(t)
   163  	defer bCleanup()
   164  
   165  	// Create a named workspace without permissions.
   166  	w, err := b.client.Workspaces.Create(
   167  		context.Background(),
   168  		b.organization,
   169  		tfe.WorkspaceCreateOptions{
   170  			Name: tfe.String("prod"),
   171  		},
   172  	)
   173  	if err != nil {
   174  		t.Fatalf("error creating named workspace: %v", err)
   175  	}
   176  	w.Permissions.CanQueueRun = false
   177  
   178  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   179  	defer configCleanup()
   180  
   181  	op.Workspace = "prod"
   182  
   183  	run, err := b.Operation(context.Background(), op)
   184  	if err != nil {
   185  		t.Fatalf("error starting operation: %v", err)
   186  	}
   187  
   188  	<-run.Done()
   189  	output := done(t)
   190  	if run.Result == backend.OperationSuccess {
   191  		t.Fatal("expected plan operation to fail")
   192  	}
   193  
   194  	errOutput := output.Stderr()
   195  	if !strings.Contains(errOutput, "Insufficient rights to generate a plan") {
   196  		t.Fatalf("expected a permissions error, got: %v", errOutput)
   197  	}
   198  }
   199  
   200  func TestCloud_planWithParallelism(t *testing.T) {
   201  	b, bCleanup := testBackendWithName(t)
   202  	defer bCleanup()
   203  
   204  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   205  	defer configCleanup()
   206  
   207  	if b.ContextOpts == nil {
   208  		b.ContextOpts = &terraform.ContextOpts{}
   209  	}
   210  	b.ContextOpts.Parallelism = 3
   211  	op.Workspace = testBackendSingleWorkspaceName
   212  
   213  	run, err := b.Operation(context.Background(), op)
   214  	if err != nil {
   215  		t.Fatalf("error starting operation: %v", err)
   216  	}
   217  
   218  	<-run.Done()
   219  	output := done(t)
   220  	if run.Result == backend.OperationSuccess {
   221  		t.Fatal("expected plan operation to fail")
   222  	}
   223  
   224  	errOutput := output.Stderr()
   225  	if !strings.Contains(errOutput, "parallelism values are currently not supported") {
   226  		t.Fatalf("expected a parallelism error, got: %v", errOutput)
   227  	}
   228  }
   229  
   230  func TestCloud_planWithPlan(t *testing.T) {
   231  	b, bCleanup := testBackendWithName(t)
   232  	defer bCleanup()
   233  
   234  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   235  	defer configCleanup()
   236  
   237  	op.PlanFile = &planfile.Reader{}
   238  	op.Workspace = testBackendSingleWorkspaceName
   239  
   240  	run, err := b.Operation(context.Background(), op)
   241  	if err != nil {
   242  		t.Fatalf("error starting operation: %v", err)
   243  	}
   244  
   245  	<-run.Done()
   246  	output := done(t)
   247  	if run.Result == backend.OperationSuccess {
   248  		t.Fatal("expected plan operation to fail")
   249  	}
   250  	if !run.PlanEmpty {
   251  		t.Fatalf("expected plan to be empty")
   252  	}
   253  
   254  	errOutput := output.Stderr()
   255  	if !strings.Contains(errOutput, "saved plan is currently not supported") {
   256  		t.Fatalf("expected a saved plan error, got: %v", errOutput)
   257  	}
   258  }
   259  
   260  func TestCloud_planWithPath(t *testing.T) {
   261  	b, bCleanup := testBackendWithName(t)
   262  	defer bCleanup()
   263  
   264  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   265  	defer configCleanup()
   266  
   267  	op.PlanOutPath = "./testdata/plan"
   268  	op.Workspace = testBackendSingleWorkspaceName
   269  
   270  	run, err := b.Operation(context.Background(), op)
   271  	if err != nil {
   272  		t.Fatalf("error starting operation: %v", err)
   273  	}
   274  
   275  	<-run.Done()
   276  	output := done(t)
   277  	if run.Result == backend.OperationSuccess {
   278  		t.Fatal("expected plan operation to fail")
   279  	}
   280  	if !run.PlanEmpty {
   281  		t.Fatalf("expected plan to be empty")
   282  	}
   283  
   284  	errOutput := output.Stderr()
   285  	if !strings.Contains(errOutput, "generated plan is currently not supported") {
   286  		t.Fatalf("expected a generated plan error, got: %v", errOutput)
   287  	}
   288  }
   289  
   290  func TestCloud_planWithoutRefresh(t *testing.T) {
   291  	b, bCleanup := testBackendWithName(t)
   292  	defer bCleanup()
   293  
   294  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   295  	defer configCleanup()
   296  	defer done(t)
   297  
   298  	op.PlanRefresh = false
   299  	op.Workspace = testBackendSingleWorkspaceName
   300  
   301  	run, err := b.Operation(context.Background(), op)
   302  	if err != nil {
   303  		t.Fatalf("error starting operation: %v", err)
   304  	}
   305  
   306  	<-run.Done()
   307  	if run.Result != backend.OperationSuccess {
   308  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   309  	}
   310  	if run.PlanEmpty {
   311  		t.Fatal("expected a non-empty plan")
   312  	}
   313  
   314  	// We should find a run inside the mock client that has refresh set
   315  	// to false.
   316  	runsAPI := b.client.Runs.(*MockRuns)
   317  	if got, want := len(runsAPI.Runs), 1; got != want {
   318  		t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
   319  	}
   320  	for _, run := range runsAPI.Runs {
   321  		if diff := cmp.Diff(false, run.Refresh); diff != "" {
   322  			t.Errorf("wrong Refresh setting in the created run\n%s", diff)
   323  		}
   324  	}
   325  }
   326  
   327  func TestCloud_planWithRefreshOnly(t *testing.T) {
   328  	b, bCleanup := testBackendWithName(t)
   329  	defer bCleanup()
   330  
   331  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   332  	defer configCleanup()
   333  	defer done(t)
   334  
   335  	op.PlanMode = plans.RefreshOnlyMode
   336  	op.Workspace = testBackendSingleWorkspaceName
   337  
   338  	run, err := b.Operation(context.Background(), op)
   339  	if err != nil {
   340  		t.Fatalf("error starting operation: %v", err)
   341  	}
   342  
   343  	<-run.Done()
   344  	if run.Result != backend.OperationSuccess {
   345  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   346  	}
   347  	if run.PlanEmpty {
   348  		t.Fatal("expected a non-empty plan")
   349  	}
   350  
   351  	// We should find a run inside the mock client that has refresh-only set
   352  	// to true.
   353  	runsAPI := b.client.Runs.(*MockRuns)
   354  	if got, want := len(runsAPI.Runs), 1; got != want {
   355  		t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
   356  	}
   357  	for _, run := range runsAPI.Runs {
   358  		if diff := cmp.Diff(true, run.RefreshOnly); diff != "" {
   359  			t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff)
   360  		}
   361  	}
   362  }
   363  
   364  func TestCloud_planWithTarget(t *testing.T) {
   365  	b, bCleanup := testBackendWithName(t)
   366  	defer bCleanup()
   367  
   368  	// When the backend code creates a new run, we'll tweak it so that it
   369  	// has a cost estimation object with the "skipped_due_to_targeting" status,
   370  	// emulating how a real server is expected to behave in that case.
   371  	b.client.Runs.(*MockRuns).ModifyNewRun = func(client *MockClient, options tfe.RunCreateOptions, run *tfe.Run) {
   372  		const fakeID = "fake"
   373  		// This is the cost estimate object embedded in the run itself which
   374  		// the backend will use to learn the ID to request from the cost
   375  		// estimates endpoint. It's pending to simulate what a freshly-created
   376  		// run is likely to look like.
   377  		run.CostEstimate = &tfe.CostEstimate{
   378  			ID:     fakeID,
   379  			Status: "pending",
   380  		}
   381  		// The backend will then use the main cost estimation API to retrieve
   382  		// the same ID indicated in the object above, where we'll then return
   383  		// the status "skipped_due_to_targeting" to trigger the special skip
   384  		// message in the backend output.
   385  		client.CostEstimates.Estimations[fakeID] = &tfe.CostEstimate{
   386  			ID:     fakeID,
   387  			Status: "skipped_due_to_targeting",
   388  		}
   389  	}
   390  
   391  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   392  	defer configCleanup()
   393  	defer done(t)
   394  
   395  	addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
   396  
   397  	op.Targets = []addrs.Targetable{addr}
   398  	op.Workspace = testBackendSingleWorkspaceName
   399  
   400  	run, err := b.Operation(context.Background(), op)
   401  	if err != nil {
   402  		t.Fatalf("error starting operation: %v", err)
   403  	}
   404  
   405  	<-run.Done()
   406  	if run.Result != backend.OperationSuccess {
   407  		t.Fatal("expected plan operation to succeed")
   408  	}
   409  	if run.PlanEmpty {
   410  		t.Fatalf("expected plan to be non-empty")
   411  	}
   412  
   413  	// testBackendDefault above attached a "mock UI" to our backend, so we
   414  	// can retrieve its non-error output via the OutputWriter in-memory buffer.
   415  	gotOutput := b.CLI.(*cli.MockUi).OutputWriter.String()
   416  	if wantOutput := "Not available for this plan, because it was created with the -target option."; !strings.Contains(gotOutput, wantOutput) {
   417  		t.Errorf("missing message about skipped cost estimation\ngot:\n%s\nwant substring: %s", gotOutput, wantOutput)
   418  	}
   419  
   420  	// We should find a run inside the mock client that has the same
   421  	// target address we requested above.
   422  	runsAPI := b.client.Runs.(*MockRuns)
   423  	if got, want := len(runsAPI.Runs), 1; got != want {
   424  		t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
   425  	}
   426  	for _, run := range runsAPI.Runs {
   427  		if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" {
   428  			t.Errorf("wrong TargetAddrs in the created run\n%s", diff)
   429  		}
   430  	}
   431  }
   432  
   433  func TestCloud_planWithReplace(t *testing.T) {
   434  	b, bCleanup := testBackendWithName(t)
   435  	defer bCleanup()
   436  
   437  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   438  	defer configCleanup()
   439  	defer done(t)
   440  
   441  	addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo")
   442  
   443  	op.ForceReplace = []addrs.AbsResourceInstance{addr}
   444  	op.Workspace = testBackendSingleWorkspaceName
   445  
   446  	run, err := b.Operation(context.Background(), op)
   447  	if err != nil {
   448  		t.Fatalf("error starting operation: %v", err)
   449  	}
   450  
   451  	<-run.Done()
   452  	if run.Result != backend.OperationSuccess {
   453  		t.Fatal("expected plan operation to succeed")
   454  	}
   455  	if run.PlanEmpty {
   456  		t.Fatalf("expected plan to be non-empty")
   457  	}
   458  
   459  	// We should find a run inside the mock client that has the same
   460  	// refresh address we requested above.
   461  	runsAPI := b.client.Runs.(*MockRuns)
   462  	if got, want := len(runsAPI.Runs), 1; got != want {
   463  		t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
   464  	}
   465  	for _, run := range runsAPI.Runs {
   466  		if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" {
   467  			t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff)
   468  		}
   469  	}
   470  }
   471  
   472  func TestCloud_planWithRequiredVariables(t *testing.T) {
   473  	b, bCleanup := testBackendWithName(t)
   474  	defer bCleanup()
   475  
   476  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-variables")
   477  	defer configCleanup()
   478  	defer done(t)
   479  
   480  	op.Variables = testVariables(terraform.ValueFromCLIArg, "foo") // "bar" variable defined in config is  missing
   481  	op.Workspace = testBackendSingleWorkspaceName
   482  
   483  	run, err := b.Operation(context.Background(), op)
   484  	if err != nil {
   485  		t.Fatalf("error starting operation: %v", err)
   486  	}
   487  
   488  	<-run.Done()
   489  	// The usual error of a required variable being missing is deferred and the operation
   490  	// is successful.
   491  	if run.Result != backend.OperationSuccess {
   492  		t.Fatal("expected plan operation to succeed")
   493  	}
   494  
   495  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   496  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
   497  		t.Fatalf("unexpected TFC header in output: %s", output)
   498  	}
   499  }
   500  
   501  func TestCloud_planNoConfig(t *testing.T) {
   502  	b, bCleanup := testBackendWithName(t)
   503  	defer bCleanup()
   504  
   505  	op, configCleanup, done := testOperationPlan(t, "./testdata/empty")
   506  	defer configCleanup()
   507  
   508  	op.Workspace = testBackendSingleWorkspaceName
   509  
   510  	run, err := b.Operation(context.Background(), op)
   511  	if err != nil {
   512  		t.Fatalf("error starting operation: %v", err)
   513  	}
   514  
   515  	<-run.Done()
   516  	output := done(t)
   517  	if run.Result == backend.OperationSuccess {
   518  		t.Fatal("expected plan operation to fail")
   519  	}
   520  	if !run.PlanEmpty {
   521  		t.Fatalf("expected plan to be empty")
   522  	}
   523  
   524  	errOutput := output.Stderr()
   525  	if !strings.Contains(errOutput, "configuration files found") {
   526  		t.Fatalf("expected configuration files error, got: %v", errOutput)
   527  	}
   528  }
   529  
   530  func TestCloud_planNoChanges(t *testing.T) {
   531  	b, bCleanup := testBackendWithName(t)
   532  	defer bCleanup()
   533  
   534  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-no-changes")
   535  	defer configCleanup()
   536  	defer done(t)
   537  
   538  	op.Workspace = testBackendSingleWorkspaceName
   539  
   540  	run, err := b.Operation(context.Background(), op)
   541  	if err != nil {
   542  		t.Fatalf("error starting operation: %v", err)
   543  	}
   544  
   545  	<-run.Done()
   546  	if run.Result != backend.OperationSuccess {
   547  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   548  	}
   549  	if !run.PlanEmpty {
   550  		t.Fatalf("expected plan to be empty")
   551  	}
   552  
   553  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   554  	if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") {
   555  		t.Fatalf("expected no changes in plan summary: %s", output)
   556  	}
   557  	if !strings.Contains(output, "Sentinel Result: true") {
   558  		t.Fatalf("expected policy check result in output: %s", output)
   559  	}
   560  }
   561  
   562  func TestCloud_planForceLocal(t *testing.T) {
   563  	// Set TF_FORCE_LOCAL_BACKEND so the cloud backend will use
   564  	// the local backend with itself as embedded backend.
   565  	if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
   566  		t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
   567  	}
   568  	defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
   569  
   570  	b, bCleanup := testBackendWithName(t)
   571  	defer bCleanup()
   572  
   573  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   574  	defer configCleanup()
   575  	defer done(t)
   576  
   577  	op.Workspace = testBackendSingleWorkspaceName
   578  
   579  	streams, done := terminal.StreamsForTesting(t)
   580  	view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
   581  	op.View = view
   582  
   583  	run, err := b.Operation(context.Background(), op)
   584  	if err != nil {
   585  		t.Fatalf("error starting operation: %v", err)
   586  	}
   587  
   588  	<-run.Done()
   589  	if run.Result != backend.OperationSuccess {
   590  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   591  	}
   592  	if run.PlanEmpty {
   593  		t.Fatalf("expected a non-empty plan")
   594  	}
   595  
   596  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   597  	if strings.Contains(output, "Running plan in Terraform Cloud") {
   598  		t.Fatalf("unexpected TFC header in output: %s", output)
   599  	}
   600  	if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   601  		t.Fatalf("expected plan summary in output: %s", output)
   602  	}
   603  }
   604  
   605  func TestCloud_planWithoutOperationsEntitlement(t *testing.T) {
   606  	b, bCleanup := testBackendNoOperations(t)
   607  	defer bCleanup()
   608  
   609  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   610  	defer configCleanup()
   611  	defer done(t)
   612  
   613  	op.Workspace = testBackendSingleWorkspaceName
   614  
   615  	streams, done := terminal.StreamsForTesting(t)
   616  	view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
   617  	op.View = view
   618  
   619  	run, err := b.Operation(context.Background(), op)
   620  	if err != nil {
   621  		t.Fatalf("error starting operation: %v", err)
   622  	}
   623  
   624  	<-run.Done()
   625  	if run.Result != backend.OperationSuccess {
   626  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   627  	}
   628  	if run.PlanEmpty {
   629  		t.Fatalf("expected a non-empty plan")
   630  	}
   631  
   632  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   633  	if strings.Contains(output, "Running plan in Terraform Cloud") {
   634  		t.Fatalf("unexpected TFC header in output: %s", output)
   635  	}
   636  	if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   637  		t.Fatalf("expected plan summary in output: %s", output)
   638  	}
   639  }
   640  
   641  func TestCloud_planWorkspaceWithoutOperations(t *testing.T) {
   642  	b, bCleanup := testBackendWithTags(t)
   643  	defer bCleanup()
   644  
   645  	ctx := context.Background()
   646  
   647  	// Create a named workspace that doesn't allow operations.
   648  	_, err := b.client.Workspaces.Create(
   649  		ctx,
   650  		b.organization,
   651  		tfe.WorkspaceCreateOptions{
   652  			Name: tfe.String("no-operations"),
   653  		},
   654  	)
   655  	if err != nil {
   656  		t.Fatalf("error creating named workspace: %v", err)
   657  	}
   658  
   659  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   660  	defer configCleanup()
   661  	defer done(t)
   662  
   663  	op.Workspace = "no-operations"
   664  
   665  	streams, done := terminal.StreamsForTesting(t)
   666  	view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
   667  	op.View = view
   668  
   669  	run, err := b.Operation(ctx, op)
   670  	if err != nil {
   671  		t.Fatalf("error starting operation: %v", err)
   672  	}
   673  
   674  	<-run.Done()
   675  	if run.Result != backend.OperationSuccess {
   676  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   677  	}
   678  	if run.PlanEmpty {
   679  		t.Fatalf("expected a non-empty plan")
   680  	}
   681  
   682  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   683  	if strings.Contains(output, "Running plan in Terraform Cloud") {
   684  		t.Fatalf("unexpected TFC header in output: %s", output)
   685  	}
   686  	if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   687  		t.Fatalf("expected plan summary in output: %s", output)
   688  	}
   689  }
   690  
   691  func TestCloud_planLockTimeout(t *testing.T) {
   692  	b, bCleanup := testBackendWithName(t)
   693  	defer bCleanup()
   694  
   695  	ctx := context.Background()
   696  
   697  	// Retrieve the workspace used to run this operation in.
   698  	w, err := b.client.Workspaces.Read(ctx, b.organization, b.WorkspaceMapping.Name)
   699  	if err != nil {
   700  		t.Fatalf("error retrieving workspace: %v", err)
   701  	}
   702  
   703  	// Create a new configuration version.
   704  	c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{})
   705  	if err != nil {
   706  		t.Fatalf("error creating configuration version: %v", err)
   707  	}
   708  
   709  	// Create a pending run to block this run.
   710  	_, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{
   711  		ConfigurationVersion: c,
   712  		Workspace:            w,
   713  	})
   714  	if err != nil {
   715  		t.Fatalf("error creating pending run: %v", err)
   716  	}
   717  
   718  	op, configCleanup, done := testOperationPlanWithTimeout(t, "./testdata/plan", 50)
   719  	defer configCleanup()
   720  	defer done(t)
   721  
   722  	input := testInput(t, map[string]string{
   723  		"cancel":  "yes",
   724  		"approve": "yes",
   725  	})
   726  
   727  	op.UIIn = input
   728  	op.UIOut = b.CLI
   729  	op.Workspace = testBackendSingleWorkspaceName
   730  
   731  	_, err = b.Operation(context.Background(), op)
   732  	if err != nil {
   733  		t.Fatalf("error starting operation: %v", err)
   734  	}
   735  
   736  	sigint := make(chan os.Signal, 1)
   737  	signal.Notify(sigint, syscall.SIGINT)
   738  	select {
   739  	case <-sigint:
   740  		// Stop redirecting SIGINT signals.
   741  		signal.Stop(sigint)
   742  	case <-time.After(200 * time.Millisecond):
   743  		t.Fatalf("expected lock timeout after 50 milliseconds, waited 200 milliseconds")
   744  	}
   745  
   746  	if len(input.answers) != 2 {
   747  		t.Fatalf("expected unused answers, got: %v", input.answers)
   748  	}
   749  
   750  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   751  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
   752  		t.Fatalf("expected TFC header in output: %s", output)
   753  	}
   754  	if !strings.Contains(output, "Lock timeout exceeded") {
   755  		t.Fatalf("expected lock timout error in output: %s", output)
   756  	}
   757  	if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   758  		t.Fatalf("unexpected plan summary in output: %s", output)
   759  	}
   760  }
   761  
   762  func TestCloud_planDestroy(t *testing.T) {
   763  	b, bCleanup := testBackendWithName(t)
   764  	defer bCleanup()
   765  
   766  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
   767  	defer configCleanup()
   768  	defer done(t)
   769  
   770  	op.PlanMode = plans.DestroyMode
   771  	op.Workspace = testBackendSingleWorkspaceName
   772  
   773  	run, err := b.Operation(context.Background(), op)
   774  	if err != nil {
   775  		t.Fatalf("error starting operation: %v", err)
   776  	}
   777  
   778  	<-run.Done()
   779  	if run.Result != backend.OperationSuccess {
   780  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   781  	}
   782  	if run.PlanEmpty {
   783  		t.Fatalf("expected a non-empty plan")
   784  	}
   785  }
   786  
   787  func TestCloud_planDestroyNoConfig(t *testing.T) {
   788  	b, bCleanup := testBackendWithName(t)
   789  	defer bCleanup()
   790  
   791  	op, configCleanup, done := testOperationPlan(t, "./testdata/empty")
   792  	defer configCleanup()
   793  	defer done(t)
   794  
   795  	op.PlanMode = plans.DestroyMode
   796  	op.Workspace = testBackendSingleWorkspaceName
   797  
   798  	run, err := b.Operation(context.Background(), op)
   799  	if err != nil {
   800  		t.Fatalf("error starting operation: %v", err)
   801  	}
   802  
   803  	<-run.Done()
   804  	if run.Result != backend.OperationSuccess {
   805  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   806  	}
   807  	if run.PlanEmpty {
   808  		t.Fatalf("expected a non-empty plan")
   809  	}
   810  }
   811  
   812  func TestCloud_planWithWorkingDirectory(t *testing.T) {
   813  	b, bCleanup := testBackendWithName(t)
   814  	defer bCleanup()
   815  
   816  	options := tfe.WorkspaceUpdateOptions{
   817  		WorkingDirectory: tfe.String("terraform"),
   818  	}
   819  
   820  	// Configure the workspace to use a custom working directory.
   821  	_, err := b.client.Workspaces.Update(context.Background(), b.organization, b.WorkspaceMapping.Name, options)
   822  	if err != nil {
   823  		t.Fatalf("error configuring working directory: %v", err)
   824  	}
   825  
   826  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-working-directory/terraform")
   827  	defer configCleanup()
   828  	defer done(t)
   829  
   830  	op.Workspace = testBackendSingleWorkspaceName
   831  
   832  	run, err := b.Operation(context.Background(), op)
   833  	if err != nil {
   834  		t.Fatalf("error starting operation: %v", err)
   835  	}
   836  
   837  	<-run.Done()
   838  	if run.Result != backend.OperationSuccess {
   839  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   840  	}
   841  	if run.PlanEmpty {
   842  		t.Fatalf("expected a non-empty plan")
   843  	}
   844  
   845  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   846  	if !strings.Contains(output, "The remote workspace is configured to work with configuration") {
   847  		t.Fatalf("expected working directory warning: %s", output)
   848  	}
   849  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
   850  		t.Fatalf("expected TFC header in output: %s", output)
   851  	}
   852  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   853  		t.Fatalf("expected plan summary in output: %s", output)
   854  	}
   855  }
   856  
   857  func TestCloud_planWithWorkingDirectoryFromCurrentPath(t *testing.T) {
   858  	b, bCleanup := testBackendWithName(t)
   859  	defer bCleanup()
   860  
   861  	options := tfe.WorkspaceUpdateOptions{
   862  		WorkingDirectory: tfe.String("terraform"),
   863  	}
   864  
   865  	// Configure the workspace to use a custom working directory.
   866  	_, err := b.client.Workspaces.Update(context.Background(), b.organization, b.WorkspaceMapping.Name, options)
   867  	if err != nil {
   868  		t.Fatalf("error configuring working directory: %v", err)
   869  	}
   870  
   871  	wd, err := os.Getwd()
   872  	if err != nil {
   873  		t.Fatalf("error getting current working directory: %v", err)
   874  	}
   875  
   876  	// We need to change into the configuration directory to make sure
   877  	// the logic to upload the correct slug is working as expected.
   878  	if err := os.Chdir("./testdata/plan-with-working-directory/terraform"); err != nil {
   879  		t.Fatalf("error changing directory: %v", err)
   880  	}
   881  	defer os.Chdir(wd) // Make sure we change back again when were done.
   882  
   883  	// For this test we need to give our current directory instead of the
   884  	// full path to the configuration as we already changed directories.
   885  	op, configCleanup, done := testOperationPlan(t, ".")
   886  	defer configCleanup()
   887  	defer done(t)
   888  
   889  	op.Workspace = testBackendSingleWorkspaceName
   890  
   891  	run, err := b.Operation(context.Background(), op)
   892  	if err != nil {
   893  		t.Fatalf("error starting operation: %v", err)
   894  	}
   895  
   896  	<-run.Done()
   897  	if run.Result != backend.OperationSuccess {
   898  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   899  	}
   900  	if run.PlanEmpty {
   901  		t.Fatalf("expected a non-empty plan")
   902  	}
   903  
   904  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   905  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
   906  		t.Fatalf("expected TFC header in output: %s", output)
   907  	}
   908  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   909  		t.Fatalf("expected plan summary in output: %s", output)
   910  	}
   911  }
   912  
   913  func TestCloud_planCostEstimation(t *testing.T) {
   914  	b, bCleanup := testBackendWithName(t)
   915  	defer bCleanup()
   916  
   917  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-cost-estimation")
   918  	defer configCleanup()
   919  	defer done(t)
   920  
   921  	op.Workspace = testBackendSingleWorkspaceName
   922  
   923  	run, err := b.Operation(context.Background(), op)
   924  	if err != nil {
   925  		t.Fatalf("error starting operation: %v", err)
   926  	}
   927  
   928  	<-run.Done()
   929  	if run.Result != backend.OperationSuccess {
   930  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   931  	}
   932  	if run.PlanEmpty {
   933  		t.Fatalf("expected a non-empty plan")
   934  	}
   935  
   936  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   937  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
   938  		t.Fatalf("expected TFC header in output: %s", output)
   939  	}
   940  	if !strings.Contains(output, "Resources: 1 of 1 estimated") {
   941  		t.Fatalf("expected cost estimate result in output: %s", output)
   942  	}
   943  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   944  		t.Fatalf("expected plan summary in output: %s", output)
   945  	}
   946  }
   947  
   948  func TestCloud_planPolicyPass(t *testing.T) {
   949  	b, bCleanup := testBackendWithName(t)
   950  	defer bCleanup()
   951  
   952  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-passed")
   953  	defer configCleanup()
   954  	defer done(t)
   955  
   956  	op.Workspace = testBackendSingleWorkspaceName
   957  
   958  	run, err := b.Operation(context.Background(), op)
   959  	if err != nil {
   960  		t.Fatalf("error starting operation: %v", err)
   961  	}
   962  
   963  	<-run.Done()
   964  	if run.Result != backend.OperationSuccess {
   965  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   966  	}
   967  	if run.PlanEmpty {
   968  		t.Fatalf("expected a non-empty plan")
   969  	}
   970  
   971  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   972  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
   973  		t.Fatalf("expected TFC header in output: %s", output)
   974  	}
   975  	if !strings.Contains(output, "Sentinel Result: true") {
   976  		t.Fatalf("expected policy check result in output: %s", output)
   977  	}
   978  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   979  		t.Fatalf("expected plan summary in output: %s", output)
   980  	}
   981  }
   982  
   983  func TestCloud_planPolicyHardFail(t *testing.T) {
   984  	b, bCleanup := testBackendWithName(t)
   985  	defer bCleanup()
   986  
   987  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-hard-failed")
   988  	defer configCleanup()
   989  
   990  	op.Workspace = testBackendSingleWorkspaceName
   991  
   992  	run, err := b.Operation(context.Background(), op)
   993  	if err != nil {
   994  		t.Fatalf("error starting operation: %v", err)
   995  	}
   996  
   997  	<-run.Done()
   998  	viewOutput := done(t)
   999  	if run.Result == backend.OperationSuccess {
  1000  		t.Fatal("expected plan operation to fail")
  1001  	}
  1002  	if !run.PlanEmpty {
  1003  		t.Fatalf("expected plan to be empty")
  1004  	}
  1005  
  1006  	errOutput := viewOutput.Stderr()
  1007  	if !strings.Contains(errOutput, "hard failed") {
  1008  		t.Fatalf("expected a policy check error, got: %v", errOutput)
  1009  	}
  1010  
  1011  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1012  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
  1013  		t.Fatalf("expected TFC header in output: %s", output)
  1014  	}
  1015  	if !strings.Contains(output, "Sentinel Result: false") {
  1016  		t.Fatalf("expected policy check result in output: %s", output)
  1017  	}
  1018  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1019  		t.Fatalf("expected plan summary in output: %s", output)
  1020  	}
  1021  }
  1022  
  1023  func TestCloud_planPolicySoftFail(t *testing.T) {
  1024  	b, bCleanup := testBackendWithName(t)
  1025  	defer bCleanup()
  1026  
  1027  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-soft-failed")
  1028  	defer configCleanup()
  1029  
  1030  	op.Workspace = testBackendSingleWorkspaceName
  1031  
  1032  	run, err := b.Operation(context.Background(), op)
  1033  	if err != nil {
  1034  		t.Fatalf("error starting operation: %v", err)
  1035  	}
  1036  
  1037  	<-run.Done()
  1038  	viewOutput := done(t)
  1039  	if run.Result == backend.OperationSuccess {
  1040  		t.Fatal("expected plan operation to fail")
  1041  	}
  1042  	if !run.PlanEmpty {
  1043  		t.Fatalf("expected plan to be empty")
  1044  	}
  1045  
  1046  	errOutput := viewOutput.Stderr()
  1047  	if !strings.Contains(errOutput, "soft failed") {
  1048  		t.Fatalf("expected a policy check error, got: %v", errOutput)
  1049  	}
  1050  
  1051  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1052  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
  1053  		t.Fatalf("expected TFC header in output: %s", output)
  1054  	}
  1055  	if !strings.Contains(output, "Sentinel Result: false") {
  1056  		t.Fatalf("expected policy check result in output: %s", output)
  1057  	}
  1058  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1059  		t.Fatalf("expected plan summary in output: %s", output)
  1060  	}
  1061  }
  1062  
  1063  func TestCloud_planWithRemoteError(t *testing.T) {
  1064  	b, bCleanup := testBackendWithName(t)
  1065  	defer bCleanup()
  1066  
  1067  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-error")
  1068  	defer configCleanup()
  1069  	defer done(t)
  1070  
  1071  	op.Workspace = testBackendSingleWorkspaceName
  1072  
  1073  	run, err := b.Operation(context.Background(), op)
  1074  	if err != nil {
  1075  		t.Fatalf("error starting operation: %v", err)
  1076  	}
  1077  
  1078  	<-run.Done()
  1079  	if run.Result == backend.OperationSuccess {
  1080  		t.Fatal("expected plan operation to fail")
  1081  	}
  1082  	if run.Result.ExitStatus() != 1 {
  1083  		t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus())
  1084  	}
  1085  
  1086  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1087  	if !strings.Contains(output, "Running plan in Terraform Cloud") {
  1088  		t.Fatalf("expected TFC header in output: %s", output)
  1089  	}
  1090  	if !strings.Contains(output, "null_resource.foo: 1 error") {
  1091  		t.Fatalf("expected plan error in output: %s", output)
  1092  	}
  1093  }
  1094  
  1095  func TestCloud_planOtherError(t *testing.T) {
  1096  	b, bCleanup := testBackendWithName(t)
  1097  	defer bCleanup()
  1098  
  1099  	op, configCleanup, done := testOperationPlan(t, "./testdata/plan")
  1100  	defer configCleanup()
  1101  	defer done(t)
  1102  
  1103  	op.Workspace = "network-error" // custom error response in backend_mock.go
  1104  
  1105  	_, err := b.Operation(context.Background(), op)
  1106  	if err == nil {
  1107  		t.Errorf("expected error, got success")
  1108  	}
  1109  
  1110  	if !strings.Contains(err.Error(),
  1111  		"Terraform Cloud returned an unexpected error:\n\nI'm a little teacup") {
  1112  		t.Fatalf("expected error message, got: %s", err.Error())
  1113  	}
  1114  }