github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/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  	"github.com/hashicorp/terraform/addrs"
    15  	"github.com/hashicorp/terraform/backend"
    16  	"github.com/hashicorp/terraform/internal/initwd"
    17  	"github.com/hashicorp/terraform/plans/planfile"
    18  	"github.com/hashicorp/terraform/terraform"
    19  	"github.com/mitchellh/cli"
    20  )
    21  
    22  func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func()) {
    23  	t.Helper()
    24  
    25  	_, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir)
    26  
    27  	return &backend.Operation{
    28  		ConfigDir:    configDir,
    29  		ConfigLoader: configLoader,
    30  		Parallelism:  defaultParallelism,
    31  		PlanRefresh:  true,
    32  		Type:         backend.OperationTypeApply,
    33  	}, configCleanup
    34  }
    35  
    36  func TestRemote_applyBasic(t *testing.T) {
    37  	b, bCleanup := testBackendDefault(t)
    38  	defer bCleanup()
    39  
    40  	op, configCleanup := testOperationApply(t, "./testdata/apply")
    41  	defer configCleanup()
    42  
    43  	input := testInput(t, map[string]string{
    44  		"approve": "yes",
    45  	})
    46  
    47  	op.UIIn = input
    48  	op.UIOut = b.CLI
    49  	op.Workspace = backend.DefaultStateName
    50  
    51  	run, err := b.Operation(context.Background(), op)
    52  	if err != nil {
    53  		t.Fatalf("error starting operation: %v", err)
    54  	}
    55  
    56  	<-run.Done()
    57  	if run.Result != backend.OperationSuccess {
    58  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
    59  	}
    60  	if run.PlanEmpty {
    61  		t.Fatalf("expected a non-empty plan")
    62  	}
    63  
    64  	if len(input.answers) > 0 {
    65  		t.Fatalf("expected no unused answers, got: %v", input.answers)
    66  	}
    67  
    68  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
    69  	if !strings.Contains(output, "Running apply in the remote backend") {
    70  		t.Fatalf("expected remote backend header in output: %s", output)
    71  	}
    72  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
    73  		t.Fatalf("expected plan summery in output: %s", output)
    74  	}
    75  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
    76  		t.Fatalf("expected apply summery in output: %s", output)
    77  	}
    78  }
    79  
    80  func TestRemote_applyCanceled(t *testing.T) {
    81  	b, bCleanup := testBackendDefault(t)
    82  	defer bCleanup()
    83  
    84  	op, configCleanup := testOperationApply(t, "./testdata/apply")
    85  	defer configCleanup()
    86  
    87  	op.Workspace = backend.DefaultStateName
    88  
    89  	run, err := b.Operation(context.Background(), op)
    90  	if err != nil {
    91  		t.Fatalf("error starting operation: %v", err)
    92  	}
    93  
    94  	// Stop the run to simulate a Ctrl-C.
    95  	run.Stop()
    96  
    97  	<-run.Done()
    98  	if run.Result == backend.OperationSuccess {
    99  		t.Fatal("expected apply operation to fail")
   100  	}
   101  }
   102  
   103  func TestRemote_applyWithoutPermissions(t *testing.T) {
   104  	b, bCleanup := testBackendNoDefault(t)
   105  	defer bCleanup()
   106  
   107  	// Create a named workspace without permissions.
   108  	w, err := b.client.Workspaces.Create(
   109  		context.Background(),
   110  		b.organization,
   111  		tfe.WorkspaceCreateOptions{
   112  			Name: tfe.String(b.prefix + "prod"),
   113  		},
   114  	)
   115  	if err != nil {
   116  		t.Fatalf("error creating named workspace: %v", err)
   117  	}
   118  	w.Permissions.CanQueueApply = false
   119  
   120  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   121  	defer configCleanup()
   122  
   123  	op.UIOut = b.CLI
   124  	op.Workspace = "prod"
   125  
   126  	run, err := b.Operation(context.Background(), op)
   127  	if err != nil {
   128  		t.Fatalf("error starting operation: %v", err)
   129  	}
   130  
   131  	<-run.Done()
   132  	if run.Result == backend.OperationSuccess {
   133  		t.Fatal("expected apply operation to fail")
   134  	}
   135  
   136  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
   137  	if !strings.Contains(errOutput, "Insufficient rights to apply changes") {
   138  		t.Fatalf("expected a permissions error, got: %v", errOutput)
   139  	}
   140  }
   141  
   142  func TestRemote_applyWithVCS(t *testing.T) {
   143  	b, bCleanup := testBackendNoDefault(t)
   144  	defer bCleanup()
   145  
   146  	// Create a named workspace with a VCS.
   147  	_, err := b.client.Workspaces.Create(
   148  		context.Background(),
   149  		b.organization,
   150  		tfe.WorkspaceCreateOptions{
   151  			Name:    tfe.String(b.prefix + "prod"),
   152  			VCSRepo: &tfe.VCSRepoOptions{},
   153  		},
   154  	)
   155  	if err != nil {
   156  		t.Fatalf("error creating named workspace: %v", err)
   157  	}
   158  
   159  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   160  	defer configCleanup()
   161  
   162  	op.Workspace = "prod"
   163  
   164  	run, err := b.Operation(context.Background(), op)
   165  	if err != nil {
   166  		t.Fatalf("error starting operation: %v", err)
   167  	}
   168  
   169  	<-run.Done()
   170  	if run.Result == backend.OperationSuccess {
   171  		t.Fatal("expected apply operation to fail")
   172  	}
   173  	if !run.PlanEmpty {
   174  		t.Fatalf("expected plan to be empty")
   175  	}
   176  
   177  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
   178  	if !strings.Contains(errOutput, "not allowed for workspaces with a VCS") {
   179  		t.Fatalf("expected a VCS error, got: %v", errOutput)
   180  	}
   181  }
   182  
   183  func TestRemote_applyWithParallelism(t *testing.T) {
   184  	b, bCleanup := testBackendDefault(t)
   185  	defer bCleanup()
   186  
   187  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   188  	defer configCleanup()
   189  
   190  	op.Parallelism = 3
   191  	op.Workspace = backend.DefaultStateName
   192  
   193  	run, err := b.Operation(context.Background(), op)
   194  	if err != nil {
   195  		t.Fatalf("error starting operation: %v", err)
   196  	}
   197  
   198  	<-run.Done()
   199  	if run.Result == backend.OperationSuccess {
   200  		t.Fatal("expected apply operation to fail")
   201  	}
   202  
   203  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
   204  	if !strings.Contains(errOutput, "parallelism values are currently not supported") {
   205  		t.Fatalf("expected a parallelism error, got: %v", errOutput)
   206  	}
   207  }
   208  
   209  func TestRemote_applyWithPlan(t *testing.T) {
   210  	b, bCleanup := testBackendDefault(t)
   211  	defer bCleanup()
   212  
   213  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   214  	defer configCleanup()
   215  
   216  	op.PlanFile = &planfile.Reader{}
   217  	op.Workspace = backend.DefaultStateName
   218  
   219  	run, err := b.Operation(context.Background(), op)
   220  	if err != nil {
   221  		t.Fatalf("error starting operation: %v", err)
   222  	}
   223  
   224  	<-run.Done()
   225  	if run.Result == backend.OperationSuccess {
   226  		t.Fatal("expected apply operation to fail")
   227  	}
   228  	if !run.PlanEmpty {
   229  		t.Fatalf("expected plan to be empty")
   230  	}
   231  
   232  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
   233  	if !strings.Contains(errOutput, "saved plan is currently not supported") {
   234  		t.Fatalf("expected a saved plan error, got: %v", errOutput)
   235  	}
   236  }
   237  
   238  func TestRemote_applyWithoutRefresh(t *testing.T) {
   239  	b, bCleanup := testBackendDefault(t)
   240  	defer bCleanup()
   241  
   242  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   243  	defer configCleanup()
   244  
   245  	op.PlanRefresh = false
   246  	op.Workspace = backend.DefaultStateName
   247  
   248  	run, err := b.Operation(context.Background(), op)
   249  	if err != nil {
   250  		t.Fatalf("error starting operation: %v", err)
   251  	}
   252  
   253  	<-run.Done()
   254  	if run.Result == backend.OperationSuccess {
   255  		t.Fatal("expected apply operation to fail")
   256  	}
   257  
   258  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
   259  	if !strings.Contains(errOutput, "refresh is currently not supported") {
   260  		t.Fatalf("expected a refresh error, got: %v", errOutput)
   261  	}
   262  }
   263  
   264  func TestRemote_applyWithTarget(t *testing.T) {
   265  	b, bCleanup := testBackendDefault(t)
   266  	defer bCleanup()
   267  
   268  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   269  	defer configCleanup()
   270  
   271  	addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
   272  
   273  	op.Targets = []addrs.Targetable{addr}
   274  	op.Workspace = backend.DefaultStateName
   275  
   276  	run, err := b.Operation(context.Background(), op)
   277  	if err != nil {
   278  		t.Fatalf("error starting operation: %v", err)
   279  	}
   280  
   281  	<-run.Done()
   282  	if run.Result != backend.OperationSuccess {
   283  		t.Fatal("expected apply operation to succeed")
   284  	}
   285  	if run.PlanEmpty {
   286  		t.Fatalf("expected plan to be non-empty")
   287  	}
   288  
   289  	// We should find a run inside the mock client that has the same
   290  	// target address we requested above.
   291  	runsAPI := b.client.Runs.(*mockRuns)
   292  	if got, want := len(runsAPI.runs), 1; got != want {
   293  		t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want)
   294  	}
   295  	for _, run := range runsAPI.runs {
   296  		if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" {
   297  			t.Errorf("wrong TargetAddrs in the created run\n%s", diff)
   298  		}
   299  	}
   300  }
   301  
   302  func TestRemote_applyWithTargetIncompatibleAPIVersion(t *testing.T) {
   303  	b, bCleanup := testBackendDefault(t)
   304  	defer bCleanup()
   305  
   306  	op, configCleanup := testOperationPlan(t, "./testdata/plan")
   307  	defer configCleanup()
   308  
   309  	// Set the tfe client's RemoteAPIVersion to an empty string, to mimic
   310  	// API versions prior to 2.3.
   311  	b.client.SetFakeRemoteAPIVersion("")
   312  
   313  	addr, _ := addrs.ParseAbsResourceStr("null_resource.foo")
   314  
   315  	op.Targets = []addrs.Targetable{addr}
   316  	op.Workspace = backend.DefaultStateName
   317  
   318  	run, err := b.Operation(context.Background(), op)
   319  	if err != nil {
   320  		t.Fatalf("error starting operation: %v", err)
   321  	}
   322  
   323  	<-run.Done()
   324  	if run.Result == backend.OperationSuccess {
   325  		t.Fatal("expected apply operation to fail")
   326  	}
   327  	if !run.PlanEmpty {
   328  		t.Fatalf("expected plan to be empty")
   329  	}
   330  
   331  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
   332  	if !strings.Contains(errOutput, "Resource targeting is not supported") {
   333  		t.Fatalf("expected a targeting error, got: %v", errOutput)
   334  	}
   335  }
   336  
   337  func TestRemote_applyWithVariables(t *testing.T) {
   338  	b, bCleanup := testBackendDefault(t)
   339  	defer bCleanup()
   340  
   341  	op, configCleanup := testOperationApply(t, "./testdata/apply-variables")
   342  	defer configCleanup()
   343  
   344  	op.Variables = testVariables(terraform.ValueFromNamedFile, "foo", "bar")
   345  	op.Workspace = backend.DefaultStateName
   346  
   347  	run, err := b.Operation(context.Background(), op)
   348  	if err != nil {
   349  		t.Fatalf("error starting operation: %v", err)
   350  	}
   351  
   352  	<-run.Done()
   353  	if run.Result == backend.OperationSuccess {
   354  		t.Fatal("expected apply operation to fail")
   355  	}
   356  
   357  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
   358  	if !strings.Contains(errOutput, "variables are currently not supported") {
   359  		t.Fatalf("expected a variables error, got: %v", errOutput)
   360  	}
   361  }
   362  
   363  func TestRemote_applyNoConfig(t *testing.T) {
   364  	b, bCleanup := testBackendDefault(t)
   365  	defer bCleanup()
   366  
   367  	op, configCleanup := testOperationApply(t, "./testdata/empty")
   368  	defer configCleanup()
   369  
   370  	op.Workspace = backend.DefaultStateName
   371  
   372  	run, err := b.Operation(context.Background(), op)
   373  	if err != nil {
   374  		t.Fatalf("error starting operation: %v", err)
   375  	}
   376  
   377  	<-run.Done()
   378  	if run.Result == backend.OperationSuccess {
   379  		t.Fatal("expected apply operation to fail")
   380  	}
   381  	if !run.PlanEmpty {
   382  		t.Fatalf("expected plan to be empty")
   383  	}
   384  
   385  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
   386  	if !strings.Contains(errOutput, "configuration files found") {
   387  		t.Fatalf("expected configuration files error, got: %v", errOutput)
   388  	}
   389  }
   390  
   391  func TestRemote_applyNoChanges(t *testing.T) {
   392  	b, bCleanup := testBackendDefault(t)
   393  	defer bCleanup()
   394  
   395  	op, configCleanup := testOperationApply(t, "./testdata/apply-no-changes")
   396  	defer configCleanup()
   397  
   398  	op.Workspace = backend.DefaultStateName
   399  
   400  	run, err := b.Operation(context.Background(), op)
   401  	if err != nil {
   402  		t.Fatalf("error starting operation: %v", err)
   403  	}
   404  
   405  	<-run.Done()
   406  	if run.Result != backend.OperationSuccess {
   407  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   408  	}
   409  	if !run.PlanEmpty {
   410  		t.Fatalf("expected plan to be empty")
   411  	}
   412  
   413  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   414  	if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") {
   415  		t.Fatalf("expected no changes in plan summery: %s", output)
   416  	}
   417  	if !strings.Contains(output, "Sentinel Result: true") {
   418  		t.Fatalf("expected policy check result in output: %s", output)
   419  	}
   420  }
   421  
   422  func TestRemote_applyNoApprove(t *testing.T) {
   423  	b, bCleanup := testBackendDefault(t)
   424  	defer bCleanup()
   425  
   426  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   427  	defer configCleanup()
   428  
   429  	input := testInput(t, map[string]string{
   430  		"approve": "no",
   431  	})
   432  
   433  	op.UIIn = input
   434  	op.UIOut = b.CLI
   435  	op.Workspace = backend.DefaultStateName
   436  
   437  	run, err := b.Operation(context.Background(), op)
   438  	if err != nil {
   439  		t.Fatalf("error starting operation: %v", err)
   440  	}
   441  
   442  	<-run.Done()
   443  	if run.Result == backend.OperationSuccess {
   444  		t.Fatal("expected apply operation to fail")
   445  	}
   446  	if !run.PlanEmpty {
   447  		t.Fatalf("expected plan to be empty")
   448  	}
   449  
   450  	if len(input.answers) > 0 {
   451  		t.Fatalf("expected no unused answers, got: %v", input.answers)
   452  	}
   453  
   454  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
   455  	if !strings.Contains(errOutput, "Apply discarded") {
   456  		t.Fatalf("expected an apply discarded error, got: %v", errOutput)
   457  	}
   458  }
   459  
   460  func TestRemote_applyAutoApprove(t *testing.T) {
   461  	b, bCleanup := testBackendDefault(t)
   462  	defer bCleanup()
   463  
   464  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   465  	defer configCleanup()
   466  
   467  	input := testInput(t, map[string]string{
   468  		"approve": "no",
   469  	})
   470  
   471  	op.AutoApprove = true
   472  	op.UIIn = input
   473  	op.UIOut = b.CLI
   474  	op.Workspace = backend.DefaultStateName
   475  
   476  	run, err := b.Operation(context.Background(), op)
   477  	if err != nil {
   478  		t.Fatalf("error starting operation: %v", err)
   479  	}
   480  
   481  	<-run.Done()
   482  	if run.Result != backend.OperationSuccess {
   483  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   484  	}
   485  	if run.PlanEmpty {
   486  		t.Fatalf("expected a non-empty plan")
   487  	}
   488  
   489  	if len(input.answers) != 1 {
   490  		t.Fatalf("expected an unused answer, got: %v", input.answers)
   491  	}
   492  
   493  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   494  	if !strings.Contains(output, "Running apply in the remote backend") {
   495  		t.Fatalf("expected remote backend header in output: %s", output)
   496  	}
   497  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   498  		t.Fatalf("expected plan summery in output: %s", output)
   499  	}
   500  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   501  		t.Fatalf("expected apply summery in output: %s", output)
   502  	}
   503  }
   504  
   505  func TestRemote_applyApprovedExternally(t *testing.T) {
   506  	b, bCleanup := testBackendDefault(t)
   507  	defer bCleanup()
   508  
   509  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   510  	defer configCleanup()
   511  
   512  	input := testInput(t, map[string]string{
   513  		"approve": "wait-for-external-update",
   514  	})
   515  
   516  	op.UIIn = input
   517  	op.UIOut = b.CLI
   518  	op.Workspace = backend.DefaultStateName
   519  
   520  	ctx := context.Background()
   521  
   522  	run, err := b.Operation(ctx, op)
   523  	if err != nil {
   524  		t.Fatalf("error starting operation: %v", err)
   525  	}
   526  
   527  	// Wait 2 seconds to make sure the run started.
   528  	time.Sleep(2 * time.Second)
   529  
   530  	wl, err := b.client.Workspaces.List(
   531  		ctx,
   532  		b.organization,
   533  		tfe.WorkspaceListOptions{
   534  			ListOptions: tfe.ListOptions{PageNumber: 2, PageSize: 10},
   535  		},
   536  	)
   537  	if err != nil {
   538  		t.Fatalf("unexpected error listing workspaces: %v", err)
   539  	}
   540  	if len(wl.Items) != 1 {
   541  		t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items))
   542  	}
   543  
   544  	rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, tfe.RunListOptions{})
   545  	if err != nil {
   546  		t.Fatalf("unexpected error listing runs: %v", err)
   547  	}
   548  	if len(rl.Items) != 1 {
   549  		t.Fatalf("expected 1 run, got %d runs", len(rl.Items))
   550  	}
   551  
   552  	err = b.client.Runs.Apply(context.Background(), rl.Items[0].ID, tfe.RunApplyOptions{})
   553  	if err != nil {
   554  		t.Fatalf("unexpected error approving run: %v", err)
   555  	}
   556  
   557  	<-run.Done()
   558  	if run.Result != backend.OperationSuccess {
   559  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   560  	}
   561  	if run.PlanEmpty {
   562  		t.Fatalf("expected a non-empty plan")
   563  	}
   564  
   565  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   566  	if !strings.Contains(output, "Running apply in the remote backend") {
   567  		t.Fatalf("expected remote backend header in output: %s", output)
   568  	}
   569  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   570  		t.Fatalf("expected plan summery in output: %s", output)
   571  	}
   572  	if !strings.Contains(output, "approved using the UI or API") {
   573  		t.Fatalf("expected external approval in output: %s", output)
   574  	}
   575  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   576  		t.Fatalf("expected apply summery in output: %s", output)
   577  	}
   578  }
   579  
   580  func TestRemote_applyDiscardedExternally(t *testing.T) {
   581  	b, bCleanup := testBackendDefault(t)
   582  	defer bCleanup()
   583  
   584  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   585  	defer configCleanup()
   586  
   587  	input := testInput(t, map[string]string{
   588  		"approve": "wait-for-external-update",
   589  	})
   590  
   591  	op.UIIn = input
   592  	op.UIOut = b.CLI
   593  	op.Workspace = backend.DefaultStateName
   594  
   595  	ctx := context.Background()
   596  
   597  	run, err := b.Operation(ctx, op)
   598  	if err != nil {
   599  		t.Fatalf("error starting operation: %v", err)
   600  	}
   601  
   602  	// Wait 2 seconds to make sure the run started.
   603  	time.Sleep(2 * time.Second)
   604  
   605  	wl, err := b.client.Workspaces.List(
   606  		ctx,
   607  		b.organization,
   608  		tfe.WorkspaceListOptions{
   609  			ListOptions: tfe.ListOptions{PageNumber: 2, PageSize: 10},
   610  		},
   611  	)
   612  	if err != nil {
   613  		t.Fatalf("unexpected error listing workspaces: %v", err)
   614  	}
   615  	if len(wl.Items) != 1 {
   616  		t.Fatalf("expected 1 workspace, got %d workspaces", len(wl.Items))
   617  	}
   618  
   619  	rl, err := b.client.Runs.List(ctx, wl.Items[0].ID, tfe.RunListOptions{})
   620  	if err != nil {
   621  		t.Fatalf("unexpected error listing runs: %v", err)
   622  	}
   623  	if len(rl.Items) != 1 {
   624  		t.Fatalf("expected 1 run, got %d runs", len(rl.Items))
   625  	}
   626  
   627  	err = b.client.Runs.Discard(context.Background(), rl.Items[0].ID, tfe.RunDiscardOptions{})
   628  	if err != nil {
   629  		t.Fatalf("unexpected error discarding run: %v", err)
   630  	}
   631  
   632  	<-run.Done()
   633  	if run.Result == backend.OperationSuccess {
   634  		t.Fatal("expected apply operation to fail")
   635  	}
   636  	if !run.PlanEmpty {
   637  		t.Fatalf("expected plan to be empty")
   638  	}
   639  
   640  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   641  	if !strings.Contains(output, "Running apply in the remote backend") {
   642  		t.Fatalf("expected remote backend header in output: %s", output)
   643  	}
   644  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   645  		t.Fatalf("expected plan summery in output: %s", output)
   646  	}
   647  	if !strings.Contains(output, "discarded using the UI or API") {
   648  		t.Fatalf("expected external discard output: %s", output)
   649  	}
   650  	if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   651  		t.Fatalf("unexpected apply summery in output: %s", output)
   652  	}
   653  }
   654  
   655  func TestRemote_applyWithAutoApply(t *testing.T) {
   656  	b, bCleanup := testBackendNoDefault(t)
   657  	defer bCleanup()
   658  
   659  	// Create a named workspace that auto applies.
   660  	_, err := b.client.Workspaces.Create(
   661  		context.Background(),
   662  		b.organization,
   663  		tfe.WorkspaceCreateOptions{
   664  			AutoApply: tfe.Bool(true),
   665  			Name:      tfe.String(b.prefix + "prod"),
   666  		},
   667  	)
   668  	if err != nil {
   669  		t.Fatalf("error creating named workspace: %v", err)
   670  	}
   671  
   672  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   673  	defer configCleanup()
   674  
   675  	input := testInput(t, map[string]string{
   676  		"approve": "yes",
   677  	})
   678  
   679  	op.UIIn = input
   680  	op.UIOut = b.CLI
   681  	op.Workspace = "prod"
   682  
   683  	run, err := b.Operation(context.Background(), op)
   684  	if err != nil {
   685  		t.Fatalf("error starting operation: %v", err)
   686  	}
   687  
   688  	<-run.Done()
   689  	if run.Result != backend.OperationSuccess {
   690  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   691  	}
   692  	if run.PlanEmpty {
   693  		t.Fatalf("expected a non-empty plan")
   694  	}
   695  
   696  	if len(input.answers) != 1 {
   697  		t.Fatalf("expected an unused answer, got: %v", input.answers)
   698  	}
   699  
   700  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   701  	if !strings.Contains(output, "Running apply in the remote backend") {
   702  		t.Fatalf("expected remote backend header in output: %s", output)
   703  	}
   704  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   705  		t.Fatalf("expected plan summery in output: %s", output)
   706  	}
   707  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   708  		t.Fatalf("expected apply summery in output: %s", output)
   709  	}
   710  }
   711  
   712  func TestRemote_applyForceLocal(t *testing.T) {
   713  	// Set TF_FORCE_LOCAL_BACKEND so the remote backend will use
   714  	// the local backend with itself as embedded backend.
   715  	if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
   716  		t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
   717  	}
   718  	defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
   719  
   720  	b, bCleanup := testBackendDefault(t)
   721  	defer bCleanup()
   722  
   723  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   724  	defer configCleanup()
   725  
   726  	input := testInput(t, map[string]string{
   727  		"approve": "yes",
   728  	})
   729  
   730  	op.UIIn = input
   731  	op.UIOut = b.CLI
   732  	op.Workspace = backend.DefaultStateName
   733  
   734  	run, err := b.Operation(context.Background(), op)
   735  	if err != nil {
   736  		t.Fatalf("error starting operation: %v", err)
   737  	}
   738  
   739  	<-run.Done()
   740  	if run.Result != backend.OperationSuccess {
   741  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   742  	}
   743  	if run.PlanEmpty {
   744  		t.Fatalf("expected a non-empty plan")
   745  	}
   746  
   747  	if len(input.answers) > 0 {
   748  		t.Fatalf("expected no unused answers, got: %v", input.answers)
   749  	}
   750  
   751  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   752  	if strings.Contains(output, "Running apply in the remote backend") {
   753  		t.Fatalf("unexpected remote backend header in output: %s", output)
   754  	}
   755  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   756  		t.Fatalf("expected plan summery in output: %s", output)
   757  	}
   758  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   759  		t.Fatalf("expected apply summery in output: %s", output)
   760  	}
   761  }
   762  
   763  func TestRemote_applyWorkspaceWithoutOperations(t *testing.T) {
   764  	b, bCleanup := testBackendNoDefault(t)
   765  	defer bCleanup()
   766  
   767  	ctx := context.Background()
   768  
   769  	// Create a named workspace that doesn't allow operations.
   770  	_, err := b.client.Workspaces.Create(
   771  		ctx,
   772  		b.organization,
   773  		tfe.WorkspaceCreateOptions{
   774  			Name: tfe.String(b.prefix + "no-operations"),
   775  		},
   776  	)
   777  	if err != nil {
   778  		t.Fatalf("error creating named workspace: %v", err)
   779  	}
   780  
   781  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   782  	defer configCleanup()
   783  
   784  	input := testInput(t, map[string]string{
   785  		"approve": "yes",
   786  	})
   787  
   788  	op.UIIn = input
   789  	op.UIOut = b.CLI
   790  	op.Workspace = "no-operations"
   791  
   792  	run, err := b.Operation(ctx, op)
   793  	if err != nil {
   794  		t.Fatalf("error starting operation: %v", err)
   795  	}
   796  
   797  	<-run.Done()
   798  	if run.Result != backend.OperationSuccess {
   799  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   800  	}
   801  	if run.PlanEmpty {
   802  		t.Fatalf("expected a non-empty plan")
   803  	}
   804  
   805  	if len(input.answers) > 0 {
   806  		t.Fatalf("expected no unused answers, got: %v", input.answers)
   807  	}
   808  
   809  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   810  	if strings.Contains(output, "Running apply in the remote backend") {
   811  		t.Fatalf("unexpected remote backend header in output: %s", output)
   812  	}
   813  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   814  		t.Fatalf("expected plan summery in output: %s", output)
   815  	}
   816  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
   817  		t.Fatalf("expected apply summery in output: %s", output)
   818  	}
   819  }
   820  
   821  func TestRemote_applyLockTimeout(t *testing.T) {
   822  	b, bCleanup := testBackendDefault(t)
   823  	defer bCleanup()
   824  
   825  	ctx := context.Background()
   826  
   827  	// Retrieve the workspace used to run this operation in.
   828  	w, err := b.client.Workspaces.Read(ctx, b.organization, b.workspace)
   829  	if err != nil {
   830  		t.Fatalf("error retrieving workspace: %v", err)
   831  	}
   832  
   833  	// Create a new configuration version.
   834  	c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{})
   835  	if err != nil {
   836  		t.Fatalf("error creating configuration version: %v", err)
   837  	}
   838  
   839  	// Create a pending run to block this run.
   840  	_, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{
   841  		ConfigurationVersion: c,
   842  		Workspace:            w,
   843  	})
   844  	if err != nil {
   845  		t.Fatalf("error creating pending run: %v", err)
   846  	}
   847  
   848  	op, configCleanup := testOperationApply(t, "./testdata/apply")
   849  	defer configCleanup()
   850  
   851  	input := testInput(t, map[string]string{
   852  		"cancel":  "yes",
   853  		"approve": "yes",
   854  	})
   855  
   856  	op.StateLockTimeout = 5 * time.Second
   857  	op.UIIn = input
   858  	op.UIOut = b.CLI
   859  	op.Workspace = backend.DefaultStateName
   860  
   861  	_, err = b.Operation(context.Background(), op)
   862  	if err != nil {
   863  		t.Fatalf("error starting operation: %v", err)
   864  	}
   865  
   866  	sigint := make(chan os.Signal, 1)
   867  	signal.Notify(sigint, syscall.SIGINT)
   868  	select {
   869  	case <-sigint:
   870  		// Stop redirecting SIGINT signals.
   871  		signal.Stop(sigint)
   872  	case <-time.After(10 * time.Second):
   873  		t.Fatalf("expected lock timeout after 5 seconds, waited 10 seconds")
   874  	}
   875  
   876  	if len(input.answers) != 2 {
   877  		t.Fatalf("expected unused answers, got: %v", input.answers)
   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, "Lock timeout exceeded") {
   885  		t.Fatalf("expected lock timout error in output: %s", output)
   886  	}
   887  	if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   888  		t.Fatalf("unexpected plan summery in 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_applyDestroy(t *testing.T) {
   896  	b, bCleanup := testBackendDefault(t)
   897  	defer bCleanup()
   898  
   899  	op, configCleanup := testOperationApply(t, "./testdata/apply-destroy")
   900  	defer configCleanup()
   901  
   902  	input := testInput(t, map[string]string{
   903  		"approve": "yes",
   904  	})
   905  
   906  	op.Destroy = true
   907  	op.UIIn = input
   908  	op.UIOut = b.CLI
   909  	op.Workspace = backend.DefaultStateName
   910  
   911  	run, err := b.Operation(context.Background(), op)
   912  	if err != nil {
   913  		t.Fatalf("error starting operation: %v", err)
   914  	}
   915  
   916  	<-run.Done()
   917  	if run.Result != backend.OperationSuccess {
   918  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   919  	}
   920  	if run.PlanEmpty {
   921  		t.Fatalf("expected a non-empty plan")
   922  	}
   923  
   924  	if len(input.answers) > 0 {
   925  		t.Fatalf("expected no unused answers, got: %v", input.answers)
   926  	}
   927  
   928  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   929  	if !strings.Contains(output, "Running apply in the remote backend") {
   930  		t.Fatalf("expected remote backend header in output: %s", output)
   931  	}
   932  	if !strings.Contains(output, "0 to add, 0 to change, 1 to destroy") {
   933  		t.Fatalf("expected plan summery in output: %s", output)
   934  	}
   935  	if !strings.Contains(output, "0 added, 0 changed, 1 destroyed") {
   936  		t.Fatalf("expected apply summery in output: %s", output)
   937  	}
   938  }
   939  
   940  func TestRemote_applyDestroyNoConfig(t *testing.T) {
   941  	b, bCleanup := testBackendDefault(t)
   942  	defer bCleanup()
   943  
   944  	input := testInput(t, map[string]string{
   945  		"approve": "yes",
   946  	})
   947  
   948  	op, configCleanup := testOperationApply(t, "./testdata/empty")
   949  	defer configCleanup()
   950  
   951  	op.Destroy = true
   952  	op.UIIn = input
   953  	op.UIOut = b.CLI
   954  	op.Workspace = backend.DefaultStateName
   955  
   956  	run, err := b.Operation(context.Background(), op)
   957  	if err != nil {
   958  		t.Fatalf("error starting operation: %v", err)
   959  	}
   960  
   961  	<-run.Done()
   962  	if run.Result != backend.OperationSuccess {
   963  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   964  	}
   965  	if run.PlanEmpty {
   966  		t.Fatalf("expected a non-empty plan")
   967  	}
   968  
   969  	if len(input.answers) > 0 {
   970  		t.Fatalf("expected no unused answers, got: %v", input.answers)
   971  	}
   972  }
   973  
   974  func TestRemote_applyPolicyPass(t *testing.T) {
   975  	b, bCleanup := testBackendDefault(t)
   976  	defer bCleanup()
   977  
   978  	op, configCleanup := testOperationApply(t, "./testdata/apply-policy-passed")
   979  	defer configCleanup()
   980  
   981  	input := testInput(t, map[string]string{
   982  		"approve": "yes",
   983  	})
   984  
   985  	op.UIIn = input
   986  	op.UIOut = b.CLI
   987  	op.Workspace = backend.DefaultStateName
   988  
   989  	run, err := b.Operation(context.Background(), op)
   990  	if err != nil {
   991  		t.Fatalf("error starting operation: %v", err)
   992  	}
   993  
   994  	<-run.Done()
   995  	if run.Result != backend.OperationSuccess {
   996  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
   997  	}
   998  	if run.PlanEmpty {
   999  		t.Fatalf("expected a non-empty plan")
  1000  	}
  1001  
  1002  	if len(input.answers) > 0 {
  1003  		t.Fatalf("expected no unused answers, got: %v", input.answers)
  1004  	}
  1005  
  1006  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1007  	if !strings.Contains(output, "Running apply in the remote backend") {
  1008  		t.Fatalf("expected remote backend header in output: %s", output)
  1009  	}
  1010  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1011  		t.Fatalf("expected plan summery in output: %s", output)
  1012  	}
  1013  	if !strings.Contains(output, "Sentinel Result: true") {
  1014  		t.Fatalf("expected policy check result in output: %s", output)
  1015  	}
  1016  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
  1017  		t.Fatalf("expected apply summery in output: %s", output)
  1018  	}
  1019  }
  1020  
  1021  func TestRemote_applyPolicyHardFail(t *testing.T) {
  1022  	b, bCleanup := testBackendDefault(t)
  1023  	defer bCleanup()
  1024  
  1025  	op, configCleanup := testOperationApply(t, "./testdata/apply-policy-hard-failed")
  1026  	defer configCleanup()
  1027  
  1028  	input := testInput(t, map[string]string{
  1029  		"approve": "yes",
  1030  	})
  1031  
  1032  	op.UIIn = input
  1033  	op.UIOut = b.CLI
  1034  	op.Workspace = backend.DefaultStateName
  1035  
  1036  	run, err := b.Operation(context.Background(), op)
  1037  	if err != nil {
  1038  		t.Fatalf("error starting operation: %v", err)
  1039  	}
  1040  
  1041  	<-run.Done()
  1042  	if run.Result == backend.OperationSuccess {
  1043  		t.Fatal("expected apply operation to fail")
  1044  	}
  1045  	if !run.PlanEmpty {
  1046  		t.Fatalf("expected plan to be empty")
  1047  	}
  1048  
  1049  	if len(input.answers) != 1 {
  1050  		t.Fatalf("expected an unused answers, got: %v", input.answers)
  1051  	}
  1052  
  1053  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
  1054  	if !strings.Contains(errOutput, "hard failed") {
  1055  		t.Fatalf("expected a policy check error, got: %v", errOutput)
  1056  	}
  1057  
  1058  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1059  	if !strings.Contains(output, "Running apply in the remote backend") {
  1060  		t.Fatalf("expected remote backend header in output: %s", output)
  1061  	}
  1062  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1063  		t.Fatalf("expected plan summery in output: %s", output)
  1064  	}
  1065  	if !strings.Contains(output, "Sentinel Result: false") {
  1066  		t.Fatalf("expected policy check result in output: %s", output)
  1067  	}
  1068  	if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
  1069  		t.Fatalf("unexpected apply summery in output: %s", output)
  1070  	}
  1071  }
  1072  
  1073  func TestRemote_applyPolicySoftFail(t *testing.T) {
  1074  	b, bCleanup := testBackendDefault(t)
  1075  	defer bCleanup()
  1076  
  1077  	op, configCleanup := testOperationApply(t, "./testdata/apply-policy-soft-failed")
  1078  	defer configCleanup()
  1079  
  1080  	input := testInput(t, map[string]string{
  1081  		"override": "override",
  1082  		"approve":  "yes",
  1083  	})
  1084  
  1085  	op.UIIn = input
  1086  	op.UIOut = b.CLI
  1087  	op.Workspace = backend.DefaultStateName
  1088  
  1089  	run, err := b.Operation(context.Background(), op)
  1090  	if err != nil {
  1091  		t.Fatalf("error starting operation: %v", err)
  1092  	}
  1093  
  1094  	<-run.Done()
  1095  	if run.Result != backend.OperationSuccess {
  1096  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
  1097  	}
  1098  	if run.PlanEmpty {
  1099  		t.Fatalf("expected a non-empty plan")
  1100  	}
  1101  
  1102  	if len(input.answers) > 0 {
  1103  		t.Fatalf("expected no unused answers, got: %v", input.answers)
  1104  	}
  1105  
  1106  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1107  	if !strings.Contains(output, "Running apply in the remote backend") {
  1108  		t.Fatalf("expected remote backend header in output: %s", output)
  1109  	}
  1110  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1111  		t.Fatalf("expected plan summery in output: %s", output)
  1112  	}
  1113  	if !strings.Contains(output, "Sentinel Result: false") {
  1114  		t.Fatalf("expected policy check result in output: %s", output)
  1115  	}
  1116  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
  1117  		t.Fatalf("expected apply summery in output: %s", output)
  1118  	}
  1119  }
  1120  
  1121  func TestRemote_applyPolicySoftFailAutoApprove(t *testing.T) {
  1122  	b, bCleanup := testBackendDefault(t)
  1123  	defer bCleanup()
  1124  
  1125  	op, configCleanup := testOperationApply(t, "./testdata/apply-policy-soft-failed")
  1126  	defer configCleanup()
  1127  
  1128  	input := testInput(t, map[string]string{
  1129  		"override": "override",
  1130  	})
  1131  
  1132  	op.AutoApprove = true
  1133  	op.UIIn = input
  1134  	op.UIOut = b.CLI
  1135  	op.Workspace = backend.DefaultStateName
  1136  
  1137  	run, err := b.Operation(context.Background(), op)
  1138  	if err != nil {
  1139  		t.Fatalf("error starting operation: %v", err)
  1140  	}
  1141  
  1142  	<-run.Done()
  1143  	if run.Result == backend.OperationSuccess {
  1144  		t.Fatal("expected apply operation to fail")
  1145  	}
  1146  	if !run.PlanEmpty {
  1147  		t.Fatalf("expected plan to be empty")
  1148  	}
  1149  
  1150  	if len(input.answers) != 1 {
  1151  		t.Fatalf("expected an unused answers, got: %v", input.answers)
  1152  	}
  1153  
  1154  	errOutput := b.CLI.(*cli.MockUi).ErrorWriter.String()
  1155  	if !strings.Contains(errOutput, "soft failed") {
  1156  		t.Fatalf("expected a policy check error, got: %v", errOutput)
  1157  	}
  1158  
  1159  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1160  	if !strings.Contains(output, "Running apply in the remote backend") {
  1161  		t.Fatalf("expected remote backend header in output: %s", output)
  1162  	}
  1163  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1164  		t.Fatalf("expected plan summery in output: %s", output)
  1165  	}
  1166  	if !strings.Contains(output, "Sentinel Result: false") {
  1167  		t.Fatalf("expected policy check result in output: %s", output)
  1168  	}
  1169  	if strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
  1170  		t.Fatalf("unexpected apply summery in output: %s", output)
  1171  	}
  1172  }
  1173  
  1174  func TestRemote_applyPolicySoftFailAutoApply(t *testing.T) {
  1175  	b, bCleanup := testBackendDefault(t)
  1176  	defer bCleanup()
  1177  
  1178  	// Create a named workspace that auto applies.
  1179  	_, err := b.client.Workspaces.Create(
  1180  		context.Background(),
  1181  		b.organization,
  1182  		tfe.WorkspaceCreateOptions{
  1183  			AutoApply: tfe.Bool(true),
  1184  			Name:      tfe.String(b.prefix + "prod"),
  1185  		},
  1186  	)
  1187  	if err != nil {
  1188  		t.Fatalf("error creating named workspace: %v", err)
  1189  	}
  1190  
  1191  	op, configCleanup := testOperationApply(t, "./testdata/apply-policy-soft-failed")
  1192  	defer configCleanup()
  1193  
  1194  	input := testInput(t, map[string]string{
  1195  		"override": "override",
  1196  		"approve":  "yes",
  1197  	})
  1198  
  1199  	op.UIIn = input
  1200  	op.UIOut = b.CLI
  1201  	op.Workspace = "prod"
  1202  
  1203  	run, err := b.Operation(context.Background(), op)
  1204  	if err != nil {
  1205  		t.Fatalf("error starting operation: %v", err)
  1206  	}
  1207  
  1208  	<-run.Done()
  1209  	if run.Result != backend.OperationSuccess {
  1210  		t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String())
  1211  	}
  1212  	if run.PlanEmpty {
  1213  		t.Fatalf("expected a non-empty plan")
  1214  	}
  1215  
  1216  	if len(input.answers) != 1 {
  1217  		t.Fatalf("expected an unused answer, got: %v", input.answers)
  1218  	}
  1219  
  1220  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1221  	if !strings.Contains(output, "Running apply in the remote backend") {
  1222  		t.Fatalf("expected remote backend header in output: %s", output)
  1223  	}
  1224  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
  1225  		t.Fatalf("expected plan summery in output: %s", output)
  1226  	}
  1227  	if !strings.Contains(output, "Sentinel Result: false") {
  1228  		t.Fatalf("expected policy check result in output: %s", output)
  1229  	}
  1230  	if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") {
  1231  		t.Fatalf("expected apply summery in output: %s", output)
  1232  	}
  1233  }
  1234  
  1235  func TestRemote_applyWithRemoteError(t *testing.T) {
  1236  	b, bCleanup := testBackendDefault(t)
  1237  	defer bCleanup()
  1238  
  1239  	op, configCleanup := testOperationApply(t, "./testdata/apply-with-error")
  1240  	defer configCleanup()
  1241  
  1242  	op.Workspace = backend.DefaultStateName
  1243  
  1244  	run, err := b.Operation(context.Background(), op)
  1245  	if err != nil {
  1246  		t.Fatalf("error starting operation: %v", err)
  1247  	}
  1248  
  1249  	<-run.Done()
  1250  	if run.Result == backend.OperationSuccess {
  1251  		t.Fatal("expected apply operation to fail")
  1252  	}
  1253  	if run.Result.ExitStatus() != 1 {
  1254  		t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus())
  1255  	}
  1256  
  1257  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
  1258  	if !strings.Contains(output, "null_resource.foo: 1 error") {
  1259  		t.Fatalf("expected apply error in output: %s", output)
  1260  	}
  1261  }