github.com/magodo/terraform@v0.11.12-beta1/backend/remote/backend_plan_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  	tfe "github.com/hashicorp/go-tfe"
    13  	"github.com/hashicorp/terraform/backend"
    14  	"github.com/hashicorp/terraform/config/module"
    15  	"github.com/hashicorp/terraform/terraform"
    16  	"github.com/mitchellh/cli"
    17  )
    18  
    19  func testOperationPlan() *backend.Operation {
    20  	return &backend.Operation{
    21  		ModuleDepth: defaultModuleDepth,
    22  		Parallelism: defaultParallelism,
    23  		PlanRefresh: true,
    24  		Type:        backend.OperationTypePlan,
    25  	}
    26  }
    27  
    28  func TestRemote_planBasic(t *testing.T) {
    29  	b := testBackendDefault(t)
    30  
    31  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
    32  	defer modCleanup()
    33  
    34  	op := testOperationPlan()
    35  	op.Module = mod
    36  	op.Workspace = backend.DefaultStateName
    37  
    38  	run, err := b.Operation(context.Background(), op)
    39  	if err != nil {
    40  		t.Fatalf("error starting operation: %v", err)
    41  	}
    42  
    43  	<-run.Done()
    44  	if run.Err != nil {
    45  		t.Fatalf("error running operation: %v", run.Err)
    46  	}
    47  	if run.PlanEmpty {
    48  		t.Fatal("expected a non-empty plan")
    49  	}
    50  
    51  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
    52  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
    53  		t.Fatalf("missing plan summery in output: %s", output)
    54  	}
    55  }
    56  
    57  func TestRemote_planWithoutPermissions(t *testing.T) {
    58  	b := testBackendNoDefault(t)
    59  
    60  	// Create a named workspace without permissions.
    61  	w, err := b.client.Workspaces.Create(
    62  		context.Background(),
    63  		b.organization,
    64  		tfe.WorkspaceCreateOptions{
    65  			Name: tfe.String(b.prefix + "prod"),
    66  		},
    67  	)
    68  	if err != nil {
    69  		t.Fatalf("error creating named workspace: %v", err)
    70  	}
    71  	w.Permissions.CanQueueRun = false
    72  
    73  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
    74  	defer modCleanup()
    75  
    76  	op := testOperationPlan()
    77  	op.Module = mod
    78  	op.Workspace = "prod"
    79  
    80  	run, err := b.Operation(context.Background(), op)
    81  	if err != nil {
    82  		t.Fatalf("error starting operation: %v", err)
    83  	}
    84  
    85  	<-run.Done()
    86  	if run.Err == nil {
    87  		t.Fatalf("expected a plan error, got: %v", run.Err)
    88  	}
    89  	if !strings.Contains(run.Err.Error(), "insufficient rights to generate a plan") {
    90  		t.Fatalf("expected a permissions error, got: %v", run.Err)
    91  	}
    92  }
    93  
    94  func TestRemote_planWithModuleDepth(t *testing.T) {
    95  	b := testBackendDefault(t)
    96  
    97  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
    98  	defer modCleanup()
    99  
   100  	op := testOperationPlan()
   101  	op.Module = mod
   102  	op.ModuleDepth = 1
   103  	op.Workspace = backend.DefaultStateName
   104  
   105  	run, err := b.Operation(context.Background(), op)
   106  	if err != nil {
   107  		t.Fatalf("error starting operation: %v", err)
   108  	}
   109  
   110  	<-run.Done()
   111  	if run.Err == nil {
   112  		t.Fatalf("expected a plan error, got: %v", run.Err)
   113  	}
   114  	if !strings.Contains(run.Err.Error(), "module depths are currently not supported") {
   115  		t.Fatalf("expected a module depth error, got: %v", run.Err)
   116  	}
   117  }
   118  
   119  func TestRemote_planWithParallelism(t *testing.T) {
   120  	b := testBackendDefault(t)
   121  
   122  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
   123  	defer modCleanup()
   124  
   125  	op := testOperationPlan()
   126  	op.Module = mod
   127  	op.Parallelism = 3
   128  	op.Workspace = backend.DefaultStateName
   129  
   130  	run, err := b.Operation(context.Background(), op)
   131  	if err != nil {
   132  		t.Fatalf("error starting operation: %v", err)
   133  	}
   134  
   135  	<-run.Done()
   136  	if run.Err == nil {
   137  		t.Fatalf("expected a plan error, got: %v", run.Err)
   138  	}
   139  	if !strings.Contains(run.Err.Error(), "parallelism values are currently not supported") {
   140  		t.Fatalf("expected a parallelism error, got: %v", run.Err)
   141  	}
   142  }
   143  
   144  func TestRemote_planWithPlan(t *testing.T) {
   145  	b := testBackendDefault(t)
   146  
   147  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
   148  	defer modCleanup()
   149  
   150  	op := testOperationPlan()
   151  	op.Module = mod
   152  	op.Plan = &terraform.Plan{}
   153  	op.Workspace = backend.DefaultStateName
   154  
   155  	run, err := b.Operation(context.Background(), op)
   156  	if err != nil {
   157  		t.Fatalf("error starting operation: %v", err)
   158  	}
   159  
   160  	<-run.Done()
   161  	if run.Err == nil {
   162  		t.Fatalf("expected a plan error, got: %v", run.Err)
   163  	}
   164  	if !run.PlanEmpty {
   165  		t.Fatalf("expected plan to be empty")
   166  	}
   167  	if !strings.Contains(run.Err.Error(), "saved plan is currently not supported") {
   168  		t.Fatalf("expected a saved plan error, got: %v", run.Err)
   169  	}
   170  }
   171  
   172  func TestRemote_planWithPath(t *testing.T) {
   173  	b := testBackendDefault(t)
   174  
   175  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
   176  	defer modCleanup()
   177  
   178  	op := testOperationPlan()
   179  	op.Module = mod
   180  	op.PlanOutPath = "./test-fixtures/plan"
   181  	op.Workspace = backend.DefaultStateName
   182  
   183  	run, err := b.Operation(context.Background(), op)
   184  	if err != nil {
   185  		t.Fatalf("error starting operation: %v", err)
   186  	}
   187  
   188  	<-run.Done()
   189  	if run.Err == nil {
   190  		t.Fatalf("expected a plan error, got: %v", run.Err)
   191  	}
   192  	if !run.PlanEmpty {
   193  		t.Fatalf("expected plan to be empty")
   194  	}
   195  	if !strings.Contains(run.Err.Error(), "generated plan is currently not supported") {
   196  		t.Fatalf("expected a generated plan error, got: %v", run.Err)
   197  	}
   198  }
   199  
   200  func TestRemote_planWithoutRefresh(t *testing.T) {
   201  	b := testBackendDefault(t)
   202  
   203  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
   204  	defer modCleanup()
   205  
   206  	op := testOperationPlan()
   207  	op.Module = mod
   208  	op.PlanRefresh = false
   209  	op.Workspace = backend.DefaultStateName
   210  
   211  	run, err := b.Operation(context.Background(), op)
   212  	if err != nil {
   213  		t.Fatalf("error starting operation: %v", err)
   214  	}
   215  
   216  	<-run.Done()
   217  	if run.Err == nil {
   218  		t.Fatalf("expected a plan error, got: %v", run.Err)
   219  	}
   220  	if !strings.Contains(run.Err.Error(), "refresh is currently not supported") {
   221  		t.Fatalf("expected a refresh error, got: %v", run.Err)
   222  	}
   223  }
   224  
   225  func TestRemote_planWithTarget(t *testing.T) {
   226  	b := testBackendDefault(t)
   227  
   228  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
   229  	defer modCleanup()
   230  
   231  	op := testOperationPlan()
   232  	op.Module = mod
   233  	op.Targets = []string{"null_resource.foo"}
   234  	op.Workspace = backend.DefaultStateName
   235  
   236  	run, err := b.Operation(context.Background(), op)
   237  	if err != nil {
   238  		t.Fatalf("error starting operation: %v", err)
   239  	}
   240  
   241  	<-run.Done()
   242  	if run.Err == nil {
   243  		t.Fatalf("expected a plan error, got: %v", run.Err)
   244  	}
   245  	if !run.PlanEmpty {
   246  		t.Fatalf("expected plan to be empty")
   247  	}
   248  	if !strings.Contains(run.Err.Error(), "targeting is currently not supported") {
   249  		t.Fatalf("expected a targeting error, got: %v", run.Err)
   250  	}
   251  }
   252  
   253  func TestRemote_planWithVariables(t *testing.T) {
   254  	b := testBackendDefault(t)
   255  
   256  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan-variables")
   257  	defer modCleanup()
   258  
   259  	op := testOperationPlan()
   260  	op.Module = mod
   261  	op.Variables = map[string]interface{}{"foo": "bar"}
   262  	op.Workspace = backend.DefaultStateName
   263  
   264  	run, err := b.Operation(context.Background(), op)
   265  	if err != nil {
   266  		t.Fatalf("error starting operation: %v", err)
   267  	}
   268  
   269  	<-run.Done()
   270  	if run.Err == nil {
   271  		t.Fatalf("expected an plan error, got: %v", run.Err)
   272  	}
   273  	if !strings.Contains(run.Err.Error(), "variables are currently not supported") {
   274  		t.Fatalf("expected a variables error, got: %v", run.Err)
   275  	}
   276  }
   277  
   278  func TestRemote_planNoConfig(t *testing.T) {
   279  	b := testBackendDefault(t)
   280  
   281  	op := testOperationPlan()
   282  	op.Module = nil
   283  	op.Workspace = backend.DefaultStateName
   284  
   285  	run, err := b.Operation(context.Background(), op)
   286  	if err != nil {
   287  		t.Fatalf("error starting operation: %v", err)
   288  	}
   289  
   290  	<-run.Done()
   291  	if run.Err == nil {
   292  		t.Fatalf("expected a plan error, got: %v", run.Err)
   293  	}
   294  	if !run.PlanEmpty {
   295  		t.Fatalf("expected plan to be empty")
   296  	}
   297  	if !strings.Contains(run.Err.Error(), "configuration files found") {
   298  		t.Fatalf("expected configuration files error, got: %v", run.Err)
   299  	}
   300  }
   301  
   302  func TestRemote_planNoChanges(t *testing.T) {
   303  	b := testBackendDefault(t)
   304  
   305  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan-no-changes")
   306  	defer modCleanup()
   307  
   308  	op := testOperationPlan()
   309  	op.Module = mod
   310  	op.Workspace = backend.DefaultStateName
   311  
   312  	run, err := b.Operation(context.Background(), op)
   313  	if err != nil {
   314  		t.Fatalf("error starting operation: %v", err)
   315  	}
   316  
   317  	<-run.Done()
   318  	if run.Err != nil {
   319  		t.Fatalf("error running operation: %v", run.Err)
   320  	}
   321  	if !run.PlanEmpty {
   322  		t.Fatalf("expected plan to be empty")
   323  	}
   324  
   325  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   326  	if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") {
   327  		t.Fatalf("expected no changes in plan summery: %s", output)
   328  	}
   329  	if !strings.Contains(output, "Sentinel Result: true") {
   330  		t.Fatalf("expected policy check result in output: %s", output)
   331  	}
   332  }
   333  
   334  func TestRemote_planForceLocal(t *testing.T) {
   335  	// Set TF_FORCE_LOCAL_BACKEND so the remote backend will use
   336  	// the local backend with itself as embedded backend.
   337  	if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil {
   338  		t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err)
   339  	}
   340  	defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND")
   341  
   342  	b := testBackendDefault(t)
   343  
   344  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
   345  	defer modCleanup()
   346  
   347  	op := testOperationPlan()
   348  	op.Module = mod
   349  	op.Workspace = backend.DefaultStateName
   350  
   351  	run, err := b.Operation(context.Background(), op)
   352  	if err != nil {
   353  		t.Fatalf("error starting operation: %v", err)
   354  	}
   355  
   356  	<-run.Done()
   357  	if run.Err != nil {
   358  		t.Fatalf("error running operation: %v", run.Err)
   359  	}
   360  	if run.PlanEmpty {
   361  		t.Fatalf("expected a non-empty plan")
   362  	}
   363  
   364  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   365  	if strings.Contains(output, "Running plan in the remote backend") {
   366  		t.Fatalf("unexpected remote backend header in output: %s", output)
   367  	}
   368  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   369  		t.Fatalf("expected plan summery in output: %s", output)
   370  	}
   371  }
   372  
   373  func TestRemote_planWithoutOperationsEntitlement(t *testing.T) {
   374  	b := testBackendNoOperations(t)
   375  
   376  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
   377  	defer modCleanup()
   378  
   379  	op := testOperationPlan()
   380  	op.Module = mod
   381  	op.Workspace = backend.DefaultStateName
   382  
   383  	run, err := b.Operation(context.Background(), op)
   384  	if err != nil {
   385  		t.Fatalf("error starting operation: %v", err)
   386  	}
   387  
   388  	<-run.Done()
   389  	if run.Err != nil {
   390  		t.Fatalf("error running operation: %v", run.Err)
   391  	}
   392  	if run.PlanEmpty {
   393  		t.Fatalf("expected a non-empty plan")
   394  	}
   395  
   396  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   397  	if strings.Contains(output, "Running plan in the remote backend") {
   398  		t.Fatalf("unexpected remote backend header in output: %s", output)
   399  	}
   400  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   401  		t.Fatalf("expected plan summery in output: %s", output)
   402  	}
   403  }
   404  
   405  func TestRemote_planWorkspaceWithoutOperations(t *testing.T) {
   406  	b := testBackendNoDefault(t)
   407  	ctx := context.Background()
   408  
   409  	// Create a named workspace that doesn't allow operations.
   410  	_, err := b.client.Workspaces.Create(
   411  		ctx,
   412  		b.organization,
   413  		tfe.WorkspaceCreateOptions{
   414  			Name: tfe.String(b.prefix + "no-operations"),
   415  		},
   416  	)
   417  	if err != nil {
   418  		t.Fatalf("error creating named workspace: %v", err)
   419  	}
   420  
   421  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
   422  	defer modCleanup()
   423  
   424  	op := testOperationPlan()
   425  	op.Module = mod
   426  	op.Workspace = "no-operations"
   427  
   428  	run, err := b.Operation(context.Background(), op)
   429  	if err != nil {
   430  		t.Fatalf("error starting operation: %v", err)
   431  	}
   432  
   433  	<-run.Done()
   434  	if run.Err != nil {
   435  		t.Fatalf("error running operation: %v", run.Err)
   436  	}
   437  	if run.PlanEmpty {
   438  		t.Fatalf("expected a non-empty plan")
   439  	}
   440  
   441  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   442  	if strings.Contains(output, "Running plan in the remote backend") {
   443  		t.Fatalf("unexpected remote backend header in output: %s", output)
   444  	}
   445  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   446  		t.Fatalf("expected plan summery in output: %s", output)
   447  	}
   448  }
   449  
   450  func TestRemote_planLockTimeout(t *testing.T) {
   451  	b := testBackendDefault(t)
   452  	ctx := context.Background()
   453  
   454  	// Retrieve the workspace used to run this operation in.
   455  	w, err := b.client.Workspaces.Read(ctx, b.organization, b.workspace)
   456  	if err != nil {
   457  		t.Fatalf("error retrieving workspace: %v", err)
   458  	}
   459  
   460  	// Create a new configuration version.
   461  	c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{})
   462  	if err != nil {
   463  		t.Fatalf("error creating configuration version: %v", err)
   464  	}
   465  
   466  	// Create a pending run to block this run.
   467  	_, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{
   468  		ConfigurationVersion: c,
   469  		Workspace:            w,
   470  	})
   471  	if err != nil {
   472  		t.Fatalf("error creating pending run: %v", err)
   473  	}
   474  
   475  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
   476  	defer modCleanup()
   477  
   478  	input := testInput(t, map[string]string{
   479  		"cancel":  "yes",
   480  		"approve": "yes",
   481  	})
   482  
   483  	op := testOperationPlan()
   484  	op.StateLockTimeout = 5 * time.Second
   485  	op.Module = mod
   486  	op.UIIn = input
   487  	op.UIOut = b.CLI
   488  	op.Workspace = backend.DefaultStateName
   489  
   490  	_, err = b.Operation(context.Background(), op)
   491  	if err != nil {
   492  		t.Fatalf("error starting operation: %v", err)
   493  	}
   494  
   495  	sigint := make(chan os.Signal, 1)
   496  	signal.Notify(sigint, syscall.SIGINT)
   497  	select {
   498  	case <-sigint:
   499  		// Stop redirecting SIGINT signals.
   500  		signal.Stop(sigint)
   501  	case <-time.After(10 * time.Second):
   502  		t.Fatalf("expected lock timeout after 5 seconds, waited 10 seconds")
   503  	}
   504  
   505  	if len(input.answers) != 2 {
   506  		t.Fatalf("expected unused answers, got: %v", input.answers)
   507  	}
   508  
   509  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   510  	if !strings.Contains(output, "Lock timeout exceeded") {
   511  		t.Fatalf("missing lock timout error in output: %s", output)
   512  	}
   513  	if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   514  		t.Fatalf("unexpected plan summery in output: %s", output)
   515  	}
   516  }
   517  
   518  func TestRemote_planDestroy(t *testing.T) {
   519  	b := testBackendDefault(t)
   520  
   521  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan")
   522  	defer modCleanup()
   523  
   524  	op := testOperationPlan()
   525  	op.Destroy = true
   526  	op.Module = mod
   527  	op.Workspace = backend.DefaultStateName
   528  
   529  	run, err := b.Operation(context.Background(), op)
   530  	if err != nil {
   531  		t.Fatalf("error starting operation: %v", err)
   532  	}
   533  
   534  	<-run.Done()
   535  	if run.Err != nil {
   536  		t.Fatalf("unexpected plan error: %v", run.Err)
   537  	}
   538  	if run.PlanEmpty {
   539  		t.Fatalf("expected a non-empty plan")
   540  	}
   541  }
   542  
   543  func TestRemote_planDestroyNoConfig(t *testing.T) {
   544  	b := testBackendDefault(t)
   545  
   546  	op := testOperationPlan()
   547  	op.Destroy = true
   548  	op.Module = nil
   549  	op.Workspace = backend.DefaultStateName
   550  
   551  	run, err := b.Operation(context.Background(), op)
   552  	if err != nil {
   553  		t.Fatalf("error starting operation: %v", err)
   554  	}
   555  
   556  	<-run.Done()
   557  	if run.Err != nil {
   558  		t.Fatalf("unexpected plan error: %v", run.Err)
   559  	}
   560  	if run.PlanEmpty {
   561  		t.Fatalf("expected a non-empty plan")
   562  	}
   563  }
   564  
   565  func TestRemote_planWithWorkingDirectory(t *testing.T) {
   566  	b := testBackendDefault(t)
   567  
   568  	options := tfe.WorkspaceUpdateOptions{
   569  		WorkingDirectory: tfe.String("terraform"),
   570  	}
   571  
   572  	// Configure the workspace to use a custom working direcrtory.
   573  	_, err := b.client.Workspaces.Update(context.Background(), b.organization, b.workspace, options)
   574  	if err != nil {
   575  		t.Fatalf("error configuring working directory: %v", err)
   576  	}
   577  
   578  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan-with-working-directory/terraform")
   579  	defer modCleanup()
   580  
   581  	op := testOperationPlan()
   582  	op.Module = mod
   583  	op.Workspace = backend.DefaultStateName
   584  
   585  	run, err := b.Operation(context.Background(), op)
   586  	if err != nil {
   587  		t.Fatalf("error starting operation: %v", err)
   588  	}
   589  
   590  	<-run.Done()
   591  	if run.Err != nil {
   592  		t.Fatalf("error running operation: %v", run.Err)
   593  	}
   594  	if run.PlanEmpty {
   595  		t.Fatalf("expected a non-empty plan")
   596  	}
   597  
   598  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   599  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   600  		t.Fatalf("missing plan summery in output: %s", output)
   601  	}
   602  }
   603  
   604  func TestRemote_planPolicyPass(t *testing.T) {
   605  	b := testBackendDefault(t)
   606  
   607  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan-policy-passed")
   608  	defer modCleanup()
   609  
   610  	input := testInput(t, map[string]string{})
   611  
   612  	op := testOperationPlan()
   613  	op.Module = mod
   614  	op.UIIn = input
   615  	op.UIOut = b.CLI
   616  	op.Workspace = backend.DefaultStateName
   617  
   618  	run, err := b.Operation(context.Background(), op)
   619  	if err != nil {
   620  		t.Fatalf("error starting operation: %v", err)
   621  	}
   622  
   623  	<-run.Done()
   624  	if run.Err != nil {
   625  		t.Fatalf("error running operation: %v", run.Err)
   626  	}
   627  	if run.PlanEmpty {
   628  		t.Fatalf("expected a non-empty plan")
   629  	}
   630  
   631  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   632  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   633  		t.Fatalf("missing plan summery in output: %s", output)
   634  	}
   635  	if !strings.Contains(output, "Sentinel Result: true") {
   636  		t.Fatalf("missing polic check result in output: %s", output)
   637  	}
   638  }
   639  
   640  func TestRemote_planPolicyHardFail(t *testing.T) {
   641  	b := testBackendDefault(t)
   642  
   643  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan-policy-hard-failed")
   644  	defer modCleanup()
   645  
   646  	input := testInput(t, map[string]string{})
   647  
   648  	op := testOperationPlan()
   649  	op.Module = mod
   650  	op.UIIn = input
   651  	op.UIOut = b.CLI
   652  	op.Workspace = backend.DefaultStateName
   653  
   654  	run, err := b.Operation(context.Background(), op)
   655  	if err != nil {
   656  		t.Fatalf("error starting operation: %v", err)
   657  	}
   658  
   659  	<-run.Done()
   660  	if run.Err == nil {
   661  		t.Fatalf("expected a plan error, got: %v", run.Err)
   662  	}
   663  	if !run.PlanEmpty {
   664  		t.Fatalf("expected plan to be empty")
   665  	}
   666  	if !strings.Contains(run.Err.Error(), "hard failed") {
   667  		t.Fatalf("expected a policy check error, got: %v", run.Err)
   668  	}
   669  
   670  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   671  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   672  		t.Fatalf("missing plan summery in output: %s", output)
   673  	}
   674  	if !strings.Contains(output, "Sentinel Result: false") {
   675  		t.Fatalf("missing policy check result in output: %s", output)
   676  	}
   677  }
   678  
   679  func TestRemote_planPolicySoftFail(t *testing.T) {
   680  	b := testBackendDefault(t)
   681  
   682  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan-policy-soft-failed")
   683  	defer modCleanup()
   684  
   685  	input := testInput(t, map[string]string{})
   686  
   687  	op := testOperationPlan()
   688  	op.Module = mod
   689  	op.UIIn = input
   690  	op.UIOut = b.CLI
   691  	op.Workspace = backend.DefaultStateName
   692  
   693  	run, err := b.Operation(context.Background(), op)
   694  	if err != nil {
   695  		t.Fatalf("error starting operation: %v", err)
   696  	}
   697  
   698  	<-run.Done()
   699  	if run.Err == nil {
   700  		t.Fatalf("expected a plan error, got: %v", run.Err)
   701  	}
   702  	if !run.PlanEmpty {
   703  		t.Fatalf("expected plan to be empty")
   704  	}
   705  	if !strings.Contains(run.Err.Error(), "soft failed") {
   706  		t.Fatalf("expected a policy check error, got: %v", run.Err)
   707  	}
   708  
   709  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   710  	if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") {
   711  		t.Fatalf("missing plan summery in output: %s", output)
   712  	}
   713  	if !strings.Contains(output, "Sentinel Result: false") {
   714  		t.Fatalf("missing policy check result in output: %s", output)
   715  	}
   716  }
   717  
   718  func TestRemote_planWithRemoteError(t *testing.T) {
   719  	b := testBackendDefault(t)
   720  
   721  	mod, modCleanup := module.TestTree(t, "./test-fixtures/plan-with-error")
   722  	defer modCleanup()
   723  
   724  	op := testOperationPlan()
   725  	op.Module = mod
   726  	op.Workspace = backend.DefaultStateName
   727  
   728  	run, err := b.Operation(context.Background(), op)
   729  	if err != nil {
   730  		t.Fatalf("error starting operation: %v", err)
   731  	}
   732  
   733  	<-run.Done()
   734  	if run.Err != nil {
   735  		t.Fatalf("error running operation: %v", run.Err)
   736  	}
   737  	if run.ExitCode != 1 {
   738  		t.Fatalf("expected exit code 1, got %d", run.ExitCode)
   739  	}
   740  
   741  	output := b.CLI.(*cli.MockUi).OutputWriter.String()
   742  	if !strings.Contains(output, "null_resource.foo: 1 error") {
   743  		t.Fatalf("missing plan error in output: %s", output)
   744  	}
   745  }