github.com/opentofu/opentofu@v1.7.1/internal/cloud/backend_apply_test.go (about)

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