From 2bd1040bbd001a9af91e188301a8ebeb664f63b3 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Wed, 26 Sep 2018 17:27:12 +0200 Subject: [PATCH] backend/remote: extend mocks and add apply tests --- backend/remote/backend.go | 1 - backend/remote/backend_apply.go | 4 +- backend/remote/backend_apply_test.go | 408 ++++++++++++++++++ backend/remote/backend_mock.go | 222 +++++++++- .../test-fixtures/apply-destroy/apply.log | 12 + .../test-fixtures/apply-destroy/main.tf | 1 + .../test-fixtures/apply-destroy/plan.log | 22 + .../test-fixtures/apply-no-changes/main.tf | 1 + .../test-fixtures/apply-no-changes/plan.log | 17 + backend/remote/test-fixtures/apply/apply.log | 12 + backend/remote/test-fixtures/apply/main.tf | 1 + .../terraform/output.log => apply/plan.log} | 8 - .../test-fixtures/apply/policy-passed.log | 16 + .../terraform/plan.log} | 8 - backend/remote/test-fixtures/plan/plan.log | 21 + backend/remote/testing.go | 5 + 16 files changed, 733 insertions(+), 26 deletions(-) create mode 100644 backend/remote/backend_apply_test.go create mode 100644 backend/remote/test-fixtures/apply-destroy/apply.log create mode 100644 backend/remote/test-fixtures/apply-destroy/main.tf create mode 100644 backend/remote/test-fixtures/apply-destroy/plan.log create mode 100644 backend/remote/test-fixtures/apply-no-changes/main.tf create mode 100644 backend/remote/test-fixtures/apply-no-changes/plan.log create mode 100644 backend/remote/test-fixtures/apply/apply.log create mode 100644 backend/remote/test-fixtures/apply/main.tf rename backend/remote/test-fixtures/{plan-with-working-directory/terraform/output.log => apply/plan.log} (67%) create mode 100644 backend/remote/test-fixtures/apply/policy-passed.log rename backend/remote/test-fixtures/{plan/output.log => plan-with-working-directory/terraform/plan.log} (67%) create mode 100644 backend/remote/test-fixtures/plan/plan.log diff --git a/backend/remote/backend.go b/backend/remote/backend.go index 1eb0e2b45..26c27159b 100644 --- a/backend/remote/backend.go +++ b/backend/remote/backend.go @@ -405,7 +405,6 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend "\n\nThe \"remote\" backend does not support the %q operation.\n"+ "Please use the remote backend web UI for running this operation:\n"+ "https://%s/app/%s/%s", op.Type, b.hostname, b.organization, op.Workspace) - // return nil, backend.ErrOperationNotSupported } // Lock diff --git a/backend/remote/backend_apply.go b/backend/remote/backend_apply.go index 41740cc5e..c17d413f9 100644 --- a/backend/remote/backend_apply.go +++ b/backend/remote/backend_apply.go @@ -91,9 +91,9 @@ func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operati fmt.Sprint(applyErrNoApplyRights, b.hostname, b.organization, op.Workspace))) } - hasUI := op.UIOut != nil && op.UIIn != nil + hasUI := op.UIIn != nil && op.UIOut != nil mustConfirm := hasUI && - (op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove) + ((op.Destroy && (!op.DestroyForce && !op.AutoApprove)) || (!op.Destroy && !op.AutoApprove)) if mustConfirm { opts := &terraform.InputOpts{Id: "approve"} diff --git a/backend/remote/backend_apply_test.go b/backend/remote/backend_apply_test.go new file mode 100644 index 000000000..245666307 --- /dev/null +++ b/backend/remote/backend_apply_test.go @@ -0,0 +1,408 @@ +package remote + +import ( + "context" + "os" + "os/signal" + "strings" + "syscall" + "testing" + "time" + + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/config/module" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" +) + +func testOperationApply() *backend.Operation { + return &backend.Operation{ + Type: backend.OperationTypeApply, + } +} + +func TestRemote_applyBasic(t *testing.T) { + b := testBackendDefault(t) + + mod, modCleanup := module.TestTree(t, "./test-fixtures/apply") + defer modCleanup() + + input := testInput(t, map[string]string{ + "approve": "yes", + }) + + op := testOperationApply() + op.Module = mod + op.UIIn = input + op.UIOut = b.CLI + 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.Err != nil { + t.Fatalf("error running operation: %v", run.Err) + } + + if len(input.answers) > 0 { + t.Fatalf("expected no unused answers, got: %v", input.answers) + } + + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { + t.Fatalf("missing plan summery in output: %s", output) + } + if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { + t.Fatalf("missing apply summery in output: %s", output) + } +} + +func TestRemote_applyWithVCS(t *testing.T) { + b := testBackendNoDefault(t) + + // Create the named workspace with a VCS. + _, err := b.client.Workspaces.Create( + context.Background(), + b.organization, + tfe.WorkspaceCreateOptions{ + Name: tfe.String(b.prefix + "prod"), + VCSRepo: &tfe.VCSRepoOptions{}, + }, + ) + if err != nil { + t.Fatalf("error creating named workspace: %v", err) + } + + mod, modCleanup := module.TestTree(t, "./test-fixtures/apply") + defer modCleanup() + + op := testOperationApply() + op.Module = mod + op.Workspace = "prod" + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + <-run.Done() + + if run.Err == nil { + t.Fatalf("expected a apply error, got: %v", run.Err) + } + if !strings.Contains(run.Err.Error(), "not allowed for workspaces with a VCS") { + t.Fatalf("expected a VCS error, got: %v", run.Err) + } +} + +func TestRemote_applyWithPlan(t *testing.T) { + b := testBackendDefault(t) + + mod, modCleanup := module.TestTree(t, "./test-fixtures/apply") + defer modCleanup() + + op := testOperationApply() + op.Module = mod + op.Plan = &terraform.Plan{} + 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.Err == nil { + t.Fatalf("expected a apply error, got: %v", run.Err) + } + if !strings.Contains(run.Err.Error(), "saved plan is currently not supported") { + t.Fatalf("expected a saved plan error, got: %v", run.Err) + } +} + +func TestRemote_applyWithTarget(t *testing.T) { + b := testBackendDefault(t) + + mod, modCleanup := module.TestTree(t, "./test-fixtures/apply") + defer modCleanup() + + op := testOperationApply() + op.Module = mod + op.Targets = []string{"null_resource.foo"} + 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.Err == nil { + t.Fatalf("expected a apply error, got: %v", run.Err) + } + if !strings.Contains(run.Err.Error(), "targeting is currently not supported") { + t.Fatalf("expected a targeting error, got: %v", run.Err) + } +} + +func TestRemote_applyNoConfig(t *testing.T) { + b := testBackendDefault(t) + + op := testOperationApply() + op.Module = nil + 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.Err == nil { + t.Fatalf("expected a apply error, got: %v", run.Err) + } + if !strings.Contains(run.Err.Error(), "configuration files found") { + t.Fatalf("expected configuration files error, got: %v", run.Err) + } +} + +func TestRemote_applyNoChanges(t *testing.T) { + b := testBackendDefault(t) + + mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-no-changes") + defer modCleanup() + + op := testOperationApply() + op.Module = mod + 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.Err != nil { + t.Fatalf("error running operation: %v", run.Err) + } + + 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 summery: %s", output) + } +} + +func TestRemote_applyNoApprove(t *testing.T) { + b := testBackendDefault(t) + + mod, modCleanup := module.TestTree(t, "./test-fixtures/apply") + defer modCleanup() + + input := testInput(t, map[string]string{ + "approve": "no", + }) + + op := testOperationApply() + op.Module = mod + op.UIIn = input + op.UIOut = b.CLI + 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.Err == nil { + t.Fatalf("expected a apply error, got: %v", run.Err) + } + if !strings.Contains(run.Err.Error(), "Apply discarded") { + t.Fatalf("expected a apply discarded error, got: %v", run.Err) + } + if len(input.answers) > 0 { + t.Fatalf("expected no unused answers, got: %v", input.answers) + } +} + +func TestRemote_applyAutoApprove(t *testing.T) { + b := testBackendDefault(t) + + mod, modCleanup := module.TestTree(t, "./test-fixtures/apply") + defer modCleanup() + + input := testInput(t, map[string]string{ + "approve": "no", + }) + + op := testOperationApply() + op.AutoApprove = true + op.Module = mod + op.UIIn = input + op.UIOut = b.CLI + 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.Err != nil { + t.Fatalf("error running operation: %v", run.Err) + } + + if len(input.answers) != 1 { + t.Fatalf("expected an unused answer, got: %v", input.answers) + } + + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { + t.Fatalf("missing plan summery in output: %s", output) + } + if !strings.Contains(output, "1 added, 0 changed, 0 destroyed") { + t.Fatalf("missing apply summery in output: %s", output) + } +} + +func TestRemote_applyLockTimeout(t *testing.T) { + b := testBackendDefault(t) + 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) + } + + mod, modCleanup := module.TestTree(t, "./test-fixtures/apply") + defer modCleanup() + + input := testInput(t, map[string]string{ + "approve": "yes", + }) + + op := testOperationApply() + op.StateLockTimeout = 5 * time.Second + op.Module = mod + 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(10 * time.Second): + t.Fatalf("expected lock timeout after 5 seconds, waited 10 seconds") + } + + if len(input.answers) != 1 { + t.Fatalf("expected an unused answer, got: %v", input.answers) + } + + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if !strings.Contains(output, "Lock timeout exceeded") { + t.Fatalf("missing lock timout error in output: %s", output) + } + if strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { + t.Fatalf("unexpected plan summery in output: %s", output) + } + if strings.Contains(output, "1 added, 0 changed, 0 destroyed") { + t.Fatalf("unexpected apply summery in output: %s", output) + } +} + +func TestRemote_applyDestroy(t *testing.T) { + b := testBackendDefault(t) + + mod, modCleanup := module.TestTree(t, "./test-fixtures/apply-destroy") + defer modCleanup() + + input := testInput(t, map[string]string{ + "approve": "yes", + }) + + op := testOperationApply() + op.Destroy = true + op.Module = mod + op.UIIn = input + op.UIOut = b.CLI + 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.Err != nil { + t.Fatalf("error running operation: %v", run.Err) + } + + if len(input.answers) > 0 { + t.Fatalf("expected no unused answers, got: %v", input.answers) + } + + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if !strings.Contains(output, "0 to add, 0 to change, 1 to destroy") { + t.Fatalf("missing plan summery in output: %s", output) + } + if !strings.Contains(output, "0 added, 0 changed, 1 destroyed") { + t.Fatalf("missing apply summery in output: %s", output) + } +} + +func TestRemote_applyDestroyNoConfig(t *testing.T) { + b := testBackendDefault(t) + + input := testInput(t, map[string]string{ + "approve": "yes", + }) + + op := testOperationApply() + op.Destroy = true + op.Module = nil + op.UIIn = input + op.UIOut = b.CLI + 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.Err != nil { + t.Fatalf("unexpected apply error: %v", run.Err) + } + + if len(input.answers) > 0 { + t.Fatalf("expected no unused answers, got: %v", input.answers) + } +} diff --git a/backend/remote/backend_mock.go b/backend/remote/backend_mock.go index 5645e2f34..dc3aa3098 100644 --- a/backend/remote/backend_mock.go +++ b/backend/remote/backend_mock.go @@ -12,11 +12,14 @@ import ( "os" "path/filepath" "strings" + "time" tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/terraform" ) type mockClient struct { + Applies *mockApplies ConfigurationVersions *mockConfigurationVersions Organizations *mockOrganizations Plans *mockPlans @@ -27,6 +30,7 @@ type mockClient struct { func newMockClient() *mockClient { c := &mockClient{} + c.Applies = newMockApplies(c) c.ConfigurationVersions = newMockConfigurationVersions(c) c.Organizations = newMockOrganizations(c) c.Plans = newMockPlans(c) @@ -36,6 +40,106 @@ func newMockClient() *mockClient { return c } +type mockApplies struct { + client *mockClient + applies map[string]*tfe.Apply + logs map[string]string +} + +func newMockApplies(client *mockClient) *mockApplies { + return &mockApplies{ + client: client, + applies: make(map[string]*tfe.Apply), + logs: make(map[string]string), + } +} + +// create is a helper function to create a mock apply that uses the configured +// working directory to find the logfile. This enables us to test if we are +// using the +func (m *mockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) { + c, ok := m.client.ConfigurationVersions.configVersions[cvID] + if !ok { + return nil, tfe.ErrResourceNotFound + } + if c.Speculative { + // Speculative means its plan-only so we don't create a Apply. + return nil, nil + } + + id := generateID("apply-") + url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) + + a := &tfe.Apply{ + ID: id, + LogReadURL: url, + Status: tfe.ApplyPending, + } + + w, ok := m.client.Workspaces.workspaceIDs[workspaceID] + if !ok { + return nil, tfe.ErrResourceNotFound + } + + m.logs[url] = filepath.Join( + m.client.ConfigurationVersions.uploadPaths[cvID], + w.WorkingDirectory, + "apply.log", + ) + m.applies[a.ID] = a + + return a, nil +} + +func (m *mockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, error) { + a, ok := m.applies[applyID] + if !ok { + return nil, tfe.ErrResourceNotFound + } + // Together with the mockLogReader this allows testing queued runs. + if a.Status == tfe.ApplyRunning { + a.Status = tfe.ApplyFinished + } + return a, nil +} + +func (m *mockApplies) Logs(ctx context.Context, applyID string) (io.Reader, error) { + a, err := m.Read(ctx, applyID) + if err != nil { + return nil, err + } + + logfile, ok := m.logs[a.LogReadURL] + if !ok { + return nil, tfe.ErrResourceNotFound + } + + if _, err := os.Stat(logfile); os.IsNotExist(err) { + return bytes.NewBufferString("logfile does not exist"), nil + } + + logs, err := ioutil.ReadFile(logfile) + if err != nil { + return nil, err + } + + done := func() (bool, error) { + a, err := m.Read(ctx, applyID) + if err != nil { + return false, err + } + if a.Status != tfe.ApplyFinished { + return false, nil + } + return true, nil + } + + return &mockLogReader{ + done: done, + logs: bytes.NewBuffer(logs), + }, nil +} + type mockConfigurationVersions struct { client *mockClient configVersions map[string]*tfe.ConfigurationVersion @@ -103,6 +207,20 @@ func (m *mockConfigurationVersions) Upload(ctx context.Context, url, path string return nil } +// mockInput is a mock implementation of terraform.UIInput. +type mockInput struct { + answers map[string]string +} + +func (m *mockInput) Input(opts *terraform.InputOpts) (string, error) { + v, ok := m.answers[opts.Id] + if !ok { + return "", fmt.Errorf("unexpected input request in test: %s", opts.Id) + } + delete(m.answers, opts.Id) + return v, nil +} + type mockOrganizations struct { client *mockClient organizations map[string]*tfe.Organization @@ -132,6 +250,32 @@ func (m *mockOrganizations) List(ctx context.Context, options tfe.OrganizationLi return orgl, nil } +// mockLogReader is a mock logreader that enables testing queued runs. +type mockLogReader struct { + done func() (bool, error) + logs *bytes.Buffer +} + +func (m *mockLogReader) Read(l []byte) (int, error) { + for { + if written, err := m.read(l); err != io.ErrNoProgress { + return written, err + } + time.Sleep(500 * time.Millisecond) + } +} + +func (m *mockLogReader) read(l []byte) (int, error) { + done, err := m.done() + if err != nil { + return 0, err + } + if !done { + return 0, io.ErrNoProgress + } + return m.logs.Read(l) +} + func (m *mockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) { org := &tfe.Organization{Name: *options.Name} m.organizations[org.Name] = org @@ -196,7 +340,7 @@ func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) { m.logs[url] = filepath.Join( m.client.ConfigurationVersions.uploadPaths[cvID], w.WorkingDirectory, - "output.log", + "plan.log", ) m.plans[p.ID] = p @@ -208,7 +352,10 @@ func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) if !ok { return nil, tfe.ErrResourceNotFound } - p.Status = tfe.PlanFinished + // Together with the mockLogReader this allows testing queued runs. + if p.Status == tfe.PlanRunning { + p.Status = tfe.PlanFinished + } return p, nil } @@ -232,7 +379,21 @@ func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) return nil, err } - return bytes.NewBuffer(logs), nil + done := func() (bool, error) { + p, err := m.Read(ctx, planID) + if err != nil { + return false, err + } + if p.Status != tfe.PlanFinished { + return false, nil + } + return true, nil + } + + return &mockLogReader{ + done: done, + logs: bytes.NewBuffer(logs), + }, nil } type mockRuns struct { @@ -272,15 +433,35 @@ func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.Run } func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) { + a, err := m.client.Applies.create(options.ConfigurationVersion.ID, options.Workspace.ID) + if err != nil { + return nil, err + } + p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID) if err != nil { return nil, err } r := &tfe.Run{ - ID: generateID("run-"), - Plan: p, - Status: tfe.RunPending, + ID: generateID("run-"), + Actions: &tfe.RunActions{}, + Apply: a, + HasChanges: true, + Permissions: &tfe.RunPermissions{}, + Plan: p, + Status: tfe.RunPending, + } + + if options.IsDestroy != nil { + r.IsDestroy = *options.IsDestroy + } + + logs, _ := ioutil.ReadFile(m.client.Plans.logs[p.LogReadURL]) + if r.IsDestroy || !bytes.Contains(logs, []byte("No changes. Infrastructure is up-to-date.")) { + r.Actions.IsConfirmable = true + r.HasChanges = true + r.Permissions.CanApply = true } m.runs[r.ID] = r @@ -294,11 +475,35 @@ func (m *mockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) { if !ok { return nil, tfe.ErrResourceNotFound } + + pending := false + for _, r := range m.runs { + if r.ID != runID && r.Status == tfe.RunPending { + pending = true + break + } + } + + if !pending { + // Only update the status if there are no other pending runs. + r.Status = tfe.RunPlanning + r.Plan.Status = tfe.PlanRunning + } + return r, nil } func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error { - panic("not implemented") + r, ok := m.runs[runID] + if !ok { + return tfe.ErrResourceNotFound + } + if r.Status != tfe.RunPending { + // Only update the status if the run is not pending anymore. + r.Status = tfe.RunApplying + r.Apply.Status = tfe.ApplyRunning + } + return nil } func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error { @@ -480,6 +685,9 @@ func (m *mockWorkspaces) Create(ctx context.Context, organization string, option ID: generateID("ws-"), Name: *options.Name, } + if options.VCSRepo != nil { + w.VCSRepo = &tfe.VCSRepo{} + } m.workspaceIDs[w.ID] = w m.workspaceNames[w.Name] = w return w, nil diff --git a/backend/remote/test-fixtures/apply-destroy/apply.log b/backend/remote/test-fixtures/apply-destroy/apply.log new file mode 100644 index 000000000..6f9fb42e4 --- /dev/null +++ b/backend/remote/test-fixtures/apply-destroy/apply.log @@ -0,0 +1,12 @@ +------------------------------------------------------------------------ + +Do you really want to destroy all resources in workspace "my-app-dev"? + Terraform will destroy all your managed infrastructure, as shown above. + There is no undo. Only 'yes' will be accepted to confirm. + + Enter a value: yes + +null_resource.hello: Destroying... (ID: 8657651096157629581) +null_resource.hello: Destruction complete after 0s + +Apply complete! Resources: 0 added, 0 changed, 1 destroyed. diff --git a/backend/remote/test-fixtures/apply-destroy/main.tf b/backend/remote/test-fixtures/apply-destroy/main.tf new file mode 100644 index 000000000..3911a2a9b --- /dev/null +++ b/backend/remote/test-fixtures/apply-destroy/main.tf @@ -0,0 +1 @@ +resource "null_resource" "foo" {} diff --git a/backend/remote/test-fixtures/apply-destroy/plan.log b/backend/remote/test-fixtures/apply-destroy/plan.log new file mode 100644 index 000000000..1d38d4168 --- /dev/null +++ b/backend/remote/test-fixtures/apply-destroy/plan.log @@ -0,0 +1,22 @@ +Terraform v0.11.7 + +Configuring remote state backend... +Initializing Terraform configuration... +Refreshing Terraform state in-memory prior to plan... +The refreshed state will be used to calculate this plan, but will not be +persisted to local or remote state storage. + +null_resource.hello: Refreshing state... (ID: 8657651096157629581) + +------------------------------------------------------------------------ + +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + - destroy + +Terraform will perform the following actions: + + - null_resource.hello + + +Plan: 0 to add, 0 to change, 1 to destroy. diff --git a/backend/remote/test-fixtures/apply-no-changes/main.tf b/backend/remote/test-fixtures/apply-no-changes/main.tf new file mode 100644 index 000000000..3911a2a9b --- /dev/null +++ b/backend/remote/test-fixtures/apply-no-changes/main.tf @@ -0,0 +1 @@ +resource "null_resource" "foo" {} diff --git a/backend/remote/test-fixtures/apply-no-changes/plan.log b/backend/remote/test-fixtures/apply-no-changes/plan.log new file mode 100644 index 000000000..704168151 --- /dev/null +++ b/backend/remote/test-fixtures/apply-no-changes/plan.log @@ -0,0 +1,17 @@ +Terraform v0.11.7 + +Configuring remote state backend... +Initializing Terraform configuration... +Refreshing Terraform state in-memory prior to plan... +The refreshed state will be used to calculate this plan, but will not be +persisted to local or remote state storage. + +null_resource.hello: Refreshing state... (ID: 8657651096157629581) + +------------------------------------------------------------------------ + +No changes. Infrastructure is up-to-date. + +This means that Terraform did not detect any differences between your +configuration and real physical resources that exist. As a result, no +actions need to be performed. diff --git a/backend/remote/test-fixtures/apply/apply.log b/backend/remote/test-fixtures/apply/apply.log new file mode 100644 index 000000000..0665a7a30 --- /dev/null +++ b/backend/remote/test-fixtures/apply/apply.log @@ -0,0 +1,12 @@ +------------------------------------------------------------------------ + +Do you want to perform these actions in workspace "my-workspace-name"? + Terraform will perform the actions described above. + Only 'yes' will be accepted to approve. + + Enter a value: yes + +null_resource.hello: Creating... +null_resource.hello: Creation complete after 0s (ID: 8657651096157629581) + +Apply complete! Resources: 1 added, 0 changed, 0 destroyed. diff --git a/backend/remote/test-fixtures/apply/main.tf b/backend/remote/test-fixtures/apply/main.tf new file mode 100644 index 000000000..3911a2a9b --- /dev/null +++ b/backend/remote/test-fixtures/apply/main.tf @@ -0,0 +1 @@ +resource "null_resource" "foo" {} diff --git a/backend/remote/test-fixtures/plan-with-working-directory/terraform/output.log b/backend/remote/test-fixtures/apply/plan.log similarity index 67% rename from backend/remote/test-fixtures/plan-with-working-directory/terraform/output.log rename to backend/remote/test-fixtures/apply/plan.log index d9fe98082..5849e5759 100644 --- a/backend/remote/test-fixtures/plan-with-working-directory/terraform/output.log +++ b/backend/remote/test-fixtures/apply/plan.log @@ -1,10 +1,3 @@ -Running plan in the remote backend. Output will stream here. Pressing Ctrl-C -will stop streaming the logs, but will not stop the plan running remotely. -To view this plan in a browser, visit: -https://atlas.local/app/demo1/my-app-web/runs/run-cPK6EnfTpqwy6ucU - -Waiting for the plan to start... - Terraform v0.11.7 Configuring remote state backend... @@ -13,7 +6,6 @@ Refreshing Terraform state in-memory prior to plan... The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage. - ------------------------------------------------------------------------ An execution plan has been generated and is shown below. diff --git a/backend/remote/test-fixtures/apply/policy-passed.log b/backend/remote/test-fixtures/apply/policy-passed.log new file mode 100644 index 000000000..c5c112f58 --- /dev/null +++ b/backend/remote/test-fixtures/apply/policy-passed.log @@ -0,0 +1,16 @@ +------------------------------------------------------------------------ + +Organization policy check: + +Sentinel Result: true + +This result means that Sentinel policies returned true and the protected +behavior is allowed by Sentinel policies. + +1 policies evaluated. + +## Policy 1: Passthrough.sentinel (soft-mandatory) + +Result: true + +TRUE - Passthrough.sentinel:1:1 - Rule "main" diff --git a/backend/remote/test-fixtures/plan/output.log b/backend/remote/test-fixtures/plan-with-working-directory/terraform/plan.log similarity index 67% rename from backend/remote/test-fixtures/plan/output.log rename to backend/remote/test-fixtures/plan-with-working-directory/terraform/plan.log index d9fe98082..5849e5759 100644 --- a/backend/remote/test-fixtures/plan/output.log +++ b/backend/remote/test-fixtures/plan-with-working-directory/terraform/plan.log @@ -1,10 +1,3 @@ -Running plan in the remote backend. Output will stream here. Pressing Ctrl-C -will stop streaming the logs, but will not stop the plan running remotely. -To view this plan in a browser, visit: -https://atlas.local/app/demo1/my-app-web/runs/run-cPK6EnfTpqwy6ucU - -Waiting for the plan to start... - Terraform v0.11.7 Configuring remote state backend... @@ -13,7 +6,6 @@ Refreshing Terraform state in-memory prior to plan... The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage. - ------------------------------------------------------------------------ An execution plan has been generated and is shown below. diff --git a/backend/remote/test-fixtures/plan/plan.log b/backend/remote/test-fixtures/plan/plan.log new file mode 100644 index 000000000..5849e5759 --- /dev/null +++ b/backend/remote/test-fixtures/plan/plan.log @@ -0,0 +1,21 @@ +Terraform v0.11.7 + +Configuring remote state backend... +Initializing Terraform configuration... +Refreshing Terraform state in-memory prior to plan... +The refreshed state will be used to calculate this plan, but will not be +persisted to local or remote state storage. + +------------------------------------------------------------------------ + +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + + null_resource.foo + id: + + +Plan: 1 to add, 0 to change, 0 to destroy. diff --git a/backend/remote/testing.go b/backend/remote/testing.go index 7df847a50..999fc6615 100644 --- a/backend/remote/testing.go +++ b/backend/remote/testing.go @@ -28,6 +28,10 @@ var ( }) ) +func testInput(t *testing.T, answers map[string]string) *mockInput { + return &mockInput{answers: answers} +} + func testBackendDefault(t *testing.T) *Remote { c := map[string]interface{}{ "organization": "hashicorp", @@ -74,6 +78,7 @@ func testBackend(t *testing.T, c map[string]interface{}) *Remote { // Replace the services we use with our mock services. b.CLI = cli.NewMockUi() + b.client.Applies = mc.Applies b.client.ConfigurationVersions = mc.ConfigurationVersions b.client.Organizations = mc.Organizations b.client.Plans = mc.Plans