Files
atlantis/server/events/plan_executor_test.go
Luke Kysow 75e90c1917 Quote extra comment args.
To avoid an attacker prepending something like
atlantis plan -- ; cat /etc/passwd
2018-02-27 12:55:26 -08:00

279 lines
11 KiB
Go

package events_test
import (
"errors"
"testing"
"github.com/mohae/deepcopy"
. "github.com/petergtz/pegomock"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/locking"
lmocks "github.com/runatlantis/atlantis/server/events/locking/mocks"
"github.com/runatlantis/atlantis/server/events/mocks"
"github.com/runatlantis/atlantis/server/events/models"
rmocks "github.com/runatlantis/atlantis/server/events/run/mocks"
tmocks "github.com/runatlantis/atlantis/server/events/terraform/mocks"
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
"github.com/runatlantis/atlantis/server/events/vcs/mocks/matchers"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
)
var planCtx = events.CommandContext{
Command: &events.Command{
Name: events.Plan,
Workspace: "workspace",
Dir: "",
},
Log: logging.NewNoopLogger(),
BaseRepo: models.Repo{},
HeadRepo: models.Repo{},
Pull: models.PullRequest{},
User: models.User{
Username: "anubhavmishra",
},
}
func TestExecute_ModifiedFilesErr(t *testing.T) {
t.Log("If GetModifiedFiles returns an error we return an error")
p, _, _ := setupPlanExecutorTest(t)
When(p.VCSClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), matchers.AnyVcsHost())).ThenReturn(nil, errors.New("err"))
r := p.Execute(&planCtx)
Assert(t, r.Error != nil, "exp .Error to be set")
Equals(t, "getting modified files: err", r.Error.Error())
}
func TestExecute_NoModifiedProjects(t *testing.T) {
t.Log("If there are no modified projects we return a failure")
p, _, _ := setupPlanExecutorTest(t)
// We don't need to actually mock VCSClient.GetModifiedFiles because by
// default it will return an empty slice which is what we want for this test.
r := p.Execute(&planCtx)
Equals(t, "No Terraform files were modified.", r.Failure)
}
func TestExecute_CloneErr(t *testing.T) {
t.Log("If AtlantisWorkspace.Clone returns an error we return an error")
p, _, _ := setupPlanExecutorTest(t)
When(p.VCSClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), matchers.AnyVcsHost())).ThenReturn([]string{"file.tf"}, nil)
When(p.Workspace.Clone(planCtx.Log, planCtx.BaseRepo, planCtx.HeadRepo, planCtx.Pull, "workspace")).ThenReturn("", errors.New("err"))
r := p.Execute(&planCtx)
Assert(t, r.Error != nil, "exp .Error to be set")
Equals(t, "err", r.Error.Error())
}
func TestExecute_DirectoryAndWorkspaceSet(t *testing.T) {
t.Log("Test that we run plan in the right directory and workspace if they're set")
p, runner, _ := setupPlanExecutorTest(t)
ctx := deepcopy.Copy(planCtx).(events.CommandContext)
ctx.Log = logging.NewNoopLogger()
ctx.Command.Dir = "dir1/dir2"
ctx.Command.Workspace = "workspace-flag"
When(p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, "workspace-flag")).
ThenReturn("/tmp/clone-repo", nil)
When(p.ProjectPreExecute.Execute(&ctx, "/tmp/clone-repo", models.Project{RepoFullName: "", Path: "dir1/dir2"})).
ThenReturn(events.PreExecuteResult{
LockResponse: locking.TryLockResponse{
LockKey: "key",
},
})
r := p.Execute(&ctx)
runner.VerifyWasCalledOnce().RunCommandWithVersion(
ctx.Log,
"/tmp/clone-repo/dir1/dir2",
[]string{"plan", "-refresh", "-no-color", "-out", "/tmp/clone-repo/dir1/dir2/workspace-flag.tfplan", "-var", "atlantis_user=anubhavmishra"},
nil,
"workspace-flag",
)
Assert(t, len(r.ProjectResults) == 1, "exp one project result")
result := r.ProjectResults[0]
Assert(t, result.PlanSuccess != nil, "exp plan success to not be nil")
Equals(t, "", result.PlanSuccess.TerraformOutput)
Equals(t, "lockurl-key", result.PlanSuccess.LockURL)
}
func TestExecute_AddedArgs(t *testing.T) {
t.Log("Test that we include extra-args added to the comment in the plan command")
p, runner, _ := setupPlanExecutorTest(t)
ctx := deepcopy.Copy(planCtx).(events.CommandContext)
ctx.Log = logging.NewNoopLogger()
ctx.Command.Flags = []string{"\"-target=resource\"", "\"-var\"", "\"a=b\"", "\";\"", "\"echo\"", "\"hi\""}
When(p.VCSClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), matchers.AnyVcsHost())).ThenReturn([]string{"file.tf"}, nil)
When(p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, "workspace")).
ThenReturn("/tmp/clone-repo", nil)
When(p.ProjectPreExecute.Execute(&ctx, "/tmp/clone-repo", models.Project{RepoFullName: "", Path: "."})).
ThenReturn(events.PreExecuteResult{
LockResponse: locking.TryLockResponse{
LockKey: "key",
},
})
r := p.Execute(&ctx)
runner.VerifyWasCalledOnce().RunCommandWithVersion(
ctx.Log,
"/tmp/clone-repo",
[]string{
"plan",
"-refresh",
"-no-color",
"-out",
"/tmp/clone-repo/workspace.tfplan",
"-var",
"atlantis_user=anubhavmishra",
// NOTE: extra args should be quoted to prevent an attacker from
// appending malicious commands.
"\"-target=resource\"",
"\"-var\"",
"\"a=b\"",
"\";\"",
"\"echo\"",
"\"hi\"",
},
nil,
"workspace",
)
Assert(t, len(r.ProjectResults) == 1, "exp one project result")
result := r.ProjectResults[0]
Assert(t, result.PlanSuccess != nil, "exp plan success to not be nil")
Equals(t, "", result.PlanSuccess.TerraformOutput)
Equals(t, "lockurl-key", result.PlanSuccess.LockURL)
}
func TestExecute_Success(t *testing.T) {
t.Log("If there are no errors, the plan should be returned")
p, runner, _ := setupPlanExecutorTest(t)
When(p.VCSClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), matchers.AnyVcsHost())).ThenReturn([]string{"file.tf"}, nil)
When(p.Workspace.Clone(planCtx.Log, planCtx.BaseRepo, planCtx.HeadRepo, planCtx.Pull, "workspace")).
ThenReturn("/tmp/clone-repo", nil)
When(p.ProjectPreExecute.Execute(&planCtx, "/tmp/clone-repo", models.Project{RepoFullName: "", Path: "."})).
ThenReturn(events.PreExecuteResult{
LockResponse: locking.TryLockResponse{
LockKey: "key",
},
})
r := p.Execute(&planCtx)
runner.VerifyWasCalledOnce().RunCommandWithVersion(
planCtx.Log,
"/tmp/clone-repo",
[]string{"plan", "-refresh", "-no-color", "-out", "/tmp/clone-repo/workspace.tfplan", "-var", "atlantis_user=anubhavmishra"},
nil,
"workspace",
)
Assert(t, len(r.ProjectResults) == 1, "exp one project result")
result := r.ProjectResults[0]
Assert(t, result.PlanSuccess != nil, "exp plan success to not be nil")
Equals(t, "", result.PlanSuccess.TerraformOutput)
Equals(t, "lockurl-key", result.PlanSuccess.LockURL)
}
func TestExecute_PreExecuteResult(t *testing.T) {
t.Log("If DefaultProjectPreExecutor.Execute returns a ProjectResult we should return it")
p, _, _ := setupPlanExecutorTest(t)
When(p.VCSClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), matchers.AnyVcsHost())).ThenReturn([]string{"file.tf"}, nil)
When(p.Workspace.Clone(planCtx.Log, planCtx.BaseRepo, planCtx.HeadRepo, planCtx.Pull, "workspace")).
ThenReturn("/tmp/clone-repo", nil)
projectResult := events.ProjectResult{
Failure: "failure",
}
When(p.ProjectPreExecute.Execute(&planCtx, "/tmp/clone-repo", models.Project{RepoFullName: "", Path: "."})).
ThenReturn(events.PreExecuteResult{ProjectResult: projectResult})
r := p.Execute(&planCtx)
Assert(t, len(r.ProjectResults) == 1, "exp one project result")
result := r.ProjectResults[0]
Equals(t, "failure", result.Failure)
}
func TestExecute_MultiProjectFailure(t *testing.T) {
t.Log("If is an error planning in one project it should be returned. It shouldn't affect another project though.")
p, runner, locker := setupPlanExecutorTest(t)
// Two projects have been modified so we should run plan in two paths.
When(p.VCSClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), matchers.AnyVcsHost())).ThenReturn([]string{"path1/file.tf", "path2/file.tf"}, nil)
When(p.Workspace.Clone(planCtx.Log, planCtx.BaseRepo, planCtx.HeadRepo, planCtx.Pull, "workspace")).
ThenReturn("/tmp/clone-repo", nil)
// Both projects will succeed in the PreExecute stage.
When(p.ProjectPreExecute.Execute(&planCtx, "/tmp/clone-repo", models.Project{RepoFullName: "", Path: "path1"})).
ThenReturn(events.PreExecuteResult{LockResponse: locking.TryLockResponse{LockKey: "key1"}})
When(p.ProjectPreExecute.Execute(&planCtx, "/tmp/clone-repo", models.Project{RepoFullName: "", Path: "path2"})).
ThenReturn(events.PreExecuteResult{LockResponse: locking.TryLockResponse{LockKey: "key2"}})
// The first project will fail when running plan
When(runner.RunCommandWithVersion(
planCtx.Log,
"/tmp/clone-repo/path1",
[]string{"plan", "-refresh", "-no-color", "-out", "/tmp/clone-repo/path1/workspace.tfplan", "-var", "atlantis_user=anubhavmishra"},
nil,
"workspace",
)).ThenReturn("", errors.New("path1 err"))
// The second will succeed. We don't need to stub it because by default it
// will return a nil error.
r := p.Execute(&planCtx)
// We expect Unlock to be called for the failed project.
locker.VerifyWasCalledOnce().Unlock("key1")
// So at the end we expect the first project to return an error and the second to be successful.
Assert(t, len(r.ProjectResults) == 2, "exp two project results")
result1 := r.ProjectResults[0]
Assert(t, result1.Error != nil, "exp err to not be nil")
Equals(t, "path1 err\n", result1.Error.Error())
result2 := r.ProjectResults[1]
Assert(t, result2.PlanSuccess != nil, "exp plan success to not be nil")
Equals(t, "", result2.PlanSuccess.TerraformOutput)
Equals(t, "lockurl-key2", result2.PlanSuccess.LockURL)
}
func TestExecute_PostPlanCommands(t *testing.T) {
t.Log("Should execute post-plan commands and return if there is an error")
p, _, _ := setupPlanExecutorTest(t)
When(p.VCSClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), matchers.AnyVcsHost())).ThenReturn([]string{"file.tf"}, nil)
When(p.Workspace.Clone(planCtx.Log, planCtx.BaseRepo, planCtx.HeadRepo, planCtx.Pull, "workspace")).
ThenReturn("/tmp/clone-repo", nil)
When(p.ProjectPreExecute.Execute(&planCtx, "/tmp/clone-repo", models.Project{RepoFullName: "", Path: "."})).
ThenReturn(events.PreExecuteResult{
ProjectConfig: events.ProjectConfig{PostPlan: []string{"post-plan"}},
})
When(p.Run.Execute(planCtx.Log, []string{"post-plan"}, "/tmp/clone-repo", "workspace", nil, "post_plan")).
ThenReturn("", errors.New("err"))
r := p.Execute(&planCtx)
Assert(t, len(r.ProjectResults) == 1, "exp one project result")
result := r.ProjectResults[0]
Assert(t, result.Error != nil, "exp plan error to not be nil")
Equals(t, "running post plan commands: err", result.Error.Error())
}
func setupPlanExecutorTest(t *testing.T) (*events.PlanExecutor, *tmocks.MockClient, *lmocks.MockLocker) {
RegisterMockTestingT(t)
vcsProxy := vcsmocks.NewMockClientProxy()
w := mocks.NewMockAtlantisWorkspace()
ppe := mocks.NewMockProjectPreExecutor()
runner := tmocks.NewMockClient()
locker := lmocks.NewMockLocker()
run := rmocks.NewMockRunner()
p := events.PlanExecutor{
VCSClient: vcsProxy,
ProjectFinder: &events.DefaultProjectFinder{},
Workspace: w,
ProjectPreExecute: ppe,
Terraform: runner,
Locker: locker,
Run: run,
}
p.LockURL = func(id string) (url string) {
return "lockurl-" + id
}
return &p, runner, locker
}