github.com/kevinklinger/open_terraform@v1.3.6/noninternal/cloud/backend_apply_test.go (about)

     1  package cloud
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"os/signal"
     8  	"strings"
     9  	"syscall"
    10  	"testing"
    11  	"time"
    12  
    13  	gomock "github.com/golang/mock/gomock"
    14  	"github.com/google/go-cmp/cmp"
    15  	tfe "github.com/hashicorp/go-tfe"
    16  	mocks "github.com/hashicorp/go-tfe/mocks"
    17  	version "github.com/hashicorp/go-version"
    18  	"github.com/kevinklinger/open_terraform/noninternal/addrs"
    19  	"github.com/kevinklinger/open_terraform/noninternal/backend"
    20  	"github.com/kevinklinger/open_terraform/noninternal/command/arguments"
    21  	"github.com/kevinklinger/open_terraform/noninternal/command/clistate"
    22  	"github.com/kevinklinger/open_terraform/noninternal/command/views"
    23  	"github.com/kevinklinger/open_terraform/noninternal/depsfile"
    24  	"github.com/kevinklinger/open_terraform/noninternal/initwd"
    25  	"github.com/kevinklinger/open_terraform/noninternal/plans"
    26  	"github.com/kevinklinger/open_terraform/noninternal/plans/planfile"
    27  	"github.com/kevinklinger/open_terraform/noninternal/states/statemgr"
    28  	"github.com/kevinklinger/open_terraform/noninternal/terminal"
    29  	"github.com/kevinklinger/open_terraform/noninternal/terraform"
    30  	tfversion "github.com/kevinklinger/open_terraform/version"
    31  	"github.com/mitchellh/cli"
    32  )
    33  
    34  func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
    35  	t.Helper()
    36  
    37  	return testOperationApplyWithTimeout(t, configDir, 0)
    38  }
    39  
    40  func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) {
    41  	t.Helper()
    42  
    43  	_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
    44  
    45  	streams, done := terminal.StreamsForTesting(t)
    46  	view := views.NewView(streams)
    47  	stateLockerView := views.NewStateLocker(arguments.ViewHuman, view)
    48  	operationView := views.NewOperation(arguments.ViewHuman, false, view)
    49  
    50  	// Many of our tests use an overridden "null" provider that's just in-memory
    51  	// inside the test process, not a separate plugin on disk.
    52  	depLocks := depsfile.NewLocks()
    53  	depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null"))
    54  
    55  	return &backend.Operation{
    56  		ConfigDir:       configDir,
    57  		ConfigLoader:    configLoader,
    58  		PlanRefresh:     true,
    59  		StateLocker:     clistate.NewLocker(timeout, stateLockerView),
    60  		Type:            backend.OperationTypeApply,
    61  		View:            operationView,
    62  		DependencyLocks: depLocks,
    63  	}, configCleanup, done
    64  }
    65  
    66  func TestCloud_applyBasic(t *testing.T) {
    67  	b, bCleanup := testBackendWithName(t)
    68  	defer bCleanup()
    69  
    70  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
    71  	defer configCleanup()
    72  	defer done(t)
    73  
    74  	input := testInput(t, map[string]string{
    75  		"approve": "yes",
    76  	})
    77  
    78  	op.UIIn = input
    79  	op.UIOut = b.CLI
    80  	op.Workspace = testBackendSingleWorkspaceName
    81  
    82  	run, err := b.Operation(context.Background(), op)
    83  	if err != nil {
    84  		t.Fatalf("error starting operation: %v", err)
    85  	}
    86  
    87  	<-run.Done()
    88  	if run.Result != backend.OperationSuccess {
    89  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
    90  	}
    91  	if run.PlanEmpty {
    92  		t.Fatalf("expected a non-empty plan")
    93  	}
    94  
    95  	if len(input.answers) > 0 {
    96  		t.Fatalf("expected no unused answers, got: %v", input.answers)
    97  	}
    98  
    99  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   100  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
   101  		t.Fatalf("expected TFC header in output: %s", output)
   102  	}
   103  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   104  		t.Fatalf("expected plan summery in output: %s", output)
   105  	}
   106  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   107  		t.Fatalf("expected apply summery in output: %s", output)
   108  	}
   109  
   110  	stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
   111  	// An error suggests that the state was not unlocked after apply
   112  	if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
   113  		t.Fatalf("unexpected error locking state after apply: %s", err.Error())
   114  	}
   115  }
   116  
   117  func TestCloud_applyCanceled(t *testing.T) {
   118  	b, bCleanup := testBackendWithName(t)
   119  	defer bCleanup()
   120  
   121  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   122  	defer configCleanup()
   123  	defer done(t)
   124  
   125  	op.Workspace = testBackendSingleWorkspaceName
   126  
   127  	run, err := b.Operation(context.Background(), op)
   128  	if err != nil {
   129  		t.Fatalf("error starting operation: %v", err)
   130  	}
   131  
   132  	// Stop the run to simulate a Ctrl-C.
   133  	run.Stop()
   134  
   135  	<-run.Done()
   136  	if run.Result == backend.OperationSuccess {
   137  		t.Fatal("expected apply operation to fail")
   138  	}
   139  
   140  	stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
   141  	if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
   142  		t.Fatalf("unexpected error locking state after cancelling apply: %s", err.Error())
   143  	}
   144  }
   145  
   146  func TestCloud_applyWithoutPermissions(t *testing.T) {
   147  	b, bCleanup := testBackendWithTags(t)
   148  	defer bCleanup()
   149  
   150  	// Create a named workspace without permissions.
   151  	w, err := b.client.Workspaces.Create(
   152  		context.Background(),
   153  		b.organization,
   154  		tfe.WorkspaceCreateOptions{
   155  			Name: tfe.String("prod"),
   156  		},
   157  	)
   158  	if err != nil {
   159  		t.Fatalf("error creating named workspace: %v", err)
   160  	}
   161  	w.Permissions.CanQueueApply = false
   162  
   163  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   164  	defer configCleanup()
   165  
   166  	op.UIOut = b.CLI
   167  	op.Workspace = "prod"
   168  
   169  	run, err := b.Operation(context.Background(), op)
   170  	if err != nil {
   171  		t.Fatalf("error starting operation: %v", err)
   172  	}
   173  
   174  	<-run.Done()
   175  	output := done(t)
   176  	if run.Result == backend.OperationSuccess {
   177  		t.Fatal("expected apply operation to fail")
   178  	}
   179  
   180  	errOutput := output.Stderr()
   181  	if !strings.Contains(errOutput, "Insufficient rights to apply changes") {
   182  		t.Fatalf("expected a permissions error, got: %v", errOutput)
   183  	}
   184  }
   185  
   186  func TestCloud_applyWithVCS(t *testing.T) {
   187  	b, bCleanup := testBackendWithTags(t)
   188  	defer bCleanup()
   189  
   190  	// Create a named workspace with a VCS.
   191  	_, err := b.client.Workspaces.Create(
   192  		context.Background(),
   193  		b.organization,
   194  		tfe.WorkspaceCreateOptions{
   195  			Name:    tfe.String("prod"),
   196  			VCSRepo: &tfe.VCSRepoOptions{},
   197  		},
   198  	)
   199  	if err != nil {
   200  		t.Fatalf("error creating named workspace: %v", err)
   201  	}
   202  
   203  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   204  	defer configCleanup()
   205  
   206  	op.Workspace = "prod"
   207  
   208  	run, err := b.Operation(context.Background(), op)
   209  	if err != nil {
   210  		t.Fatalf("error starting operation: %v", err)
   211  	}
   212  
   213  	<-run.Done()
   214  	output := done(t)
   215  	if run.Result == backend.OperationSuccess {
   216  		t.Fatal("expected apply operation to fail")
   217  	}
   218  	if !run.PlanEmpty {
   219  		t.Fatalf("expected plan to be empty")
   220  	}
   221  
   222  	errOutput := output.Stderr()
   223  	if !strings.Contains(errOutput, "not allowed for workspaces with a VCS") {
   224  		t.Fatalf("expected a VCS error, got: %v", errOutput)
   225  	}
   226  }
   227  
   228  func TestCloud_applyWithParallelism(t *testing.T) {
   229  	b, bCleanup := testBackendWithName(t)
   230  	defer bCleanup()
   231  
   232  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   233  	defer configCleanup()
   234  
   235  	if b.ContextOpts == nil {
   236  		b.ContextOpts = &terraform.ContextOpts{}
   237  	}
   238  	b.ContextOpts.Parallelism = 3
   239  	op.Workspace = testBackendSingleWorkspaceName
   240  
   241  	run, err := b.Operation(context.Background(), op)
   242  	if err != nil {
   243  		t.Fatalf("error starting operation: %v", err)
   244  	}
   245  
   246  	<-run.Done()
   247  	output := done(t)
   248  	if run.Result == backend.OperationSuccess {
   249  		t.Fatal("expected apply operation to fail")
   250  	}
   251  
   252  	errOutput := output.Stderr()
   253  	if !strings.Contains(errOutput, "parallelism values are currently not supported") {
   254  		t.Fatalf("expected a parallelism error, got: %v", errOutput)
   255  	}
   256  }
   257  
   258  func TestCloud_applyWithPlan(t *testing.T) {
   259  	b, bCleanup := testBackendWithName(t)
   260  	defer bCleanup()
   261  
   262  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   263  	defer configCleanup()
   264  
   265  	op.PlanFile = &planfile.Reader{}
   266  	op.Workspace = testBackendSingleWorkspaceName
   267  
   268  	run, err := b.Operation(context.Background(), op)
   269  	if err != nil {
   270  		t.Fatalf("error starting operation: %v", err)
   271  	}
   272  
   273  	<-run.Done()
   274  	output := done(t)
   275  	if run.Result == backend.OperationSuccess {
   276  		t.Fatal("expected apply operation to fail")
   277  	}
   278  	if !run.PlanEmpty {
   279  		t.Fatalf("expected plan to be empty")
   280  	}
   281  
   282  	errOutput := output.Stderr()
   283  	if !strings.Contains(errOutput, "saved plan is currently not supported") {
   284  		t.Fatalf("expected a saved plan error, got: %v", errOutput)
   285  	}
   286  }
   287  
   288  func TestCloud_applyWithoutRefresh(t *testing.T) {
   289  	b, bCleanup := testBackendWithName(t)
   290  	defer bCleanup()
   291  
   292  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   293  	defer configCleanup()
   294  	defer done(t)
   295  
   296  	op.PlanRefresh = false
   297  	op.Workspace = testBackendSingleWorkspaceName
   298  
   299  	run, err := b.Operation(context.Background(), op)
   300  	if err != nil {
   301  		t.Fatalf("error starting operation: %v", err)
   302  	}
   303  
   304  	<-run.Done()
   305  	if run.Result != backend.OperationSuccess {
   306  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   307  	}
   308  	if run.PlanEmpty {
   309  		t.Fatalf("expected plan to be non-empty")
   310  	}
   311  
   312  	// We should find a run inside the mock client that has refresh set
   313  	// to false.
   314  	runsAPI := b.client.Runs.(*MockRuns)
   315  	if got, want := len(runsAPI.Runs), 1; got != want {
   316  		t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
   317  	}
   318  	for _, run := range runsAPI.Runs {
   319  		if diff := cmp.Diff(false, run.Refresh); diff != "" {
   320  			t.Errorf("wrong Refresh setting in the created run\n%s", diff)
   321  		}
   322  	}
   323  }
   324  
   325  func TestCloud_applyWithRefreshOnly(t *testing.T) {
   326  	b, bCleanup := testBackendWithName(t)
   327  	defer bCleanup()
   328  
   329  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   330  	defer configCleanup()
   331  	defer done(t)
   332  
   333  	op.PlanMode = plans.RefreshOnlyMode
   334  	op.Workspace = testBackendSingleWorkspaceName
   335  
   336  	run, err := b.Operation(context.Background(), op)
   337  	if err != nil {
   338  		t.Fatalf("error starting operation: %v", err)
   339  	}
   340  
   341  	<-run.Done()
   342  	if run.Result != backend.OperationSuccess {
   343  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   344  	}
   345  	if run.PlanEmpty {
   346  		t.Fatalf("expected plan to be non-empty")
   347  	}
   348  
   349  	// We should find a run inside the mock client that has refresh-only set
   350  	// to true.
   351  	runsAPI := b.client.Runs.(*MockRuns)
   352  	if got, want := len(runsAPI.Runs), 1; got != want {
   353  		t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
   354  	}
   355  	for _, run := range runsAPI.Runs {
   356  		if diff := cmp.Diff(true, run.RefreshOnly); diff != "" {
   357  			t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff)
   358  		}
   359  	}
   360  }
   361  
   362  func TestCloud_applyWithTarget(t *testing.T) {
   363  	b, bCleanup := testBackendWithName(t)
   364  	defer bCleanup()
   365  
   366  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   367  	defer configCleanup()
   368  	defer done(t)
   369  
   370  	addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
   371  
   372  	op.Targets = []addrs.Targetable{addr}
   373  	op.Workspace = testBackendSingleWorkspaceName
   374  
   375  	run, err := b.Operation(context.Background(), op)
   376  	if err != nil {
   377  		t.Fatalf("error starting operation: %v", err)
   378  	}
   379  
   380  	<-run.Done()
   381  	if run.Result != backend.OperationSuccess {
   382  		t.Fatal("expected apply operation to succeed")
   383  	}
   384  	if run.PlanEmpty {
   385  		t.Fatalf("expected plan to be non-empty")
   386  	}
   387  
   388  	// We should find a run inside the mock client that has the same
   389  	// target address we requested above.
   390  	runsAPI := b.client.Runs.(*MockRuns)
   391  	if got, want := len(runsAPI.Runs), 1; got != want {
   392  		t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
   393  	}
   394  	for _, run := range runsAPI.Runs {
   395  		if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" {
   396  			t.Errorf("wrong TargetAddrs in the created run\n%s", diff)
   397  		}
   398  	}
   399  }
   400  
   401  func TestCloud_applyWithReplace(t *testing.T) {
   402  	b, bCleanup := testBackendWithName(t)
   403  	defer bCleanup()
   404  
   405  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   406  	defer configCleanup()
   407  	defer done(t)
   408  
   409  	addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo")
   410  
   411  	op.ForceReplace = []addrs.AbsResourceInstance{addr}
   412  	op.Workspace = testBackendSingleWorkspaceName
   413  
   414  	run, err := b.Operation(context.Background(), op)
   415  	if err != nil {
   416  		t.Fatalf("error starting operation: %v", err)
   417  	}
   418  
   419  	<-run.Done()
   420  	if run.Result != backend.OperationSuccess {
   421  		t.Fatal("expected plan operation to succeed")
   422  	}
   423  	if run.PlanEmpty {
   424  		t.Fatalf("expected plan to be non-empty")
   425  	}
   426  
   427  	// We should find a run inside the mock client that has the same
   428  	// refresh address we requested above.
   429  	runsAPI := b.client.Runs.(*MockRuns)
   430  	if got, want := len(runsAPI.Runs), 1; got != want {
   431  		t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
   432  	}
   433  	for _, run := range runsAPI.Runs {
   434  		if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" {
   435  			t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff)
   436  		}
   437  	}
   438  }
   439  
   440  func TestCloud_applyWithRequiredVariables(t *testing.T) {
   441  	b, bCleanup := testBackendWithName(t)
   442  	defer bCleanup()
   443  
   444  	op, configCleanup, done := testOperationApply(t, "./testdata/apply-variables")
   445  	defer configCleanup()
   446  	defer done(t)
   447  
   448  	op.Variables = testVariables(terraform.ValueFromNamedFile, "foo") // "bar" variable value missing
   449  	op.Workspace = testBackendSingleWorkspaceName
   450  
   451  	run, err := b.Operation(context.Background(), op)
   452  	if err != nil {
   453  		t.Fatalf("error starting operation: %v", err)
   454  	}
   455  
   456  	<-run.Done()
   457  	// The usual error of a required variable being missing is deferred and the operation
   458  	// is successful
   459  	if run.Result != backend.OperationSuccess {
   460  		t.Fatal("expected plan operation to succeed")
   461  	}
   462  
   463  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   464  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
   465  		t.Fatalf("unexpected TFC header in output: %s", output)
   466  	}
   467  }
   468  
   469  func TestCloud_applyNoConfig(t *testing.T) {
   470  	b, bCleanup := testBackendWithName(t)
   471  	defer bCleanup()
   472  
   473  	op, configCleanup, done := testOperationApply(t, "./testdata/empty")
   474  	defer configCleanup()
   475  
   476  	op.Workspace = testBackendSingleWorkspaceName
   477  
   478  	run, err := b.Operation(context.Background(), op)
   479  	if err != nil {
   480  		t.Fatalf("error starting operation: %v", err)
   481  	}
   482  
   483  	<-run.Done()
   484  	output := done(t)
   485  	if run.Result == backend.OperationSuccess {
   486  		t.Fatal("expected apply operation to fail")
   487  	}
   488  	if !run.PlanEmpty {
   489  		t.Fatalf("expected plan to be empty")
   490  	}
   491  
   492  	errOutput := output.Stderr()
   493  	if !strings.Contains(errOutput, "configuration files found") {
   494  		t.Fatalf("expected configuration files error, got: %v", errOutput)
   495  	}
   496  
   497  	stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName)
   498  	// An error suggests that the state was not unlocked after apply
   499  	if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil {
   500  		t.Fatalf("unexpected error locking state after failed apply: %s", err.Error())
   501  	}
   502  }
   503  
   504  func TestCloud_applyNoChanges(t *testing.T) {
   505  	b, bCleanup := testBackendWithName(t)
   506  	defer bCleanup()
   507  
   508  	op, configCleanup, done := testOperationApply(t, "./testdata/apply-no-changes")
   509  	defer configCleanup()
   510  	defer done(t)
   511  
   512  	op.Workspace = testBackendSingleWorkspaceName
   513  
   514  	run, err := b.Operation(context.Background(), op)
   515  	if err != nil {
   516  		t.Fatalf("error starting operation: %v", err)
   517  	}
   518  
   519  	<-run.Done()
   520  	if run.Result != backend.OperationSuccess {
   521  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   522  	}
   523  	if !run.PlanEmpty {
   524  		t.Fatalf("expected plan to be empty")
   525  	}
   526  
   527  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   528  	if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") {
   529  		t.Fatalf("expected no changes in plan summery: %s", output)
   530  	}
   531  	if !strings.Contains(output, "Sentinel Result: true") {
   532  		t.Fatalf("expected policy check result in output: %s", output)
   533  	}
   534  }
   535  
   536  func TestCloud_applyNoApprove(t *testing.T) {
   537  	b, bCleanup := testBackendWithName(t)
   538  	defer bCleanup()
   539  
   540  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   541  	defer configCleanup()
   542  
   543  	input := testInput(t, map[string]string{
   544  		"approve": "no",
   545  	})
   546  
   547  	op.UIIn = input
   548  	op.UIOut = b.CLI
   549  	op.Workspace = testBackendSingleWorkspaceName
   550  
   551  	run, err := b.Operation(context.Background(), op)
   552  	if err != nil {
   553  		t.Fatalf("error starting operation: %v", err)
   554  	}
   555  
   556  	<-run.Done()
   557  	output := done(t)
   558  	if run.Result == backend.OperationSuccess {
   559  		t.Fatal("expected apply operation to fail")
   560  	}
   561  	if !run.PlanEmpty {
   562  		t.Fatalf("expected plan to be empty")
   563  	}
   564  
   565  	if len(input.answers) > 0 {
   566  		t.Fatalf("expected no unused answers, got: %v", input.answers)
   567  	}
   568  
   569  	errOutput := output.Stderr()
   570  	if !strings.Contains(errOutput, "Apply discarded") {
   571  		t.Fatalf("expected an apply discarded error, got: %v", errOutput)
   572  	}
   573  }
   574  
   575  func TestCloud_applyAutoApprove(t *testing.T) {
   576  	b, bCleanup := testBackendWithName(t)
   577  	defer bCleanup()
   578  	ctrl := gomock.NewController(t)
   579  
   580  	applyMock := mocks.NewMockApplies(ctrl)
   581  	// This needs three new lines because we check for a minimum of three lines
   582  	// in the parsing of logs in `opApply` function.
   583  	logs := strings.NewReader(applySuccessOneResourceAdded)
   584  	applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
   585  	b.client.Applies = applyMock
   586  
   587  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   588  	defer configCleanup()
   589  	defer done(t)
   590  
   591  	input := testInput(t, map[string]string{
   592  		"approve": "no",
   593  	})
   594  
   595  	op.AutoApprove = true
   596  	op.UIIn = input
   597  	op.UIOut = b.CLI
   598  	op.Workspace = testBackendSingleWorkspaceName
   599  
   600  	run, err := b.Operation(context.Background(), op)
   601  	if err != nil {
   602  		t.Fatalf("error starting operation: %v", err)
   603  	}
   604  
   605  	<-run.Done()
   606  	if run.Result != backend.OperationSuccess {
   607  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   608  	}
   609  	if run.PlanEmpty {
   610  		t.Fatalf("expected a non-empty plan")
   611  	}
   612  
   613  	if len(input.answers) != 1 {
   614  		t.Fatalf("expected an unused answer, got: %v", input.answers)
   615  	}
   616  
   617  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   618  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
   619  		t.Fatalf("expected TFC header in output: %s", output)
   620  	}
   621  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   622  		t.Fatalf("expected plan summery in output: %s", output)
   623  	}
   624  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   625  		t.Fatalf("expected apply summery in output: %s", output)
   626  	}
   627  }
   628  
   629  func TestCloud_applyApprovedExternally(t *testing.T) {
   630  	b, bCleanup := testBackendWithName(t)
   631  	defer bCleanup()
   632  
   633  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   634  	defer configCleanup()
   635  	defer done(t)
   636  
   637  	input := testInput(t, map[string]string{
   638  		"approve": "wait-for-external-update",
   639  	})
   640  
   641  	op.UIIn = input
   642  	op.UIOut = b.CLI
   643  	op.Workspace = testBackendSingleWorkspaceName
   644  
   645  	ctx := context.Background()
   646  
   647  	run, err := b.Operation(ctx, op)
   648  	if err != nil {
   649  		t.Fatalf("error starting operation: %v", err)
   650  	}
   651  
   652  	// Wait 50 milliseconds to make sure the run started.
   653  	time.Sleep(50 * time.Millisecond)
   654  
   655  	wl, err := b.client.Workspaces.List(
   656  		ctx,
   657  		b.organization,
   658  		nil,
   659  	)
   660  	if err != nil {
   661  		t.Fatalf("unexpected error listing workspaces: %v", err)
   662  	}
   663  	if len(wl.Items) != 1 {
   664  		t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items))
   665  	}
   666  
   667  	rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, nil)
   668  	if err != nil {
   669  		t.Fatalf("unexpected error listing runs: %v", err)
   670  	}
   671  	if len(rl.Items) != 1 {
   672  		t.Fatalf("expected 1 run, got %d runs", len(rl.Items))
   673  	}
   674  
   675  	err = b.client.Runs.Apply(context.Background(), rl.Items[0].ID, tfe.RunApplyOptions{})
   676  	if err != nil {
   677  		t.Fatalf("unexpected error approving run: %v", err)
   678  	}
   679  
   680  	<-run.Done()
   681  	if run.Result != backend.OperationSuccess {
   682  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   683  	}
   684  	if run.PlanEmpty {
   685  		t.Fatalf("expected a non-empty plan")
   686  	}
   687  
   688  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   689  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
   690  		t.Fatalf("expected TFC header in output: %s", output)
   691  	}
   692  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   693  		t.Fatalf("expected plan summery in output: %s", output)
   694  	}
   695  	if !strings.Contains(output, "approved using the UI or API") {
   696  		t.Fatalf("expected external approval in output: %s", output)
   697  	}
   698  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   699  		t.Fatalf("expected apply summery in output: %s", output)
   700  	}
   701  }
   702  
   703  func TestCloud_applyDiscardedExternally(t *testing.T) {
   704  	b, bCleanup := testBackendWithName(t)
   705  	defer bCleanup()
   706  
   707  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   708  	defer configCleanup()
   709  	defer done(t)
   710  
   711  	input := testInput(t, map[string]string{
   712  		"approve": "wait-for-external-update",
   713  	})
   714  
   715  	op.UIIn = input
   716  	op.UIOut = b.CLI
   717  	op.Workspace = testBackendSingleWorkspaceName
   718  
   719  	ctx := context.Background()
   720  
   721  	run, err := b.Operation(ctx, op)
   722  	if err != nil {
   723  		t.Fatalf("error starting operation: %v", err)
   724  	}
   725  
   726  	// Wait 50 milliseconds to make sure the run started.
   727  	time.Sleep(50 * time.Millisecond)
   728  
   729  	wl, err := b.client.Workspaces.List(
   730  		ctx,
   731  		b.organization,
   732  		nil,
   733  	)
   734  	if err != nil {
   735  		t.Fatalf("unexpected error listing workspaces: %v", err)
   736  	}
   737  	if len(wl.Items) != 1 {
   738  		t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items))
   739  	}
   740  
   741  	rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, nil)
   742  	if err != nil {
   743  		t.Fatalf("unexpected error listing runs: %v", err)
   744  	}
   745  	if len(rl.Items) != 1 {
   746  		t.Fatalf("expected 1 run, got %d runs", len(rl.Items))
   747  	}
   748  
   749  	err = b.client.Runs.Discard(context.Background(), rl.Items[0].ID, tfe.RunDiscardOptions{})
   750  	if err != nil {
   751  		t.Fatalf("unexpected error discarding run: %v", err)
   752  	}
   753  
   754  	<-run.Done()
   755  	if run.Result == backend.OperationSuccess {
   756  		t.Fatal("expected apply operation to fail")
   757  	}
   758  	if !run.PlanEmpty {
   759  		t.Fatalf("expected plan to be empty")
   760  	}
   761  
   762  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   763  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
   764  		t.Fatalf("expected TFC header in output: %s", output)
   765  	}
   766  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   767  		t.Fatalf("expected plan summery in output: %s", output)
   768  	}
   769  	if !strings.Contains(output, "discarded using the UI or API") {
   770  		t.Fatalf("expected external discard output: %s", output)
   771  	}
   772  	if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   773  		t.Fatalf("unexpected apply summery in output: %s", output)
   774  	}
   775  }
   776  
   777  func TestCloud_applyWithAutoApprove(t *testing.T) {
   778  	b, bCleanup := testBackendWithTags(t)
   779  	defer bCleanup()
   780  	ctrl := gomock.NewController(t)
   781  
   782  	applyMock := mocks.NewMockApplies(ctrl)
   783  	// This needs three new lines because we check for a minimum of three lines
   784  	// in the parsing of logs in `opApply` function.
   785  	logs := strings.NewReader(applySuccessOneResourceAdded)
   786  	applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
   787  	b.client.Applies = applyMock
   788  
   789  	// Create a named workspace that auto applies.
   790  	_, err := b.client.Workspaces.Create(
   791  		context.Background(),
   792  		b.organization,
   793  		tfe.WorkspaceCreateOptions{
   794  			Name: tfe.String("prod"),
   795  		},
   796  	)
   797  	if err != nil {
   798  		t.Fatalf("error creating named workspace: %v", err)
   799  	}
   800  
   801  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   802  	defer configCleanup()
   803  	defer done(t)
   804  
   805  	input := testInput(t, map[string]string{
   806  		"approve": "yes",
   807  	})
   808  
   809  	op.UIIn = input
   810  	op.UIOut = b.CLI
   811  	op.Workspace = "prod"
   812  	op.AutoApprove = true
   813  
   814  	run, err := b.Operation(context.Background(), op)
   815  	if err != nil {
   816  		t.Fatalf("error starting operation: %v", err)
   817  	}
   818  
   819  	<-run.Done()
   820  	if run.Result != backend.OperationSuccess {
   821  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   822  	}
   823  	if run.PlanEmpty {
   824  		t.Fatalf("expected a non-empty plan")
   825  	}
   826  
   827  	if len(input.answers) != 1 {
   828  		t.Fatalf("expected an unused answer, got: %v", input.answers)
   829  	}
   830  
   831  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   832  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
   833  		t.Fatalf("expected TFC header in output: %s", output)
   834  	}
   835  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   836  		t.Fatalf("expected plan summery in output: %s", output)
   837  	}
   838  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   839  		t.Fatalf("expected apply summery in output: %s", output)
   840  	}
   841  }
   842  
   843  func TestCloud_applyForceLocal(t *testing.T) {
   844  	// Set TF_FORCE_LOCAL_BACKEND so the cloud backend will use
   845  	// the local backend with itself as embedded backend.
   846  	if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
   847  		t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
   848  	}
   849  	defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
   850  
   851  	b, bCleanup := testBackendWithName(t)
   852  	defer bCleanup()
   853  
   854  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   855  	defer configCleanup()
   856  	defer done(t)
   857  
   858  	input := testInput(t, map[string]string{
   859  		"approve": "yes",
   860  	})
   861  
   862  	op.UIIn = input
   863  	op.UIOut = b.CLI
   864  	op.Workspace = testBackendSingleWorkspaceName
   865  
   866  	streams, done := terminal.StreamsForTesting(t)
   867  	view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
   868  	op.View = view
   869  
   870  	run, err := b.Operation(context.Background(), op)
   871  	if err != nil {
   872  		t.Fatalf("error starting operation: %v", err)
   873  	}
   874  
   875  	<-run.Done()
   876  	if run.Result != backend.OperationSuccess {
   877  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   878  	}
   879  	if run.PlanEmpty {
   880  		t.Fatalf("expected a non-empty plan")
   881  	}
   882  
   883  	if len(input.answers) > 0 {
   884  		t.Fatalf("expected no unused answers, got: %v", input.answers)
   885  	}
   886  
   887  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   888  	if strings.Contains(output, "Running apply in Terraform Cloud") {
   889  		t.Fatalf("unexpected TFC header in output: %s", output)
   890  	}
   891  	if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   892  		t.Fatalf("expected plan summary in output: %s", output)
   893  	}
   894  	if !run.State.HasManagedResourceInstanceObjects() {
   895  		t.Fatalf("expected resources in state")
   896  	}
   897  }
   898  
   899  func TestCloud_applyWorkspaceWithoutOperations(t *testing.T) {
   900  	b, bCleanup := testBackendWithTags(t)
   901  	defer bCleanup()
   902  
   903  	ctx := context.Background()
   904  
   905  	// Create a named workspace that doesn't allow operations.
   906  	_, err := b.client.Workspaces.Create(
   907  		ctx,
   908  		b.organization,
   909  		tfe.WorkspaceCreateOptions{
   910  			Name: tfe.String("no-operations"),
   911  		},
   912  	)
   913  	if err != nil {
   914  		t.Fatalf("error creating named workspace: %v", err)
   915  	}
   916  
   917  	op, configCleanup, done := testOperationApply(t, "./testdata/apply")
   918  	defer configCleanup()
   919  	defer done(t)
   920  
   921  	input := testInput(t, map[string]string{
   922  		"approve": "yes",
   923  	})
   924  
   925  	op.UIIn = input
   926  	op.UIOut = b.CLI
   927  	op.Workspace = "no-operations"
   928  
   929  	streams, done := terminal.StreamsForTesting(t)
   930  	view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
   931  	op.View = view
   932  
   933  	run, err := b.Operation(ctx, op)
   934  	if err != nil {
   935  		t.Fatalf("error starting operation: %v", err)
   936  	}
   937  
   938  	<-run.Done()
   939  	if run.Result != backend.OperationSuccess {
   940  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   941  	}
   942  	if run.PlanEmpty {
   943  		t.Fatalf("expected a non-empty plan")
   944  	}
   945  
   946  	if len(input.answers) > 0 {
   947  		t.Fatalf("expected no unused answers, got: %v", input.answers)
   948  	}
   949  
   950  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   951  	if strings.Contains(output, "Running apply in Terraform Cloud") {
   952  		t.Fatalf("unexpected TFC header in output: %s", output)
   953  	}
   954  	if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   955  		t.Fatalf("expected plan summary in output: %s", output)
   956  	}
   957  	if !run.State.HasManagedResourceInstanceObjects() {
   958  		t.Fatalf("expected resources in state")
   959  	}
   960  }
   961  
   962  func TestCloud_applyLockTimeout(t *testing.T) {
   963  	b, bCleanup := testBackendWithName(t)
   964  	defer bCleanup()
   965  
   966  	ctx := context.Background()
   967  
   968  	// Retrieve the workspace used to run this operation in.
   969  	w, err := b.client.Workspaces.Read(ctx, b.organization, b.WorkspaceMapping.Name)
   970  	if err != nil {
   971  		t.Fatalf("error retrieving workspace: %v", err)
   972  	}
   973  
   974  	// Create a new configuration version.
   975  	c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{})
   976  	if err != nil {
   977  		t.Fatalf("error creating configuration version: %v", err)
   978  	}
   979  
   980  	// Create a pending run to block this run.
   981  	_, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{
   982  		ConfigurationVersion: c,
   983  		Workspace:            w,
   984  	})
   985  	if err != nil {
   986  		t.Fatalf("error creating pending run: %v", err)
   987  	}
   988  
   989  	op, configCleanup, done := testOperationApplyWithTimeout(t, "./testdata/apply", 50*time.Millisecond)
   990  	defer configCleanup()
   991  	defer done(t)
   992  
   993  	input := testInput(t, map[string]string{
   994  		"cancel":  "yes",
   995  		"approve": "yes",
   996  	})
   997  
   998  	op.UIIn = input
   999  	op.UIOut = b.CLI
  1000  	op.Workspace = testBackendSingleWorkspaceName
  1001  
  1002  	_, err = b.Operation(context.Background(), op)
  1003  	if err != nil {
  1004  		t.Fatalf("error starting operation: %v", err)
  1005  	}
  1006  
  1007  	sigint := make(chan os.Signal, 1)
  1008  	signal.Notify(sigint, syscall.SIGINT)
  1009  	select {
  1010  	case <-sigint:
  1011  		// Stop redirecting SIGINT signals.
  1012  		signal.Stop(sigint)
  1013  	case <-time.After(200 * time.Millisecond):
  1014  		t.Fatalf("expected lock timeout after 50 milliseconds, waited 200 milliseconds")
  1015  	}
  1016  
  1017  	if len(input.answers) != 2 {
  1018  		t.Fatalf("expected unused answers, got: %v", input.answers)
  1019  	}
  1020  
  1021  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1022  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
  1023  		t.Fatalf("expected TFC header in output: %s", output)
  1024  	}
  1025  	if !strings.Contains(output, "Lock timeout exceeded") {
  1026  		t.Fatalf("expected lock timout error in output: %s", output)
  1027  	}
  1028  	if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1029  		t.Fatalf("unexpected plan summery in output: %s", output)
  1030  	}
  1031  	if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
  1032  		t.Fatalf("unexpected apply summery in output: %s", output)
  1033  	}
  1034  }
  1035  
  1036  func TestCloud_applyDestroy(t *testing.T) {
  1037  	b, bCleanup := testBackendWithName(t)
  1038  	defer bCleanup()
  1039  
  1040  	op, configCleanup, done := testOperationApply(t, "./testdata/apply-destroy")
  1041  	defer configCleanup()
  1042  	defer done(t)
  1043  
  1044  	input := testInput(t, map[string]string{
  1045  		"approve": "yes",
  1046  	})
  1047  
  1048  	op.PlanMode = plans.DestroyMode
  1049  	op.UIIn = input
  1050  	op.UIOut = b.CLI
  1051  	op.Workspace = testBackendSingleWorkspaceName
  1052  
  1053  	run, err := b.Operation(context.Background(), op)
  1054  	if err != nil {
  1055  		t.Fatalf("error starting operation: %v", err)
  1056  	}
  1057  
  1058  	<-run.Done()
  1059  	if run.Result != backend.OperationSuccess {
  1060  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
  1061  	}
  1062  	if run.PlanEmpty {
  1063  		t.Fatalf("expected a non-empty plan")
  1064  	}
  1065  
  1066  	if len(input.answers) > 0 {
  1067  		t.Fatalf("expected no unused answers, got: %v", input.answers)
  1068  	}
  1069  
  1070  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1071  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
  1072  		t.Fatalf("expected TFC header in output: %s", output)
  1073  	}
  1074  	if !strings.Contains(output, "0 to add, 0 to change, 1 to destroy") {
  1075  		t.Fatalf("expected plan summery in output: %s", output)
  1076  	}
  1077  	if !strings.Contains(output, "0 added, 0 changed, 1 destroyed") {
  1078  		t.Fatalf("expected apply summery in output: %s", output)
  1079  	}
  1080  }
  1081  
  1082  func TestCloud_applyDestroyNoConfig(t *testing.T) {
  1083  	b, bCleanup := testBackendWithName(t)
  1084  	defer bCleanup()
  1085  
  1086  	input := testInput(t, map[string]string{
  1087  		"approve": "yes",
  1088  	})
  1089  
  1090  	op, configCleanup, done := testOperationApply(t, "./testdata/empty")
  1091  	defer configCleanup()
  1092  	defer done(t)
  1093  
  1094  	op.PlanMode = plans.DestroyMode
  1095  	op.UIIn = input
  1096  	op.UIOut = b.CLI
  1097  	op.Workspace = testBackendSingleWorkspaceName
  1098  
  1099  	run, err := b.Operation(context.Background(), op)
  1100  	if err != nil {
  1101  		t.Fatalf("error starting operation: %v", err)
  1102  	}
  1103  
  1104  	<-run.Done()
  1105  	if run.Result != backend.OperationSuccess {
  1106  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
  1107  	}
  1108  	if run.PlanEmpty {
  1109  		t.Fatalf("expected a non-empty plan")
  1110  	}
  1111  
  1112  	if len(input.answers) > 0 {
  1113  		t.Fatalf("expected no unused answers, got: %v", input.answers)
  1114  	}
  1115  }
  1116  
  1117  func TestCloud_applyPolicyPass(t *testing.T) {
  1118  	b, bCleanup := testBackendWithName(t)
  1119  	defer bCleanup()
  1120  
  1121  	op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-passed")
  1122  	defer configCleanup()
  1123  	defer done(t)
  1124  
  1125  	input := testInput(t, map[string]string{
  1126  		"approve": "yes",
  1127  	})
  1128  
  1129  	op.UIIn = input
  1130  	op.UIOut = b.CLI
  1131  	op.Workspace = testBackendSingleWorkspaceName
  1132  
  1133  	run, err := b.Operation(context.Background(), op)
  1134  	if err != nil {
  1135  		t.Fatalf("error starting operation: %v", err)
  1136  	}
  1137  
  1138  	<-run.Done()
  1139  	if run.Result != backend.OperationSuccess {
  1140  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
  1141  	}
  1142  	if run.PlanEmpty {
  1143  		t.Fatalf("expected a non-empty plan")
  1144  	}
  1145  
  1146  	if len(input.answers) > 0 {
  1147  		t.Fatalf("expected no unused answers, got: %v", input.answers)
  1148  	}
  1149  
  1150  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1151  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
  1152  		t.Fatalf("expected TFC header in output: %s", output)
  1153  	}
  1154  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1155  		t.Fatalf("expected plan summery in output: %s", output)
  1156  	}
  1157  	if !strings.Contains(output, "Sentinel Result: true") {
  1158  		t.Fatalf("expected policy check result in output: %s", output)
  1159  	}
  1160  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
  1161  		t.Fatalf("expected apply summery in output: %s", output)
  1162  	}
  1163  }
  1164  
  1165  func TestCloud_applyPolicyHardFail(t *testing.T) {
  1166  	b, bCleanup := testBackendWithName(t)
  1167  	defer bCleanup()
  1168  
  1169  	op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-hard-failed")
  1170  	defer configCleanup()
  1171  
  1172  	input := testInput(t, map[string]string{
  1173  		"approve": "yes",
  1174  	})
  1175  
  1176  	op.UIIn = input
  1177  	op.UIOut = b.CLI
  1178  	op.Workspace = testBackendSingleWorkspaceName
  1179  
  1180  	run, err := b.Operation(context.Background(), op)
  1181  	if err != nil {
  1182  		t.Fatalf("error starting operation: %v", err)
  1183  	}
  1184  
  1185  	<-run.Done()
  1186  	viewOutput := done(t)
  1187  	if run.Result == backend.OperationSuccess {
  1188  		t.Fatal("expected apply operation to fail")
  1189  	}
  1190  	if !run.PlanEmpty {
  1191  		t.Fatalf("expected plan to be empty")
  1192  	}
  1193  
  1194  	if len(input.answers) != 1 {
  1195  		t.Fatalf("expected an unused answers, got: %v", input.answers)
  1196  	}
  1197  
  1198  	errOutput := viewOutput.Stderr()
  1199  	if !strings.Contains(errOutput, "hard failed") {
  1200  		t.Fatalf("expected a policy check error, got: %v", errOutput)
  1201  	}
  1202  
  1203  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1204  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
  1205  		t.Fatalf("expected TFC header in output: %s", output)
  1206  	}
  1207  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1208  		t.Fatalf("expected plan summery in output: %s", output)
  1209  	}
  1210  	if !strings.Contains(output, "Sentinel Result: false") {
  1211  		t.Fatalf("expected policy check result in output: %s", output)
  1212  	}
  1213  	if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
  1214  		t.Fatalf("unexpected apply summery in output: %s", output)
  1215  	}
  1216  }
  1217  
  1218  func TestCloud_applyPolicySoftFail(t *testing.T) {
  1219  	b, bCleanup := testBackendWithName(t)
  1220  	defer bCleanup()
  1221  
  1222  	op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
  1223  	defer configCleanup()
  1224  	defer done(t)
  1225  
  1226  	input := testInput(t, map[string]string{
  1227  		"override": "override",
  1228  		"approve":  "yes",
  1229  	})
  1230  
  1231  	op.AutoApprove = false
  1232  	op.UIIn = input
  1233  	op.UIOut = b.CLI
  1234  	op.Workspace = testBackendSingleWorkspaceName
  1235  
  1236  	run, err := b.Operation(context.Background(), op)
  1237  	if err != nil {
  1238  		t.Fatalf("error starting operation: %v", err)
  1239  	}
  1240  
  1241  	<-run.Done()
  1242  	if run.Result != backend.OperationSuccess {
  1243  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
  1244  	}
  1245  	if run.PlanEmpty {
  1246  		t.Fatalf("expected a non-empty plan")
  1247  	}
  1248  
  1249  	if len(input.answers) > 0 {
  1250  		t.Fatalf("expected no unused answers, got: %v", input.answers)
  1251  	}
  1252  
  1253  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1254  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
  1255  		t.Fatalf("expected TFC header in output: %s", output)
  1256  	}
  1257  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1258  		t.Fatalf("expected plan summery in output: %s", output)
  1259  	}
  1260  	if !strings.Contains(output, "Sentinel Result: false") {
  1261  		t.Fatalf("expected policy check result in output: %s", output)
  1262  	}
  1263  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
  1264  		t.Fatalf("expected apply summery in output: %s", output)
  1265  	}
  1266  }
  1267  
  1268  func TestCloud_applyPolicySoftFailAutoApproveSuccess(t *testing.T) {
  1269  	b, bCleanup := testBackendWithName(t)
  1270  	defer bCleanup()
  1271  	ctrl := gomock.NewController(t)
  1272  
  1273  	policyCheckMock := mocks.NewMockPolicyChecks(ctrl)
  1274  	// This needs three new lines because we check for a minimum of three lines
  1275  	// in the parsing of logs in `opApply` function.
  1276  	logs := strings.NewReader(fmt.Sprintf("%s\n%s", sentinelSoftFail, applySuccessOneResourceAdded))
  1277  
  1278  	pc := &tfe.PolicyCheck{
  1279  		ID: "pc-1",
  1280  		Actions: &tfe.PolicyActions{
  1281  			IsOverridable: true,
  1282  		},
  1283  		Permissions: &tfe.PolicyPermissions{
  1284  			CanOverride: true,
  1285  		},
  1286  		Scope:  tfe.PolicyScopeOrganization,
  1287  		Status: tfe.PolicySoftFailed,
  1288  	}
  1289  	policyCheckMock.EXPECT().Read(gomock.Any(), gomock.Any()).Return(pc, nil)
  1290  	policyCheckMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
  1291  	policyCheckMock.EXPECT().Override(gomock.Any(), gomock.Any()).Return(nil, nil)
  1292  	b.client.PolicyChecks = policyCheckMock
  1293  	applyMock := mocks.NewMockApplies(ctrl)
  1294  	// This needs three new lines because we check for a minimum of three lines
  1295  	// in the parsing of logs in `opApply` function.
  1296  	logs = strings.NewReader("\n\n\n1 added, 0 changed, 0 destroyed")
  1297  	applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
  1298  	b.client.Applies = applyMock
  1299  
  1300  	op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
  1301  	defer configCleanup()
  1302  
  1303  	input := testInput(t, map[string]string{})
  1304  
  1305  	op.AutoApprove = true
  1306  	op.UIIn = input
  1307  	op.UIOut = b.CLI
  1308  	op.Workspace = testBackendSingleWorkspaceName
  1309  
  1310  	run, err := b.Operation(context.Background(), op)
  1311  	if err != nil {
  1312  		t.Fatalf("error starting operation: %v", err)
  1313  	}
  1314  
  1315  	<-run.Done()
  1316  	viewOutput := done(t)
  1317  	if run.Result != backend.OperationSuccess {
  1318  		t.Fatal("expected apply operation to success due to auto-approve")
  1319  	}
  1320  
  1321  	if run.PlanEmpty {
  1322  		t.Fatalf("expected plan to not be empty, plan opertion completed without error")
  1323  	}
  1324  
  1325  	if len(input.answers) != 0 {
  1326  		t.Fatalf("expected no answers, got: %v", input.answers)
  1327  	}
  1328  
  1329  	errOutput := viewOutput.Stderr()
  1330  	if strings.Contains(errOutput, "soft failed") {
  1331  		t.Fatalf("expected no policy check errors, instead got: %v", errOutput)
  1332  	}
  1333  
  1334  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1335  	if !strings.Contains(output, "Sentinel Result: false") {
  1336  		t.Fatalf("expected policy check to be false, insead got: %s", output)
  1337  	}
  1338  	if !strings.Contains(output, "Apply complete!") {
  1339  		t.Fatalf("expected apply to be complete, instead got: %s", output)
  1340  	}
  1341  
  1342  	if !strings.Contains(output, "Resources: 1 added, 0 changed, 0 destroyed") {
  1343  		t.Fatalf("expected resources, instead got: %s", output)
  1344  	}
  1345  }
  1346  
  1347  func TestCloud_applyPolicySoftFailAutoApprove(t *testing.T) {
  1348  	b, bCleanup := testBackendWithName(t)
  1349  	defer bCleanup()
  1350  	ctrl := gomock.NewController(t)
  1351  
  1352  	applyMock := mocks.NewMockApplies(ctrl)
  1353  	// This needs three new lines because we check for a minimum of three lines
  1354  	// in the parsing of logs in `opApply` function.
  1355  	logs := strings.NewReader(applySuccessOneResourceAdded)
  1356  	applyMock.EXPECT().Logs(gomock.Any(), gomock.Any()).Return(logs, nil)
  1357  	b.client.Applies = applyMock
  1358  
  1359  	// Create a named workspace that auto applies.
  1360  	_, err := b.client.Workspaces.Create(
  1361  		context.Background(),
  1362  		b.organization,
  1363  		tfe.WorkspaceCreateOptions{
  1364  			Name: tfe.String("prod"),
  1365  		},
  1366  	)
  1367  	if err != nil {
  1368  		t.Fatalf("error creating named workspace: %v", err)
  1369  	}
  1370  
  1371  	op, configCleanup, done := testOperationApply(t, "./testdata/apply-policy-soft-failed")
  1372  	defer configCleanup()
  1373  	defer done(t)
  1374  
  1375  	input := testInput(t, map[string]string{
  1376  		"override": "override",
  1377  		"approve":  "yes",
  1378  	})
  1379  
  1380  	op.UIIn = input
  1381  	op.UIOut = b.CLI
  1382  	op.Workspace = "prod"
  1383  	op.AutoApprove = true
  1384  
  1385  	run, err := b.Operation(context.Background(), op)
  1386  	if err != nil {
  1387  		t.Fatalf("error starting operation: %v", err)
  1388  	}
  1389  
  1390  	<-run.Done()
  1391  	if run.Result != backend.OperationSuccess {
  1392  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
  1393  	}
  1394  	if run.PlanEmpty {
  1395  		t.Fatalf("expected a non-empty plan")
  1396  	}
  1397  
  1398  	if len(input.answers) != 2 {
  1399  		t.Fatalf("expected an unused answer, got: %v", input.answers)
  1400  	}
  1401  
  1402  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1403  	if !strings.Contains(output, "Running apply in Terraform Cloud") {
  1404  		t.Fatalf("expected TFC header in output: %s", output)
  1405  	}
  1406  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1407  		t.Fatalf("expected plan summery in output: %s", output)
  1408  	}
  1409  	if !strings.Contains(output, "Sentinel Result: false") {
  1410  		t.Fatalf("expected policy check result in output: %s", output)
  1411  	}
  1412  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
  1413  		t.Fatalf("expected apply summery in output: %s", output)
  1414  	}
  1415  }
  1416  
  1417  func TestCloud_applyWithRemoteError(t *testing.T) {
  1418  	b, bCleanup := testBackendWithName(t)
  1419  	defer bCleanup()
  1420  
  1421  	op, configCleanup, done := testOperationApply(t, "./testdata/apply-with-error")
  1422  	defer configCleanup()
  1423  	defer done(t)
  1424  
  1425  	op.Workspace = testBackendSingleWorkspaceName
  1426  
  1427  	run, err := b.Operation(context.Background(), op)
  1428  	if err != nil {
  1429  		t.Fatalf("error starting operation: %v", err)
  1430  	}
  1431  
  1432  	<-run.Done()
  1433  	if run.Result == backend.OperationSuccess {
  1434  		t.Fatal("expected apply operation to fail")
  1435  	}
  1436  	if run.Result.ExitStatus() != 1 {
  1437  		t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus())
  1438  	}
  1439  
  1440  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1441  	if !strings.Contains(output, "null_resource.foo: 1 error") {
  1442  		t.Fatalf("expected apply error in output: %s", output)
  1443  	}
  1444  }
  1445  
  1446  func TestCloud_applyVersionCheck(t *testing.T) {
  1447  	testCases := map[string]struct {
  1448  		localVersion  string
  1449  		remoteVersion string
  1450  		forceLocal    bool
  1451  		executionMode string
  1452  		wantErr       string
  1453  	}{
  1454  		"versions can be different for remote apply": {
  1455  			localVersion:  "0.14.0",
  1456  			remoteVersion: "0.13.5",
  1457  			executionMode: "remote",
  1458  		},
  1459  		"versions can be different for local apply": {
  1460  			localVersion:  "0.14.0",
  1461  			remoteVersion: "0.13.5",
  1462  			executionMode: "local",
  1463  		},
  1464  		"force local with remote operations and different versions is acceptable": {
  1465  			localVersion:  "0.14.0",
  1466  			remoteVersion: "0.14.0-acme-provider-bundle",
  1467  			forceLocal:    true,
  1468  			executionMode: "remote",
  1469  		},
  1470  		"no error if versions are identical": {
  1471  			localVersion:  "0.14.0",
  1472  			remoteVersion: "0.14.0",
  1473  			forceLocal:    true,
  1474  			executionMode: "remote",
  1475  		},
  1476  		"no error if force local but workspace has remote operations disabled": {
  1477  			localVersion:  "0.14.0",
  1478  			remoteVersion: "0.13.5",
  1479  			forceLocal:    true,
  1480  			executionMode: "local",
  1481  		},
  1482  	}
  1483  
  1484  	for name, tc := range testCases {
  1485  		t.Run(name, func(t *testing.T) {
  1486  			b, bCleanup := testBackendWithName(t)
  1487  			defer bCleanup()
  1488  
  1489  			// SETUP: Save original local version state and restore afterwards
  1490  			p := tfversion.Prerelease
  1491  			v := tfversion.Version
  1492  			s := tfversion.SemVer
  1493  			defer func() {
  1494  				tfversion.Prerelease = p
  1495  				tfversion.Version = v
  1496  				tfversion.SemVer = s
  1497  			}()
  1498  
  1499  			// SETUP: Set local version for the test case
  1500  			tfversion.Prerelease = ""
  1501  			tfversion.Version = tc.localVersion
  1502  			tfversion.SemVer = version.Must(version.NewSemver(tc.localVersion))
  1503  
  1504  			// SETUP: Set force local for the test case
  1505  			b.forceLocal = tc.forceLocal
  1506  
  1507  			ctx := context.Background()
  1508  
  1509  			// SETUP: set the operations and Terraform Version fields on the
  1510  			// remote workspace
  1511  			_, err := b.client.Workspaces.Update(
  1512  				ctx,
  1513  				b.organization,
  1514  				b.WorkspaceMapping.Name,
  1515  				tfe.WorkspaceUpdateOptions{
  1516  					ExecutionMode:    tfe.String(tc.executionMode),
  1517  					TerraformVersion: tfe.String(tc.remoteVersion),
  1518  				},
  1519  			)
  1520  			if err != nil {
  1521  				t.Fatalf("error creating named workspace: %v", err)
  1522  			}
  1523  
  1524  			// RUN: prepare the apply operation and run it
  1525  			op, configCleanup, opDone := testOperationApply(t, "./testdata/apply")
  1526  			defer configCleanup()
  1527  			defer opDone(t)
  1528  
  1529  			streams, done := terminal.StreamsForTesting(t)
  1530  			view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams))
  1531  			op.View = view
  1532  
  1533  			input := testInput(t, map[string]string{
  1534  				"approve": "yes",
  1535  			})
  1536  
  1537  			op.UIIn = input
  1538  			op.UIOut = b.CLI
  1539  			op.Workspace = testBackendSingleWorkspaceName
  1540  
  1541  			run, err := b.Operation(ctx, op)
  1542  			if err != nil {
  1543  				t.Fatalf("error starting operation: %v", err)
  1544  			}
  1545  
  1546  			// RUN: wait for completion
  1547  			<-run.Done()
  1548  			output := done(t)
  1549  
  1550  			if tc.wantErr != "" {
  1551  				// ASSERT: if the test case wants an error, check for failure
  1552  				// and the error message
  1553  				if run.Result != backend.OperationFailure {
  1554  					t.Fatalf("expected run to fail, but result was %#v", run.Result)
  1555  				}
  1556  				errOutput := output.Stderr()
  1557  				if !strings.Contains(errOutput, tc.wantErr) {
  1558  					t.Fatalf("missing error %q\noutput: %s", tc.wantErr, errOutput)
  1559  				}
  1560  			} else {
  1561  				// ASSERT: otherwise, check for success and appropriate output
  1562  				// based on whether the run should be local or remote
  1563  				if run.Result != backend.OperationSuccess {
  1564  					t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
  1565  				}
  1566  				output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1567  				hasRemote := strings.Contains(output, "Running apply in Terraform Cloud")
  1568  				hasSummary := strings.Contains(output, "1 added, 0 changed, 0 destroyed")
  1569  				hasResources := run.State.HasManagedResourceInstanceObjects()
  1570  				if !tc.forceLocal && !isLocalExecutionMode(tc.executionMode) {
  1571  					if !hasRemote {
  1572  						t.Errorf("missing TFC header in output: %s", output)
  1573  					}
  1574  					if !hasSummary {
  1575  						t.Errorf("expected apply summary in output: %s", output)
  1576  					}
  1577  				} else {
  1578  					if hasRemote {
  1579  						t.Errorf("unexpected TFC header in output: %s", output)
  1580  					}
  1581  					if !hasResources {
  1582  						t.Errorf("expected resources in state")
  1583  					}
  1584  				}
  1585  			}
  1586  		})
  1587  	}
  1588  }
  1589  
  1590  const applySuccessOneResourceAdded = `
  1591  Terraform v0.11.10
  1592  
  1593  Initializing plugins and modules...
  1594  null_resource.hello: Creating...
  1595  null_resource.hello: Creation complete after 0s (ID: 8657651096157629581)
  1596  
  1597  Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
  1598  `
  1599  
  1600  const sentinelSoftFail = `
  1601  Sentinel Result: false
  1602  
  1603  Sentinel evaluated to false because one or more Sentinel policies evaluated
  1604  to false. This false was not due to an undefined value or runtime error.
  1605  
  1606  1 policies evaluated.
  1607  
  1608  ## Policy 1: Passthrough.sentinel (soft-mandatory)
  1609  
  1610  Result: false
  1611  
  1612  FALSE - Passthrough.sentinel:1:1 - Rule "main"
  1613  `