package cloud import ( "context" "os" "os/signal" "strings" "syscall" "testing" "time" "github.com/google/go-cmp/cmp" tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planfile" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" "github.com/mitchellh/cli" ) func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() return testOperationPlanWithTimeout(t, configDir, 0) } func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) streams, done := terminal.StreamsForTesting(t) view := views.NewView(streams) stateLockerView := views.NewStateLocker(arguments.ViewHuman, view) operationView := views.NewOperation(arguments.ViewHuman, false, view) // Many of our tests use an overridden "null" provider that's just in-memory // inside the test process, not a separate plugin on disk. depLocks := depsfile.NewLocks() depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null")) return &backend.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, PlanRefresh: true, StateLocker: clistate.NewLocker(timeout, stateLockerView), Type: backend.OperationTypePlan, View: operationView, DependencyLocks: depLocks, }, configCleanup, done } func TestCloud_planBasic(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatal("expected a non-empty plan") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("expected TFC header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) // An error suggests that the state was not unlocked after the operation finished if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { t.Fatalf("unexpected error locking state after successful plan: %s", err.Error()) } } func TestCloud_planCanceled(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } // Stop the run to simulate a Ctrl-C. run.Stop() <-run.Done() if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) // An error suggests that the state was not unlocked after the operation finished if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { t.Fatalf("unexpected error locking state after cancelled plan: %s", err.Error()) } } func TestCloud_planLongLine(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan-long-line") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatal("expected a non-empty plan") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("expected TFC header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } func TestCloud_planWithoutPermissions(t *testing.T) { b, bCleanup := testBackendWithPrefix(t) defer bCleanup() // Create a named workspace without permissions. w, err := b.client.Workspaces.Create( context.Background(), b.organization, tfe.WorkspaceCreateOptions{ Name: tfe.String(b.WorkspaceMapping.Prefix + "prod"), }, ) if err != nil { t.Fatalf("error creating named workspace: %v", err) } w.Permissions.CanQueueRun = false op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() op.Workspace = "prod" run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() output := done(t) if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } errOutput := output.Stderr() if !strings.Contains(errOutput, "Insufficient rights to generate a plan") { t.Fatalf("expected a permissions error, got: %v", errOutput) } } func TestCloud_planWithParallelism(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() if b.ContextOpts == nil { b.ContextOpts = &terraform.ContextOpts{} } b.ContextOpts.Parallelism = 3 op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() output := done(t) if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } errOutput := output.Stderr() if !strings.Contains(errOutput, "parallelism values are currently not supported") { t.Fatalf("expected a parallelism error, got: %v", errOutput) } } func TestCloud_planWithPlan(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() op.PlanFile = &planfile.Reader{} op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() output := done(t) if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := output.Stderr() if !strings.Contains(errOutput, "saved plan is currently not supported") { t.Fatalf("expected a saved plan error, got: %v", errOutput) } } func TestCloud_planWithPath(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() op.PlanOutPath = "./testdata/plan" op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() output := done(t) if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := output.Stderr() if !strings.Contains(errOutput, "generated plan is currently not supported") { t.Fatalf("expected a generated plan error, got: %v", errOutput) } } func TestCloud_planWithoutRefresh(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) op.PlanRefresh = false op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatal("expected a non-empty plan") } // We should find a run inside the mock client that has refresh set // to false. runsAPI := b.client.Runs.(*MockRuns) if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } for _, run := range runsAPI.Runs { if diff := cmp.Diff(false, run.Refresh); diff != "" { t.Errorf("wrong Refresh setting in the created run\n%s", diff) } } } func TestCloud_planWithRefreshOnly(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) op.PlanMode = plans.RefreshOnlyMode op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatal("expected a non-empty plan") } // We should find a run inside the mock client that has refresh-only set // to true. runsAPI := b.client.Runs.(*MockRuns) if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } for _, run := range runsAPI.Runs { if diff := cmp.Diff(true, run.RefreshOnly); diff != "" { t.Errorf("wrong RefreshOnly setting in the created run\n%s", diff) } } } func TestCloud_planWithTarget(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() // When the backend code creates a new run, we'll tweak it so that it // has a cost estimation object with the "skipped_due_to_targeting" status, // emulating how a real server is expected to behave in that case. b.client.Runs.(*MockRuns).ModifyNewRun = func(client *MockClient, options tfe.RunCreateOptions, run *tfe.Run) { const fakeID = "fake" // This is the cost estimate object embedded in the run itself which // the backend will use to learn the ID to request from the cost // estimates endpoint. It's pending to simulate what a freshly-created // run is likely to look like. run.CostEstimate = &tfe.CostEstimate{ ID: fakeID, Status: "pending", } // The backend will then use the main cost estimation API to retrieve // the same ID indicated in the object above, where we'll then return // the status "skipped_due_to_targeting" to trigger the special skip // message in the backend output. client.CostEstimates.Estimations[fakeID] = &tfe.CostEstimate{ ID: fakeID, Status: "skipped_due_to_targeting", } } op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") op.Targets = []addrs.Targetable{addr} op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatal("expected plan operation to succeed") } if run.PlanEmpty { t.Fatalf("expected plan to be non-empty") } // testBackendDefault above attached a "mock UI" to our backend, so we // can retrieve its non-error output via the OutputWriter in-memory buffer. gotOutput := b.CLI.(*cli.MockUi).OutputWriter.String() if wantOutput := "Not available for this plan, because it was created with the -target option."; !strings.Contains(gotOutput, wantOutput) { t.Errorf("missing message about skipped cost estimation\ngot:\n%s\nwant substring: %s", gotOutput, wantOutput) } // We should find a run inside the mock client that has the same // target address we requested above. runsAPI := b.client.Runs.(*MockRuns) if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } for _, run := range runsAPI.Runs { if diff := cmp.Diff([]string{"null_resource.foo"}, run.TargetAddrs); diff != "" { t.Errorf("wrong TargetAddrs in the created run\n%s", diff) } } } func TestCloud_planWithReplace(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo") op.ForceReplace = []addrs.AbsResourceInstance{addr} op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatal("expected plan operation to succeed") } if run.PlanEmpty { t.Fatalf("expected plan to be non-empty") } // We should find a run inside the mock client that has the same // refresh address we requested above. runsAPI := b.client.Runs.(*MockRuns) if got, want := len(runsAPI.Runs), 1; got != want { t.Fatalf("wrong number of runs in the mock client %d; want %d", got, want) } for _, run := range runsAPI.Runs { if diff := cmp.Diff([]string{"null_resource.foo"}, run.ReplaceAddrs); diff != "" { t.Errorf("wrong ReplaceAddrs in the created run\n%s", diff) } } } func TestCloud_planWithRequiredVariables(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan-variables") defer configCleanup() defer done(t) op.Variables = testVariables(terraform.ValueFromCLIArg, "foo") // "bar" variable value missing op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() // The usual error of a required variable being missing is deferred and the operation // is successful if run.Result != backend.OperationSuccess { t.Fatal("expected plan operation to succeed") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("unexpected TFC header in output: %s", output) } } func TestCloud_planNoConfig(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/empty") defer configCleanup() op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() output := done(t) if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := output.Stderr() if !strings.Contains(errOutput, "configuration files found") { t.Fatalf("expected configuration files error, got: %v", errOutput) } } func TestCloud_planNoChanges(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan-no-changes") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "No changes. Infrastructure is up-to-date.") { t.Fatalf("expected no changes in plan summary: %s", output) } if !strings.Contains(output, "Sentinel Result: true") { t.Fatalf("expected policy check result in output: %s", output) } } func TestCloud_planForceLocal(t *testing.T) { // Set TF_FORCE_LOCAL_BACKEND so the cloud backend will use // the local backend with itself as embedded backend. if err := os.Setenv("TF_FORCE_LOCAL_BACKEND", "1"); err != nil { t.Fatalf("error setting environment variable TF_FORCE_LOCAL_BACKEND: %v", err) } defer os.Unsetenv("TF_FORCE_LOCAL_BACKEND") b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName streams, done := terminal.StreamsForTesting(t) view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) op.View = view run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatalf("expected a non-empty plan") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("unexpected TFC header in output: %s", output) } if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } func TestCloud_planWithoutOperationsEntitlement(t *testing.T) { b, bCleanup := testBackendNoOperations(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName streams, done := terminal.StreamsForTesting(t) view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) op.View = view run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatalf("expected a non-empty plan") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("unexpected TFC header in output: %s", output) } if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } func TestCloud_planWorkspaceWithoutOperations(t *testing.T) { b, bCleanup := testBackendWithPrefix(t) defer bCleanup() ctx := context.Background() // Create a named workspace that doesn't allow operations. _, err := b.client.Workspaces.Create( ctx, b.organization, tfe.WorkspaceCreateOptions{ Name: tfe.String(b.WorkspaceMapping.Prefix + "no-operations"), }, ) if err != nil { t.Fatalf("error creating named workspace: %v", err) } op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) op.Workspace = "no-operations" streams, done := terminal.StreamsForTesting(t) view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) op.View = view run, err := b.Operation(ctx, op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatalf("expected a non-empty plan") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("unexpected TFC header in output: %s", output) } if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } func TestCloud_planLockTimeout(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() ctx := context.Background() // Retrieve the workspace used to run this operation in. w, err := b.client.Workspaces.Read(ctx, b.organization, b.WorkspaceMapping.Name) if err != nil { t.Fatalf("error retrieving workspace: %v", err) } // Create a new configuration version. c, err := b.client.ConfigurationVersions.Create(ctx, w.ID, tfe.ConfigurationVersionCreateOptions{}) if err != nil { t.Fatalf("error creating configuration version: %v", err) } // Create a pending run to block this run. _, err = b.client.Runs.Create(ctx, tfe.RunCreateOptions{ ConfigurationVersion: c, Workspace: w, }) if err != nil { t.Fatalf("error creating pending run: %v", err) } op, configCleanup, done := testOperationPlanWithTimeout(t, "./testdata/plan", 50) defer configCleanup() defer done(t) input := testInput(t, map[string]string{ "cancel": "yes", "approve": "yes", }) op.UIIn = input op.UIOut = b.CLI op.Workspace = testBackendSingleWorkspaceName _, err = b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } sigint := make(chan os.Signal, 1) signal.Notify(sigint, syscall.SIGINT) select { case <-sigint: // Stop redirecting SIGINT signals. signal.Stop(sigint) case <-time.After(200 * time.Millisecond): t.Fatalf("expected lock timeout after 50 milliseconds, waited 200 milliseconds") } if len(input.answers) != 2 { t.Fatalf("expected unused answers, got: %v", input.answers) } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("expected TFC header in output: %s", output) } if !strings.Contains(output, "Lock timeout exceeded") { t.Fatalf("expected lock timout error in output: %s", output) } if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("unexpected plan summary in output: %s", output) } } func TestCloud_planDestroy(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) op.PlanMode = plans.DestroyMode op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatalf("expected a non-empty plan") } } func TestCloud_planDestroyNoConfig(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/empty") defer configCleanup() defer done(t) op.PlanMode = plans.DestroyMode op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatalf("expected a non-empty plan") } } func TestCloud_planWithWorkingDirectory(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() options := tfe.WorkspaceUpdateOptions{ WorkingDirectory: tfe.String("terraform"), } // Configure the workspace to use a custom working directory. _, err := b.client.Workspaces.Update(context.Background(), b.organization, b.WorkspaceMapping.Name, options) if err != nil { t.Fatalf("error configuring working directory: %v", err) } op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-working-directory/terraform") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatalf("expected a non-empty plan") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "The remote workspace is configured to work with configuration") { t.Fatalf("expected working directory warning: %s", output) } if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("expected TFC header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } func TestCloud_planWithWorkingDirectoryFromCurrentPath(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() options := tfe.WorkspaceUpdateOptions{ WorkingDirectory: tfe.String("terraform"), } // Configure the workspace to use a custom working directory. _, err := b.client.Workspaces.Update(context.Background(), b.organization, b.WorkspaceMapping.Name, options) if err != nil { t.Fatalf("error configuring working directory: %v", err) } wd, err := os.Getwd() if err != nil { t.Fatalf("error getting current working directory: %v", err) } // We need to change into the configuration directory to make sure // the logic to upload the correct slug is working as expected. if err := os.Chdir("./testdata/plan-with-working-directory/terraform"); err != nil { t.Fatalf("error changing directory: %v", err) } defer os.Chdir(wd) // Make sure we change back again when were done. // For this test we need to give our current directory instead of the // full path to the configuration as we already changed directories. op, configCleanup, done := testOperationPlan(t, ".") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatalf("expected a non-empty plan") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("expected TFC header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } func TestCloud_planCostEstimation(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan-cost-estimation") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatalf("expected a non-empty plan") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("expected TFC header in output: %s", output) } if !strings.Contains(output, "Resources: 1 of 1 estimated") { t.Fatalf("expected cost estimate result in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } func TestCloud_planPolicyPass(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-passed") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result != backend.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { t.Fatalf("expected a non-empty plan") } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("expected TFC header in output: %s", output) } if !strings.Contains(output, "Sentinel Result: true") { t.Fatalf("expected policy check result in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } func TestCloud_planPolicyHardFail(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-hard-failed") defer configCleanup() op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() viewOutput := done(t) if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := viewOutput.Stderr() if !strings.Contains(errOutput, "hard failed") { t.Fatalf("expected a policy check error, got: %v", errOutput) } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("expected TFC header in output: %s", output) } if !strings.Contains(output, "Sentinel Result: false") { t.Fatalf("expected policy check result in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } func TestCloud_planPolicySoftFail(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-soft-failed") defer configCleanup() op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() viewOutput := done(t) if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { t.Fatalf("expected plan to be empty") } errOutput := viewOutput.Stderr() if !strings.Contains(errOutput, "soft failed") { t.Fatalf("expected a policy check error, got: %v", errOutput) } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("expected TFC header in output: %s", output) } if !strings.Contains(output, "Sentinel Result: false") { t.Fatalf("expected policy check result in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } func TestCloud_planWithRemoteError(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-error") defer configCleanup() defer done(t) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) if err != nil { t.Fatalf("error starting operation: %v", err) } <-run.Done() if run.Result == backend.OperationSuccess { t.Fatal("expected plan operation to fail") } if run.Result.ExitStatus() != 1 { t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus()) } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Running plan in Terraform Cloud") { t.Fatalf("expected TFC header in output: %s", output) } if !strings.Contains(output, "null_resource.foo: 1 error") { t.Fatalf("expected plan error in output: %s", output) } } func TestCloud_planOtherError(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() defer done(t) op.Workspace = "network-error" // custom error response in backend_mock.go _, err := b.Operation(context.Background(), op) if err == nil { t.Errorf("expected error, got success") } if !strings.Contains(err.Error(), "Terraform Cloud returned an unexpected error:\n\nI'm a little teacup") { t.Fatalf("expected error message, got: %s", err.Error()) } }