kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/backend/remote/backend_apply_test.go (about)

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