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