From ab5fec9aecaf40f900fcf61d46ed5b35ccec560f Mon Sep 17 00:00:00 2001 From: CJ Horton Date: Thu, 13 May 2021 21:28:16 -0700 Subject: [PATCH] backend/remote: test new options and modes Tests for -refresh=false, -refresh-only, and -replace, as well as tests to ensure we error appropriately on mismatched API versions. --- internal/backend/remote/backend_apply_test.go | 188 +++++++++++++++++- internal/backend/remote/backend_mock.go | 9 + internal/backend/remote/backend_plan_test.go | 188 +++++++++++++++++- internal/backend/remote/testing.go | 2 +- 4 files changed, 382 insertions(+), 5 deletions(-) diff --git a/internal/backend/remote/backend_apply_test.go b/internal/backend/remote/backend_apply_test.go index 5cbd4c4d6..8ba2aa271 100644 --- a/internal/backend/remote/backend_apply_test.go +++ b/internal/backend/remote/backend_apply_test.go @@ -279,6 +279,45 @@ func TestRemote_applyWithoutRefresh(t *testing.T) { op, configCleanup, done := testOperationApply(t, "./testdata/apply") 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.Fatalf("expected plan to be non-empty") + } + + // 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 TestRemote_applyWithoutRefreshIncompatibleAPIVersion(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + op, configCleanup, done := testOperationApply(t, "./testdata/apply") + defer configCleanup() + + b.client.SetFakeRemoteAPIVersion("2.3") op.PlanRefresh = false op.Workspace = backend.DefaultStateName @@ -293,10 +332,82 @@ func TestRemote_applyWithoutRefresh(t *testing.T) { if run.Result == backend.OperationSuccess { t.Fatal("expected apply operation to fail") } + if !run.PlanEmpty { + t.Fatalf("expected plan to be empty") + } errOutput := output.Stderr() - if !strings.Contains(errOutput, "refresh is currently not supported") { - t.Fatalf("expected a refresh error, got: %v", errOutput) + if !strings.Contains(errOutput, "Planning without refresh is not supported") { + t.Fatalf("expected a not supported error, got: %v", errOutput) + } +} + +func TestRemote_applyWithRefreshOnly(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + op, configCleanup, done := testOperationApply(t, "./testdata/apply") + 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.Fatalf("expected plan to be non-empty") + } + + // 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 TestRemote_applyWithRefreshOnlyIncompatibleAPIVersion(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + op, configCleanup, done := testOperationApply(t, "./testdata/apply") + 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 apply 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 a not supported error, got: %v", errOutput) } } @@ -375,6 +486,79 @@ func TestRemote_applyWithTargetIncompatibleAPIVersion(t *testing.T) { } } +func TestRemote_applyWithReplace(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + op, configCleanup, done := testOperationApply(t, "./testdata/apply") + 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.(*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_applyWithReplaceIncompatibleAPIVersion(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + op, configCleanup, done := testOperationApply(t, "./testdata/apply") + 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 apply 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 a not supported error, got: %v", errOutput) + } +} + func TestRemote_applyWithVariables(t *testing.T) { b, bCleanup := testBackendDefault(t) defer bCleanup() diff --git a/internal/backend/remote/backend_mock.go b/internal/backend/remote/backend_mock.go index 5ade80e81..abf150f7c 100644 --- a/internal/backend/remote/backend_mock.go +++ b/internal/backend/remote/backend_mock.go @@ -788,6 +788,7 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t HasChanges: false, Permissions: &tfe.RunPermissions{}, Plan: p, + ReplaceAddrs: options.ReplaceAddrs, Status: tfe.RunPending, TargetAddrs: options.TargetAddrs, } @@ -804,6 +805,14 @@ func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t r.IsDestroy = *options.IsDestroy } + if options.Refresh != nil { + r.Refresh = *options.Refresh + } + + if options.RefreshOnly != nil { + r.RefreshOnly = *options.RefreshOnly + } + w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID] if !ok { return nil, tfe.ErrResourceNotFound diff --git a/internal/backend/remote/backend_plan_test.go b/internal/backend/remote/backend_plan_test.go index b274963f4..4d5676516 100644 --- a/internal/backend/remote/backend_plan_test.go +++ b/internal/backend/remote/backend_plan_test.go @@ -284,6 +284,45 @@ func TestRemote_planWithoutRefresh(t *testing.T) { 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.(*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 @@ -298,10 +337,82 @@ func TestRemote_planWithoutRefresh(t *testing.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 is currently not supported") { - t.Fatalf("expected a refresh error, got: %v", errOutput) + 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.(*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) } } @@ -414,6 +525,79 @@ func TestRemote_planWithTargetIncompatibleAPIVersion(t *testing.T) { } } +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.(*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() diff --git a/internal/backend/remote/testing.go b/internal/backend/remote/testing.go index 24a768c67..f3c66941a 100644 --- a/internal/backend/remote/testing.go +++ b/internal/backend/remote/testing.go @@ -200,7 +200,7 @@ func testServer(t *testing.T) *httptest.Server { // Respond to pings to get the API version header. mux.HandleFunc("/api/v2/ping", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - w.Header().Set("TFP-API-Version", "2.3") + w.Header().Set("TFP-API-Version", "2.4") }) // Respond to the initial query to read the hashicorp org entitlements.