| package remote |
| |
| 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/cloud" |
| "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 TestRemote_planBasic(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| defer done(t) |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("expected remote backend 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(backend.DefaultStateName) |
| // 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 TestRemote_planCanceled(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| defer done(t) |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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(backend.DefaultStateName) |
| // 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 TestRemote_planLongLine(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan-long-line") |
| defer configCleanup() |
| defer done(t) |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("expected remote backend 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 TestRemote_planWithoutPermissions(t *testing.T) { |
| b, bCleanup := testBackendNoDefault(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.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 TestRemote_planWithParallelism(t *testing.T) { |
| b, bCleanup := testBackendDefault(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 = backend.DefaultStateName |
| |
| 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 TestRemote_planWithPlan(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| |
| op.PlanFile = &planfile.Reader{} |
| op.Workspace = backend.DefaultStateName |
| |
| 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 TestRemote_planWithPath(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| |
| op.PlanOutPath = "./testdata/plan" |
| op.Workspace = backend.DefaultStateName |
| |
| 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 TestRemote_planWithoutRefresh(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| defer done(t) |
| |
| op.PlanRefresh = false |
| op.Workspace = backend.DefaultStateName |
| |
| 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.(*cloud.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 TestRemote_planWithoutRefreshIncompatibleAPIVersion(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| |
| b.client.SetFakeRemoteAPIVersion("2.3") |
| |
| op.PlanRefresh = false |
| op.Workspace = backend.DefaultStateName |
| |
| 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, "Planning without refresh is not supported") { |
| t.Fatalf("expected not supported error, got: %v", errOutput) |
| } |
| } |
| |
| func TestRemote_planWithRefreshOnly(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| defer done(t) |
| |
| op.PlanMode = plans.RefreshOnlyMode |
| op.Workspace = backend.DefaultStateName |
| |
| 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.(*cloud.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 TestRemote_planWithRefreshOnlyIncompatibleAPIVersion(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| |
| b.client.SetFakeRemoteAPIVersion("2.3") |
| |
| op.PlanMode = plans.RefreshOnlyMode |
| op.Workspace = backend.DefaultStateName |
| |
| 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, "Refresh-only mode is not supported") { |
| t.Fatalf("expected not supported error, got: %v", errOutput) |
| } |
| } |
| |
| func TestRemote_planWithTarget(t *testing.T) { |
| b, bCleanup := testBackendDefault(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.(*cloud.MockRuns).ModifyNewRun = func(client *cloud.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 = backend.DefaultStateName |
| |
| 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.(*cloud.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 TestRemote_planWithTargetIncompatibleAPIVersion(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| |
| // Set the tfe client's RemoteAPIVersion to an empty string, to mimic |
| // API versions prior to 2.3. |
| b.client.SetFakeRemoteAPIVersion("") |
| |
| addr, _ := addrs.ParseAbsResourceStr("null_resource.foo") |
| |
| op.Targets = []addrs.Targetable{addr} |
| op.Workspace = backend.DefaultStateName |
| |
| 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, "Resource targeting is not supported") { |
| t.Fatalf("expected a targeting error, got: %v", errOutput) |
| } |
| } |
| |
| func TestRemote_planWithReplace(t *testing.T) { |
| b, bCleanup := testBackendDefault(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 = backend.DefaultStateName |
| |
| 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.(*cloud.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 TestRemote_planWithReplaceIncompatibleAPIVersion(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| |
| b.client.SetFakeRemoteAPIVersion("2.3") |
| |
| addr, _ := addrs.ParseAbsResourceInstanceStr("null_resource.foo") |
| |
| op.ForceReplace = []addrs.AbsResourceInstance{addr} |
| op.Workspace = backend.DefaultStateName |
| |
| 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, "Planning resource replacements is not supported") { |
| t.Fatalf("expected not supported error, got: %v", errOutput) |
| } |
| } |
| |
| func TestRemote_planWithVariables(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan-variables") |
| defer configCleanup() |
| |
| op.Variables = testVariables(terraform.ValueFromCLIArg, "foo", "bar") |
| op.Workspace = backend.DefaultStateName |
| |
| 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, "variables are currently not supported") { |
| t.Fatalf("expected a variables error, got: %v", errOutput) |
| } |
| } |
| |
| func TestRemote_planNoConfig(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/empty") |
| defer configCleanup() |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 TestRemote_planNoChanges(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan-no-changes") |
| defer configCleanup() |
| defer done(t) |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 TestRemote_planForceLocal(t *testing.T) { |
| // Set TF_FORCE_LOCAL_BACKEND so the remote 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 := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| defer done(t) |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("unexpected remote backend 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 TestRemote_planWithoutOperationsEntitlement(t *testing.T) { |
| b, bCleanup := testBackendNoOperations(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| defer done(t) |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("unexpected remote backend 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 TestRemote_planWorkspaceWithoutOperations(t *testing.T) { |
| b, bCleanup := testBackendNoDefault(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.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 the remote backend") { |
| t.Fatalf("unexpected remote backend 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 TestRemote_planLockTimeout(t *testing.T) { |
| b, bCleanup := testBackendDefault(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.workspace) |
| 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 = backend.DefaultStateName |
| |
| _, 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 the remote backend") { |
| t.Fatalf("expected remote backend 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 TestRemote_planDestroy(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan") |
| defer configCleanup() |
| defer done(t) |
| |
| op.PlanMode = plans.DestroyMode |
| op.Workspace = backend.DefaultStateName |
| |
| 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 TestRemote_planDestroyNoConfig(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/empty") |
| defer configCleanup() |
| defer done(t) |
| |
| op.PlanMode = plans.DestroyMode |
| op.Workspace = backend.DefaultStateName |
| |
| 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 TestRemote_planWithWorkingDirectory(t *testing.T) { |
| b, bCleanup := testBackendDefault(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.workspace, 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 = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("expected remote backend 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 TestRemote_planWithWorkingDirectoryFromCurrentPath(t *testing.T) { |
| b, bCleanup := testBackendDefault(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.workspace, 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 = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("expected remote backend 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 TestRemote_planCostEstimation(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan-cost-estimation") |
| defer configCleanup() |
| defer done(t) |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("expected remote backend 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 TestRemote_planPolicyPass(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-passed") |
| defer configCleanup() |
| defer done(t) |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("expected remote backend 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 TestRemote_planPolicyHardFail(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-hard-failed") |
| defer configCleanup() |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("expected remote backend 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 TestRemote_planPolicySoftFail(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan-policy-soft-failed") |
| defer configCleanup() |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("expected remote backend 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 TestRemote_planWithRemoteError(t *testing.T) { |
| b, bCleanup := testBackendDefault(t) |
| defer bCleanup() |
| |
| op, configCleanup, done := testOperationPlan(t, "./testdata/plan-with-error") |
| defer configCleanup() |
| defer done(t) |
| |
| op.Workspace = backend.DefaultStateName |
| |
| 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 the remote backend") { |
| t.Fatalf("expected remote backend header in output: %s", output) |
| } |
| if !strings.Contains(output, "null_resource.foo: 1 error") { |
| t.Fatalf("expected plan error in output: %s", output) |
| } |
| } |
| |
| func TestRemote_planOtherError(t *testing.T) { |
| b, bCleanup := testBackendDefault(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(), |
| "the configured \"remote\" backend encountered an unexpected error:\n\nI'm a little teacup") { |
| t.Fatalf("expected error message, got: %s", err.Error()) |
| } |
| } |