Files
atlantis/server/events/project_command_runner_test.go
Aayush Gupta 58e9b42bbc feat: Use UUIDs to identify log streaming jobs (#2051)
* Add UUID for Log Streaming Job ID (#167)

* Update log handler to close buffered  channels when an operation is complete (#170)

* Add preliminary check before registering new receivers in the log handler (#173)

* Using projectOutputBuffers to check for jobID instead of receiverBuffers (#181)

* Refactor log handler  (#175)

* Reverting go.mod and go.sum

* Fix lint errors

* Fix linting
2022-02-09 13:15:49 -08:00

628 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
package events_test
import (
"errors"
"fmt"
"os"
"testing"
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/runtime"
tmocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/mocks"
eventmocks "github.com/runatlantis/atlantis/server/events/mocks"
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
)
// Test that it runs the expected plan steps.
func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
RegisterMockTestingT(t)
mockInit := mocks.NewMockStepRunner()
mockPlan := mocks.NewMockStepRunner()
mockApply := mocks.NewMockStepRunner()
mockRun := mocks.NewMockCustomStepRunner()
realEnv := runtime.EnvStepRunner{}
mockWorkingDir := mocks.NewMockWorkingDir()
mockLocker := mocks.NewMockProjectLocker()
mockApplyReqHandler := mocks.NewMockApplyRequirement()
runner := events.DefaultProjectCommandRunner{
Locker: mockLocker,
LockURLGenerator: mockURLGenerator{},
InitStepRunner: mockInit,
PlanStepRunner: mockPlan,
ApplyStepRunner: mockApply,
RunStepRunner: mockRun,
EnvStepRunner: &realEnv,
PullApprovedChecker: nil,
WorkingDir: mockWorkingDir,
Webhooks: nil,
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
AggregateApplyRequirements: mockApplyReqHandler,
}
repoDir, cleanup := TempDir(t)
defer cleanup()
When(mockWorkingDir.Clone(
matchers.AnyPtrToLoggingSimpleLogger(),
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
AnyString(),
)).ThenReturn(repoDir, false, nil)
When(mockLocker.TryLock(
matchers.AnyPtrToLoggingSimpleLogger(),
matchers.AnyModelsPullRequest(),
matchers.AnyModelsUser(),
AnyString(),
matchers.AnyModelsProject(),
)).ThenReturn(&events.TryLockResponse{
LockAcquired: true,
LockKey: "lock-key",
}, nil)
expEnvs := map[string]string{
"name": "value",
}
ctx := models.ProjectCommandContext{
Log: logging.NewNoopLogger(t),
Steps: []valid.Step{
{
StepName: "env",
EnvVarName: "name",
EnvVarValue: "value",
},
{
StepName: "run",
},
{
StepName: "apply",
},
{
StepName: "plan",
},
{
StepName: "init",
},
},
Workspace: "default",
RepoRelDir: ".",
}
// Each step will output its step name.
When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil)
When(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("plan", nil)
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", nil)
When(mockRun.Run(ctx, "", repoDir, expEnvs)).ThenReturn("run", nil)
res := runner.Plan(ctx)
Assert(t, res.PlanSuccess != nil, "exp plan success")
Equals(t, "https://lock-key", res.PlanSuccess.LockURL)
Equals(t, "run\napply\nplan\ninit", res.PlanSuccess.TerraformOutput)
expSteps := []string{"run", "apply", "plan", "init", "env"}
for _, step := range expSteps {
switch step {
case "init":
mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
case "plan":
mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
case "apply":
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
case "run":
mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, expEnvs)
}
}
}
func TestProjectOutputWrapper(t *testing.T) {
RegisterMockTestingT(t)
ctx := models.ProjectCommandContext{
Log: logging.NewNoopLogger(t),
Steps: []valid.Step{
{
StepName: "plan",
},
},
Workspace: "default",
RepoRelDir: ".",
}
cases := []struct {
Description string
Failure bool
Error bool
Success bool
CommandName models.CommandName
}{
{
Description: "plan success",
Success: true,
CommandName: models.PlanCommand,
},
{
Description: "plan failure",
Failure: true,
CommandName: models.PlanCommand,
},
{
Description: "plan error",
Error: true,
CommandName: models.PlanCommand,
},
{
Description: "apply success",
Success: true,
CommandName: models.ApplyCommand,
},
{
Description: "apply failure",
Failure: true,
CommandName: models.ApplyCommand,
},
{
Description: "apply error",
Error: true,
CommandName: models.ApplyCommand,
},
}
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
var prjResult models.ProjectResult
var expCommitStatus models.CommitStatus
mockJobURLSetter := eventmocks.NewMockJobURLSetter()
mockJobMessageSender := eventmocks.NewMockJobMessageSender()
mockProjectCommandRunner := mocks.NewMockProjectCommandRunner()
runner := &events.ProjectOutputWrapper{
JobURLSetter: mockJobURLSetter,
JobMessageSender: mockJobMessageSender,
ProjectCommandRunner: mockProjectCommandRunner,
}
if c.Success {
prjResult = models.ProjectResult{
PlanSuccess: &models.PlanSuccess{},
ApplySuccess: "exists",
}
expCommitStatus = models.SuccessCommitStatus
} else if c.Failure {
prjResult = models.ProjectResult{
Failure: "failure",
}
expCommitStatus = models.FailedCommitStatus
} else if c.Error {
prjResult = models.ProjectResult{
Error: errors.New("error"),
}
expCommitStatus = models.FailedCommitStatus
}
When(mockProjectCommandRunner.Plan(matchers.AnyModelsProjectCommandContext())).ThenReturn(prjResult)
When(mockProjectCommandRunner.Apply(matchers.AnyModelsProjectCommandContext())).ThenReturn(prjResult)
switch c.CommandName {
case models.PlanCommand:
runner.Plan(ctx)
case models.ApplyCommand:
runner.Apply(ctx)
}
mockJobURLSetter.VerifyWasCalled(Once()).SetJobURLWithStatus(ctx, c.CommandName, models.PendingCommitStatus)
mockJobURLSetter.VerifyWasCalled(Once()).SetJobURLWithStatus(ctx, c.CommandName, expCommitStatus)
switch c.CommandName {
case models.PlanCommand:
mockProjectCommandRunner.VerifyWasCalledOnce().Plan(ctx)
case models.ApplyCommand:
mockProjectCommandRunner.VerifyWasCalledOnce().Apply(ctx)
}
})
}
}
// Test what happens if there's no working dir. This signals that the project
// was never planned.
func TestDefaultProjectCommandRunner_ApplyNotCloned(t *testing.T) {
mockWorkingDir := mocks.NewMockWorkingDir()
runner := &events.DefaultProjectCommandRunner{
WorkingDir: mockWorkingDir,
}
ctx := models.ProjectCommandContext{}
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn("", os.ErrNotExist)
res := runner.Apply(ctx)
ErrEquals(t, "project has not been cloneddid you run plan?", res.Error)
}
// Test that if approval is required and the PR isn't approved we give an error.
func TestDefaultProjectCommandRunner_ApplyNotApproved(t *testing.T) {
RegisterMockTestingT(t)
mockWorkingDir := mocks.NewMockWorkingDir()
runner := &events.DefaultProjectCommandRunner{
WorkingDir: mockWorkingDir,
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
AggregateApplyRequirements: &events.AggregateApplyRequirements{
WorkingDir: mockWorkingDir,
},
}
ctx := models.ProjectCommandContext{
ApplyRequirements: []string{"approved"},
}
tmp, cleanup := TempDir(t)
defer cleanup()
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)
res := runner.Apply(ctx)
Equals(t, "Pull request must be approved by at least one person other than the author before running apply.", res.Failure)
}
// Test that if mergeable is required and the PR isn't mergeable we give an error.
func TestDefaultProjectCommandRunner_ApplyNotMergeable(t *testing.T) {
RegisterMockTestingT(t)
mockWorkingDir := mocks.NewMockWorkingDir()
runner := &events.DefaultProjectCommandRunner{
WorkingDir: mockWorkingDir,
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
AggregateApplyRequirements: &events.AggregateApplyRequirements{
WorkingDir: mockWorkingDir,
},
}
ctx := models.ProjectCommandContext{
PullReqStatus: models.PullReqStatus{
Mergeable: false,
},
ApplyRequirements: []string{"mergeable"},
}
tmp, cleanup := TempDir(t)
defer cleanup()
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)
res := runner.Apply(ctx)
Equals(t, "Pull request must be mergeable before running apply.", res.Failure)
}
// Test that if undiverged is required and the PR is diverged we give an error.
func TestDefaultProjectCommandRunner_ApplyDiverged(t *testing.T) {
RegisterMockTestingT(t)
mockWorkingDir := mocks.NewMockWorkingDir()
runner := &events.DefaultProjectCommandRunner{
WorkingDir: mockWorkingDir,
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
AggregateApplyRequirements: &events.AggregateApplyRequirements{
WorkingDir: mockWorkingDir,
},
}
ctx := models.ProjectCommandContext{
ApplyRequirements: []string{"undiverged"},
}
tmp, cleanup := TempDir(t)
defer cleanup()
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)
res := runner.Apply(ctx)
Equals(t, "Default branch must be rebased onto pull request before running apply.", res.Failure)
}
// Test that it runs the expected apply steps.
func TestDefaultProjectCommandRunner_Apply(t *testing.T) {
cases := []struct {
description string
steps []valid.Step
applyReqs []string
expSteps []string
expOut string
expFailure string
pullMergeable bool
}{
{
description: "normal workflow",
steps: valid.DefaultApplyStage.Steps,
expSteps: []string{"apply"},
expOut: "apply",
},
{
description: "approval required",
steps: valid.DefaultApplyStage.Steps,
applyReqs: []string{"approved"},
expSteps: []string{"approve", "apply"},
expOut: "apply",
},
{
description: "mergeable required",
steps: valid.DefaultApplyStage.Steps,
pullMergeable: true,
applyReqs: []string{"mergeable"},
expSteps: []string{"apply"},
expOut: "apply",
},
{
description: "mergeable required, pull not mergeable",
steps: valid.DefaultApplyStage.Steps,
pullMergeable: false,
applyReqs: []string{"mergeable"},
expSteps: []string{""},
expOut: "",
expFailure: "Pull request must be mergeable before running apply.",
},
{
description: "mergeable and approved required",
steps: valid.DefaultApplyStage.Steps,
pullMergeable: true,
applyReqs: []string{"mergeable", "approved"},
expSteps: []string{"approved", "apply"},
expOut: "apply",
},
{
description: "workflow with custom apply stage",
steps: []valid.Step{
{
StepName: "env",
EnvVarName: "key",
EnvVarValue: "value",
},
{
StepName: "run",
},
{
StepName: "apply",
},
{
StepName: "plan",
},
{
StepName: "init",
},
},
expSteps: []string{"env", "run", "apply", "plan", "init"},
expOut: "run\napply\nplan\ninit",
},
}
for _, c := range cases {
if c.description != "workflow with custom apply stage" {
continue
}
t.Run(c.description, func(t *testing.T) {
RegisterMockTestingT(t)
mockInit := mocks.NewMockStepRunner()
mockPlan := mocks.NewMockStepRunner()
mockApply := mocks.NewMockStepRunner()
mockRun := mocks.NewMockCustomStepRunner()
mockEnv := mocks.NewMockEnvStepRunner()
mockWorkingDir := mocks.NewMockWorkingDir()
mockLocker := mocks.NewMockProjectLocker()
mockSender := mocks.NewMockWebhooksSender()
applyReqHandler := &events.AggregateApplyRequirements{
WorkingDir: mockWorkingDir,
}
runner := events.DefaultProjectCommandRunner{
Locker: mockLocker,
LockURLGenerator: mockURLGenerator{},
InitStepRunner: mockInit,
PlanStepRunner: mockPlan,
ApplyStepRunner: mockApply,
RunStepRunner: mockRun,
EnvStepRunner: mockEnv,
WorkingDir: mockWorkingDir,
Webhooks: mockSender,
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
AggregateApplyRequirements: applyReqHandler,
}
repoDir, cleanup := TempDir(t)
defer cleanup()
When(mockWorkingDir.GetWorkingDir(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
AnyString(),
)).ThenReturn(repoDir, nil)
ctx := models.ProjectCommandContext{
Log: logging.NewNoopLogger(t),
Steps: c.steps,
Workspace: "default",
ApplyRequirements: c.applyReqs,
RepoRelDir: ".",
PullReqStatus: models.PullReqStatus{
ApprovalStatus: models.ApprovalStatus{
IsApproved: true,
},
Mergeable: true,
},
}
expEnvs := map[string]string{
"key": "value",
}
When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil)
When(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("plan", nil)
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", nil)
When(mockRun.Run(ctx, "", repoDir, expEnvs)).ThenReturn("run", nil)
When(mockEnv.Run(ctx, "", "value", repoDir, make(map[string]string))).ThenReturn("value", nil)
res := runner.Apply(ctx)
Equals(t, c.expOut, res.ApplySuccess)
Equals(t, c.expFailure, res.Failure)
for _, step := range c.expSteps {
switch step {
case "init":
mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
case "plan":
mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
case "apply":
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
case "run":
mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, expEnvs)
case "env":
mockEnv.VerifyWasCalledOnce().Run(ctx, "", "value", repoDir, expEnvs)
}
}
})
}
}
// Test that it runs the expected apply steps.
func TestDefaultProjectCommandRunner_ApplyRunStepFailure(t *testing.T) {
RegisterMockTestingT(t)
mockApply := mocks.NewMockStepRunner()
mockWorkingDir := mocks.NewMockWorkingDir()
mockLocker := mocks.NewMockProjectLocker()
mockSender := mocks.NewMockWebhooksSender()
applyReqHandler := &events.AggregateApplyRequirements{
WorkingDir: mockWorkingDir,
}
runner := events.DefaultProjectCommandRunner{
Locker: mockLocker,
LockURLGenerator: mockURLGenerator{},
ApplyStepRunner: mockApply,
WorkingDir: mockWorkingDir,
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
AggregateApplyRequirements: applyReqHandler,
Webhooks: mockSender,
}
repoDir, cleanup := TempDir(t)
defer cleanup()
When(mockWorkingDir.GetWorkingDir(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
AnyString(),
)).ThenReturn(repoDir, nil)
ctx := models.ProjectCommandContext{
Log: logging.NewNoopLogger(t),
Steps: []valid.Step{
{
StepName: "apply",
},
},
Workspace: "default",
ApplyRequirements: []string{},
RepoRelDir: ".",
}
expEnvs := map[string]string{}
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", fmt.Errorf("something went wrong"))
res := runner.Apply(ctx)
Assert(t, res.ApplySuccess == "", "exp apply failure")
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
}
// Test run and env steps. We don't use mocks for this test since we're
// not running any Terraform.
func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) {
RegisterMockTestingT(t)
tfClient := tmocks.NewMockClient()
tfVersion, err := version.NewVersion("0.12.0")
Ok(t, err)
run := runtime.RunStepRunner{
TerraformExecutor: tfClient,
DefaultTFVersion: tfVersion,
}
env := runtime.EnvStepRunner{
RunStepRunner: &run,
}
mockWorkingDir := mocks.NewMockWorkingDir()
mockLocker := mocks.NewMockProjectLocker()
runner := events.DefaultProjectCommandRunner{
Locker: mockLocker,
LockURLGenerator: mockURLGenerator{},
RunStepRunner: &run,
EnvStepRunner: &env,
WorkingDir: mockWorkingDir,
Webhooks: nil,
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
}
repoDir, cleanup := TempDir(t)
defer cleanup()
When(mockWorkingDir.Clone(
matchers.AnyPtrToLoggingSimpleLogger(),
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
AnyString(),
)).ThenReturn(repoDir, false, nil)
When(mockLocker.TryLock(
matchers.AnyPtrToLoggingSimpleLogger(),
matchers.AnyModelsPullRequest(),
matchers.AnyModelsUser(),
AnyString(),
matchers.AnyModelsProject(),
)).ThenReturn(&events.TryLockResponse{
LockAcquired: true,
LockKey: "lock-key",
}, nil)
ctx := models.ProjectCommandContext{
Log: logging.NewNoopLogger(t),
Steps: []valid.Step{
{
StepName: "run",
RunCommand: "echo var=$var",
},
{
StepName: "env",
EnvVarName: "var",
EnvVarValue: "value",
},
{
StepName: "run",
RunCommand: "echo var=$var",
},
{
StepName: "env",
EnvVarName: "dynamic_var",
RunCommand: "echo dynamic_value",
},
{
StepName: "run",
RunCommand: "echo dynamic_var=$dynamic_var",
},
// Test overriding the variable
{
StepName: "env",
EnvVarName: "dynamic_var",
EnvVarValue: "overridden",
},
{
StepName: "run",
RunCommand: "echo dynamic_var=$dynamic_var",
},
},
Workspace: "default",
RepoRelDir: ".",
}
res := runner.Plan(ctx)
Assert(t, res.PlanSuccess != nil, "exp plan success")
Equals(t, "https://lock-key", res.PlanSuccess.LockURL)
Equals(t, "var=\n\nvar=value\n\ndynamic_var=dynamic_value\n\ndynamic_var=overridden\n", res.PlanSuccess.TerraformOutput)
}
type mockURLGenerator struct{}
func (m mockURLGenerator) GenerateLockURL(lockID string) string {
return "https://" + lockID
}