First pass at naked plan/apply working on all.

This commit is contained in:
Luke Kysow
2018-08-07 20:10:14 -10:00
parent 5a578ca80b
commit 12a9a19fde
15 changed files with 331 additions and 155 deletions

View File

@@ -101,15 +101,7 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo
return
}
var results []ProjectResult
for _, cmd := range projectCmds {
res := c.ProjectCommandRunner.Plan(cmd)
results = append(results, ProjectResult{
ProjectCommandResult: res,
RepoRelDir: cmd.RepoRelDir,
Workspace: cmd.Workspace,
})
}
results := c.runProjectCmds(projectCmds, PlanCommand)
c.updatePull(ctx, AutoplanCommand{}, CommandResult{ProjectResults: results})
}
@@ -152,45 +144,48 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
BaseRepo: baseRepo,
}
defer c.logPanics(ctx)
if !c.validateCtxAndComment(ctx) {
return
}
if err := c.CommitStatusUpdater.Update(ctx.BaseRepo, ctx.Pull, models.PendingCommitStatus, cmd.CommandName()); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}
var result ProjectCommandResult
var projectCmds []models.ProjectCommandContext
switch cmd.Name {
case PlanCommand:
projectCmd, err := c.ProjectCommandBuilder.BuildPlanCommand(ctx, cmd)
if err != nil {
c.updatePull(ctx, cmd, CommandResult{Error: err})
return
}
result = c.ProjectCommandRunner.Plan(projectCmd)
projectCmds, err = c.ProjectCommandBuilder.BuildPlanCommands(ctx, cmd)
case ApplyCommand:
projectCmd, err := c.ProjectCommandBuilder.BuildApplyCommand(ctx, cmd)
if err != nil {
c.updatePull(ctx, cmd, CommandResult{Error: err})
return
}
result = c.ProjectCommandRunner.Apply(projectCmd)
projectCmds, err = c.ProjectCommandBuilder.BuildApplyCommands(ctx, cmd)
default:
ctx.Log.Err("failed to determine desired command, neither plan nor apply")
return
}
if err != nil {
c.updatePull(ctx, cmd, CommandResult{Error: err})
return
}
results := c.runProjectCmds(projectCmds, cmd.Name)
c.updatePull(
ctx,
cmd,
CommandResult{
ProjectResults: []ProjectResult{{
RepoRelDir: cmd.RepoRelDir,
Workspace: cmd.Workspace,
ProjectCommandResult: result,
}}})
ProjectResults: results})
}
func (c *DefaultCommandRunner) runProjectCmds(cmds []models.ProjectCommandContext, cmdName CommandName) []ProjectResult {
var results []ProjectResult
for _, pCmd := range cmds {
var res ProjectResult
switch cmdName {
case PlanCommand:
res = c.ProjectCommandRunner.Plan(pCmd)
case ApplyCommand:
res = c.ProjectCommandRunner.Apply(pCmd)
}
results = append(results, res)
}
return results
}
func (c *DefaultCommandRunner) getGithubData(baseRepo models.Repo, pullNum int) (models.PullRequest, models.Repo, error) {

View File

@@ -154,57 +154,3 @@ func TestRunCommentCommand_ClosedPull(t *testing.T) {
ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, nil, fixtures.User, fixtures.Pull.Num, nil)
vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "Atlantis commands can't be run on closed pull requests")
}
func TestRunCommentCommand_FullRun(t *testing.T) {
pull := &github.PullRequest{
State: github.String("closed"),
}
expCmdResult := events.CommandResult{
ProjectResults: []events.ProjectResult{
{
RepoRelDir: ".",
Workspace: "default",
},
},
}
for _, c := range []events.CommandName{events.PlanCommand, events.ApplyCommand} {
setup(t)
cmd := events.NewCommentCommand(".", nil, c, false, "default", "")
When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil)
When(eventParsing.ParseGithubPull(pull)).ThenReturn(fixtures.Pull, fixtures.GithubRepo, fixtures.GithubRepo, nil)
cmdCtx := models.ProjectCommandContext{RepoRelDir: "."}
switch c {
case events.PlanCommand:
When(projectCommandBuilder.BuildPlanCommand(matchers.AnyPtrToEventsCommandContext(), matchers.AnyPtrToEventsCommentCommand())).ThenReturn(cmdCtx, nil)
case events.ApplyCommand:
When(projectCommandBuilder.BuildApplyCommand(matchers.AnyPtrToEventsCommandContext(), matchers.AnyPtrToEventsCommentCommand())).ThenReturn(cmdCtx, nil)
}
ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, cmd)
ghStatus.VerifyWasCalledOnce().Update(fixtures.GithubRepo, fixtures.Pull, models.PendingCommitStatus, c)
_, _, response := ghStatus.VerifyWasCalledOnce().UpdateProjectResult(matchers.AnyPtrToEventsCommandContext(), matchers.AnyEventsCommandName(), matchers.AnyEventsCommandResult()).GetCapturedArguments()
Equals(t, expCmdResult, response)
vcsClient.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString())
}
}
func TestRunAutoplanCommands(t *testing.T) {
expCmdResult := events.CommandResult{
ProjectResults: []events.ProjectResult{
{
RepoRelDir: ".",
Workspace: "default",
},
},
}
setup(t)
When(projectCommandBuilder.BuildAutoplanCommands(matchers.AnyPtrToEventsCommandContext())).ThenReturn([]models.ProjectCommandContext{{RepoRelDir: ".", Workspace: "default"}}, nil)
ch.RunAutoplanCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User)
ghStatus.VerifyWasCalledOnce().Update(fixtures.GithubRepo, fixtures.Pull, models.PendingCommitStatus, events.PlanCommand)
_, _, response := ghStatus.VerifyWasCalledOnce().UpdateProjectResult(matchers.AnyPtrToEventsCommandContext(), matchers.AnyEventsCommandName(), matchers.AnyEventsCommandResult()).GetCapturedArguments()
Equals(t, expCmdResult, response)
vcsClient.VerifyWasCalledOnce().CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString())
}

View File

@@ -0,0 +1,20 @@
package matchers
import (
"reflect"
"github.com/petergtz/pegomock"
events "github.com/runatlantis/atlantis/server/events"
)
func AnyEventsProjectResult() events.ProjectResult {
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(events.ProjectResult))(nil)).Elem()))
var nullValue events.ProjectResult
return nullValue
}
func EqEventsProjectResult(value events.ProjectResult) events.ProjectResult {
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
var nullValue events.ProjectResult
return nullValue
}

View File

@@ -35,14 +35,14 @@ func (mock *MockProjectCommandBuilder) BuildAutoplanCommands(ctx *events.Command
return ret0, ret1
}
func (mock *MockProjectCommandBuilder) BuildPlanCommand(ctx *events.CommandContext, commentCommand *events.CommentCommand) (models.ProjectCommandContext, error) {
func (mock *MockProjectCommandBuilder) BuildPlanCommands(ctx *events.CommandContext, commentCommand *events.CommentCommand) ([]models.ProjectCommandContext, error) {
params := []pegomock.Param{ctx, commentCommand}
result := pegomock.GetGenericMockFrom(mock).Invoke("BuildPlanCommand", params, []reflect.Type{reflect.TypeOf((*models.ProjectCommandContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 models.ProjectCommandContext
result := pegomock.GetGenericMockFrom(mock).Invoke("BuildPlanCommands", params, []reflect.Type{reflect.TypeOf((*[]models.ProjectCommandContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 []models.ProjectCommandContext
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(models.ProjectCommandContext)
ret0 = result[0].([]models.ProjectCommandContext)
}
if result[1] != nil {
ret1 = result[1].(error)
@@ -51,14 +51,14 @@ func (mock *MockProjectCommandBuilder) BuildPlanCommand(ctx *events.CommandConte
return ret0, ret1
}
func (mock *MockProjectCommandBuilder) BuildApplyCommand(ctx *events.CommandContext, commentCommand *events.CommentCommand) (models.ProjectCommandContext, error) {
func (mock *MockProjectCommandBuilder) BuildApplyCommands(ctx *events.CommandContext, commentCommand *events.CommentCommand) ([]models.ProjectCommandContext, error) {
params := []pegomock.Param{ctx, commentCommand}
result := pegomock.GetGenericMockFrom(mock).Invoke("BuildApplyCommand", params, []reflect.Type{reflect.TypeOf((*models.ProjectCommandContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 models.ProjectCommandContext
result := pegomock.GetGenericMockFrom(mock).Invoke("BuildApplyCommands", params, []reflect.Type{reflect.TypeOf((*[]models.ProjectCommandContext)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 []models.ProjectCommandContext
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(models.ProjectCommandContext)
ret0 = result[0].([]models.ProjectCommandContext)
}
if result[1] != nil {
ret1 = result[1].(error)
@@ -112,23 +112,23 @@ func (c *ProjectCommandBuilder_BuildAutoplanCommands_OngoingVerification) GetAll
return
}
func (verifier *VerifierProjectCommandBuilder) BuildPlanCommand(ctx *events.CommandContext, commentCommand *events.CommentCommand) *ProjectCommandBuilder_BuildPlanCommand_OngoingVerification {
func (verifier *VerifierProjectCommandBuilder) BuildPlanCommands(ctx *events.CommandContext, commentCommand *events.CommentCommand) *ProjectCommandBuilder_BuildPlanCommands_OngoingVerification {
params := []pegomock.Param{ctx, commentCommand}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildPlanCommand", params)
return &ProjectCommandBuilder_BuildPlanCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildPlanCommands", params)
return &ProjectCommandBuilder_BuildPlanCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type ProjectCommandBuilder_BuildPlanCommand_OngoingVerification struct {
type ProjectCommandBuilder_BuildPlanCommands_OngoingVerification struct {
mock *MockProjectCommandBuilder
methodInvocations []pegomock.MethodInvocation
}
func (c *ProjectCommandBuilder_BuildPlanCommand_OngoingVerification) GetCapturedArguments() (*events.CommandContext, *events.CommentCommand) {
func (c *ProjectCommandBuilder_BuildPlanCommands_OngoingVerification) GetCapturedArguments() (*events.CommandContext, *events.CommentCommand) {
ctx, commentCommand := c.GetAllCapturedArguments()
return ctx[len(ctx)-1], commentCommand[len(commentCommand)-1]
}
func (c *ProjectCommandBuilder_BuildPlanCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext, _param1 []*events.CommentCommand) {
func (c *ProjectCommandBuilder_BuildPlanCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext, _param1 []*events.CommentCommand) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]*events.CommandContext, len(params[0]))
@@ -143,23 +143,23 @@ func (c *ProjectCommandBuilder_BuildPlanCommand_OngoingVerification) GetAllCaptu
return
}
func (verifier *VerifierProjectCommandBuilder) BuildApplyCommand(ctx *events.CommandContext, commentCommand *events.CommentCommand) *ProjectCommandBuilder_BuildApplyCommand_OngoingVerification {
func (verifier *VerifierProjectCommandBuilder) BuildApplyCommands(ctx *events.CommandContext, commentCommand *events.CommentCommand) *ProjectCommandBuilder_BuildApplyCommands_OngoingVerification {
params := []pegomock.Param{ctx, commentCommand}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildApplyCommand", params)
return &ProjectCommandBuilder_BuildApplyCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildApplyCommands", params)
return &ProjectCommandBuilder_BuildApplyCommands_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type ProjectCommandBuilder_BuildApplyCommand_OngoingVerification struct {
type ProjectCommandBuilder_BuildApplyCommands_OngoingVerification struct {
mock *MockProjectCommandBuilder
methodInvocations []pegomock.MethodInvocation
}
func (c *ProjectCommandBuilder_BuildApplyCommand_OngoingVerification) GetCapturedArguments() (*events.CommandContext, *events.CommentCommand) {
func (c *ProjectCommandBuilder_BuildApplyCommands_OngoingVerification) GetCapturedArguments() (*events.CommandContext, *events.CommentCommand) {
ctx, commentCommand := c.GetAllCapturedArguments()
return ctx[len(ctx)-1], commentCommand[len(commentCommand)-1]
}
func (c *ProjectCommandBuilder_BuildApplyCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext, _param1 []*events.CommentCommand) {
func (c *ProjectCommandBuilder_BuildApplyCommands_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext, _param1 []*events.CommentCommand) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]*events.CommandContext, len(params[0]))

View File

@@ -19,25 +19,25 @@ func NewMockProjectCommandRunner() *MockProjectCommandRunner {
return &MockProjectCommandRunner{fail: pegomock.GlobalFailHandler}
}
func (mock *MockProjectCommandRunner) Plan(ctx models.ProjectCommandContext) events.ProjectCommandResult {
func (mock *MockProjectCommandRunner) Plan(ctx models.ProjectCommandContext) events.ProjectResult {
params := []pegomock.Param{ctx}
result := pegomock.GetGenericMockFrom(mock).Invoke("Plan", params, []reflect.Type{reflect.TypeOf((*events.ProjectCommandResult)(nil)).Elem()})
var ret0 events.ProjectCommandResult
result := pegomock.GetGenericMockFrom(mock).Invoke("Plan", params, []reflect.Type{reflect.TypeOf((*events.ProjectResult)(nil)).Elem()})
var ret0 events.ProjectResult
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(events.ProjectCommandResult)
ret0 = result[0].(events.ProjectResult)
}
}
return ret0
}
func (mock *MockProjectCommandRunner) Apply(ctx models.ProjectCommandContext) events.ProjectCommandResult {
func (mock *MockProjectCommandRunner) Apply(ctx models.ProjectCommandContext) events.ProjectResult {
params := []pegomock.Param{ctx}
result := pegomock.GetGenericMockFrom(mock).Invoke("Apply", params, []reflect.Type{reflect.TypeOf((*events.ProjectCommandResult)(nil)).Elem()})
var ret0 events.ProjectCommandResult
result := pegomock.GetGenericMockFrom(mock).Invoke("Apply", params, []reflect.Type{reflect.TypeOf((*events.ProjectResult)(nil)).Elem()})
var ret0 events.ProjectResult
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(events.ProjectCommandResult)
ret0 = result[0].(events.ProjectResult)
}
}
return ret0

View File

@@ -51,6 +51,22 @@ func (mock *MockWorkingDir) GetWorkingDir(r models.Repo, p models.PullRequest, w
return ret0, ret1
}
func (mock *MockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) (string, error) {
params := []pegomock.Param{r, p}
result := pegomock.GetGenericMockFrom(mock).Invoke("GetPullDir", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockWorkingDir) Delete(r models.Repo, p models.PullRequest) error {
params := []pegomock.Param{r, p}
result := pegomock.GetGenericMockFrom(mock).Invoke("Delete", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
@@ -171,6 +187,37 @@ func (c *WorkingDir_GetWorkingDir_OngoingVerification) GetAllCapturedArguments()
return
}
func (verifier *VerifierWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) *WorkingDir_GetPullDir_OngoingVerification {
params := []pegomock.Param{r, p}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetPullDir", params)
return &WorkingDir_GetPullDir_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type WorkingDir_GetPullDir_OngoingVerification struct {
mock *MockWorkingDir
methodInvocations []pegomock.MethodInvocation
}
func (c *WorkingDir_GetPullDir_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest) {
r, p := c.GetAllCapturedArguments()
return r[len(r)-1], p[len(p)-1]
}
func (c *WorkingDir_GetPullDir_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]models.Repo, len(params[0]))
for u, param := range params[0] {
_param0[u] = param.(models.Repo)
}
_param1 = make([]models.PullRequest, len(params[1]))
for u, param := range params[1] {
_param1[u] = param.(models.PullRequest)
}
}
return
}
func (verifier *VerifierWorkingDir) Delete(r models.Repo, p models.PullRequest) *WorkingDir_Delete_OngoingVerification {
params := []pegomock.Param{r, p}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Delete", params)

View File

@@ -286,4 +286,6 @@ type ProjectCommandContext struct {
// ex. atlantis plan -- -target=resource
CommentArgs []string
Workspace string
// Verbose is true when the user would like verbose output.
Verbose bool
}

View File

@@ -2,6 +2,9 @@ package events
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
@@ -12,12 +15,17 @@ import (
"github.com/runatlantis/atlantis/server/logging"
)
const (
DefaultRepoRelDir = "."
DefaultWorkspace = "default"
)
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder
type ProjectCommandBuilder interface {
BuildAutoplanCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error)
BuildPlanCommand(ctx *CommandContext, commentCommand *CommentCommand) (models.ProjectCommandContext, error)
BuildApplyCommand(ctx *CommandContext, commentCommand *CommentCommand) (models.ProjectCommandContext, error)
BuildPlanCommands(ctx *CommandContext, commentCommand *CommentCommand) ([]models.ProjectCommandContext, error)
BuildApplyCommands(ctx *CommandContext, commentCommand *CommentCommand) ([]models.ProjectCommandContext, error)
}
type DefaultProjectCommandBuilder struct {
@@ -35,8 +43,25 @@ type TerraformExec interface {
}
func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) {
cmds, err := p.BuildPlanAllCommands(ctx, nil, false)
if err != nil {
return nil, err
}
// Filter out projects where autoplanning is specifically disabled.
var autoplanEnabled []models.ProjectCommandContext
for _, cmd := range cmds {
if cmd.ProjectConfig != nil && !cmd.ProjectConfig.Autoplan.Enabled {
ctx.Log.Debug("ignoring project at dir %q, workspace: %q because autoplan is disabled", cmd.RepoRelDir, cmd.Workspace)
continue
}
autoplanEnabled = append(autoplanEnabled, cmd)
}
return autoplanEnabled, nil
}
func (p *DefaultProjectCommandBuilder) BuildPlanAllCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) {
// Need to lock the workspace we're about to clone to.
workspace := "default"
workspace := DefaultWorkspace
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, workspace, ctx.Pull.Num)
if err != nil {
ctx.Log.Warn("workspace was locked")
@@ -94,8 +119,9 @@ func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext
RepoRelDir: mp.Path,
ProjectConfig: nil,
GlobalConfig: nil,
CommentArgs: nil,
Workspace: "default",
CommentArgs: commentFlags,
Workspace: DefaultWorkspace,
Verbose: verbose,
})
}
} else {
@@ -105,7 +131,7 @@ func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext
if err != nil {
return nil, err
}
ctx.Log.Info("%d projects are to be autoplanned based on their when_modified config", len(matchingProjects))
ctx.Log.Info("%d projects are to be planned based on their when_modified config", len(matchingProjects))
// Use for i instead of range because need to get the pointer to the
// project config.
@@ -117,55 +143,163 @@ func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext
Pull: ctx.Pull,
User: ctx.User,
Log: ctx.Log,
CommentArgs: nil,
CommentArgs: commentFlags,
Workspace: mp.Workspace,
RepoRelDir: mp.Dir,
ProjectConfig: &mp,
GlobalConfig: &config,
Verbose: verbose,
})
}
}
return projCtxs, nil
}
func (p *DefaultProjectCommandBuilder) BuildPlanCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) {
var projCtx models.ProjectCommandContext
func (p *DefaultProjectCommandBuilder) BuildProjectPlanCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) {
workspace := DefaultWorkspace
if cmd.Workspace != "" {
workspace = cmd.Workspace
}
var pcc models.ProjectCommandContext
ctx.Log.Debug("building plan command")
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, cmd.Workspace, ctx.Pull.Num)
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, workspace, ctx.Pull.Num)
if err != nil {
return projCtx, err
return pcc, err
}
defer unlockFn()
ctx.Log.Debug("cloning repository")
repoDir, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, cmd.Workspace)
repoDir, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace)
if err != nil {
return projCtx, err
return pcc, err
}
return p.buildProjectCommandCtx(ctx, cmd, repoDir)
repoRelDir := DefaultRepoRelDir
if cmd.RepoRelDir != "" {
repoRelDir = cmd.RepoRelDir
}
return p.buildProjectCommandCtx(ctx, cmd.ProjectName, cmd.Flags, repoDir, repoRelDir, workspace)
}
func (p *DefaultProjectCommandBuilder) BuildApplyCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) {
var projCtx models.ProjectCommandContext
func (p *DefaultProjectCommandBuilder) BuildPlanCommands(ctx *CommandContext, cmd *CommentCommand) ([]models.ProjectCommandContext, error) {
if !cmd.IsForSpecificProject() {
return p.BuildPlanAllCommands(ctx, cmd.Flags, cmd.Verbose)
}
pcc, err := p.BuildProjectPlanCommand(ctx, cmd)
if err != nil {
return nil, err
}
return []models.ProjectCommandContext{pcc}, nil
}
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, cmd.Workspace, ctx.Pull.Num)
func (p *DefaultProjectCommandBuilder) BuildApplyAllCommands(ctx *CommandContext, commentCmd *CommentCommand) ([]models.ProjectCommandContext, error) {
// lock all dirs in this pull request
unlockFn, err := p.WorkingDirLocker.TryLockPull(ctx.BaseRepo.FullName, ctx.Pull.Num)
if err != nil {
return nil, err
}
defer unlockFn()
pullDir, err := p.WorkingDir.GetPullDir(ctx.BaseRepo, ctx.Pull)
if err != nil {
return nil, err
}
plans, err := p.findUnappliedPlans(pullDir)
if err != nil {
return nil, err
}
var cmds []models.ProjectCommandContext
for _, plan := range plans {
cmd, err := p.buildProjectCommandCtx(ctx, commentCmd.ProjectName, commentCmd.Flags, plan.RepoDir, plan.RepoRelDir, plan.Workspace)
if err != nil {
return nil, errors.Wrapf(err, "building command for dir %q", plan.RepoRelDir)
}
cmds = append(cmds, cmd)
}
return cmds, nil
}
type UnappliedPlan struct {
RepoDir string
RepoRelDir string
Workspace string
}
func (p *DefaultProjectCommandBuilder) findUnappliedPlans(pullDir string) ([]UnappliedPlan, error) {
workspaceDirs, err := ioutil.ReadDir(pullDir)
if err != nil {
return nil, err
}
var plans []UnappliedPlan
for _, workspaceDir := range workspaceDirs {
workspace := workspaceDir.Name()
repoDir := filepath.Join(pullDir, workspace)
err := filepath.Walk(repoDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Check if the plan is for the right workspace,
if !info.IsDir() && filepath.Ext(path) == ".tfplan" {
repoRelDir, _ := filepath.Rel(repoDir, filepath.Dir(path))
plans = append(plans, UnappliedPlan{
RepoDir: repoDir,
RepoRelDir: repoRelDir,
Workspace: workspace,
})
}
return nil
})
if err != nil {
return nil, errors.Wrapf(err, "searching dir %q for unapplied plans", repoDir)
}
}
return plans, nil
}
func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *CommandContext, cmd *CommentCommand) ([]models.ProjectCommandContext, error) {
if !cmd.IsForSpecificProject() {
return p.BuildApplyAllCommands(ctx, cmd)
}
pac, err := p.BuildProjectApplyCommand(ctx, cmd)
if err != nil {
return nil, err
}
return []models.ProjectCommandContext{pac}, nil
}
func (p *DefaultProjectCommandBuilder) BuildProjectApplyCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) {
workspace := DefaultWorkspace
if cmd.Workspace != "" {
workspace = cmd.Workspace
}
var projCtx models.ProjectCommandContext
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, workspace, ctx.Pull.Num)
if err != nil {
return projCtx, err
}
defer unlockFn()
repoDir, err := p.WorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, cmd.Workspace)
repoDir, err := p.WorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, workspace)
if err != nil {
return projCtx, err
}
return p.buildProjectCommandCtx(ctx, cmd, repoDir)
repoRelDir := DefaultRepoRelDir
if cmd.RepoRelDir != "" {
repoRelDir = cmd.RepoRelDir
}
return p.buildProjectCommandCtx(ctx, cmd.ProjectName, cmd.Flags, repoDir, repoRelDir, workspace)
}
func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContext, cmd *CommentCommand, repoDir string) (models.ProjectCommandContext, error) {
projCfg, globalCfg, err := p.getCfg(cmd.ProjectName, cmd.RepoRelDir, cmd.Workspace, repoDir)
func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContext, projectName string, commentFlags []string, repoDir string, repoRelDir string, workspace string) (models.ProjectCommandContext, error) {
projCfg, globalCfg, err := p.getCfg(projectName, repoRelDir, workspace, repoDir)
if err != nil {
return models.ProjectCommandContext{}, err
}
@@ -173,10 +307,8 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex
// Override any dir/workspace defined on the comment with what was
// defined in config. This shouldn't matter since we don't allow comments
// with both project name and dir/workspace.
dir := cmd.RepoRelDir
workspace := cmd.Workspace
if projCfg != nil {
dir = projCfg.Dir
repoRelDir = projCfg.Dir
workspace = projCfg.Workspace
}
@@ -186,9 +318,9 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex
Pull: ctx.Pull,
User: ctx.User,
Log: ctx.Log,
CommentArgs: cmd.Flags,
CommentArgs: commentFlags,
Workspace: workspace,
RepoRelDir: dir,
RepoRelDir: repoRelDir,
ProjectConfig: projCfg,
GlobalConfig: globalCfg,
}, nil

View File

@@ -395,10 +395,14 @@ projects:
pull := models.PullRequest{}
logger := logging.NewNoopLogger()
workingDir := mocks.NewMockWorkingDir()
expWorkspace := c.Cmd.Workspace
if expWorkspace == "" {
expWorkspace = "default"
}
if cmdName == events.PlanCommand {
When(workingDir.Clone(logger, baseRepo, headRepo, pull, c.Cmd.Workspace)).ThenReturn(tmpDir, nil)
When(workingDir.Clone(logger, baseRepo, headRepo, pull, expWorkspace)).ThenReturn(tmpDir, nil)
} else {
When(workingDir.GetWorkingDir(baseRepo, pull, c.Cmd.Workspace)).ThenReturn(tmpDir, nil)
When(workingDir.GetWorkingDir(baseRepo, pull, expWorkspace)).ThenReturn(tmpDir, nil)
}
if c.AtlantisYAML != "" {
err := ioutil.WriteFile(filepath.Join(tmpDir, yaml.AtlantisYAMLFilename), []byte(c.AtlantisYAML), 0600)
@@ -426,20 +430,21 @@ projects:
User: models.User{},
Log: logger,
}
var actCtx models.ProjectCommandContext
var actCtxs []models.ProjectCommandContext
if cmdName == events.PlanCommand {
actCtx, err = builder.BuildPlanCommand(cmdCtx, &c.Cmd)
actCtxs, err = builder.BuildPlanCommands(cmdCtx, &c.Cmd)
} else {
actCtx, err = builder.BuildApplyCommand(cmdCtx, &c.Cmd)
actCtxs, err = builder.BuildApplyCommands(cmdCtx, &c.Cmd)
}
if c.ExpErr != "" {
ErrEquals(t, c.ExpErr, err)
return
}
Ok(t, err)
Equals(t, 1, len(actCtxs))
actCtx := actCtxs[0]
Equals(t, baseRepo, actCtx.BaseRepo)
Equals(t, baseRepo, actCtx.HeadRepo)
Equals(t, pull, actCtx.Pull)

View File

@@ -55,8 +55,8 @@ type PlanSuccess struct {
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_project_command_runner.go ProjectCommandRunner
type ProjectCommandRunner interface {
Plan(ctx models.ProjectCommandContext) ProjectCommandResult
Apply(ctx models.ProjectCommandContext) ProjectCommandResult
Plan(ctx models.ProjectCommandContext) ProjectResult
Apply(ctx models.ProjectCommandContext) ProjectResult
}
type DefaultProjectCommandRunner struct {
@@ -73,7 +73,25 @@ type DefaultProjectCommandRunner struct {
RequireApprovalOverride bool
}
func (p *DefaultProjectCommandRunner) Plan(ctx models.ProjectCommandContext) ProjectCommandResult {
func (p *DefaultProjectCommandRunner) Plan(ctx models.ProjectCommandContext) ProjectResult {
result := p.doPlan(ctx)
return ProjectResult{
ProjectCommandResult: result,
RepoRelDir: ctx.RepoRelDir,
Workspace: ctx.Workspace,
}
}
func (p *DefaultProjectCommandRunner) Apply(ctx models.ProjectCommandContext) ProjectResult {
result := p.doApply(ctx)
return ProjectResult{
ProjectCommandResult: result,
RepoRelDir: ctx.RepoRelDir,
Workspace: ctx.Workspace,
}
}
func (p *DefaultProjectCommandRunner) doPlan(ctx models.ProjectCommandContext) ProjectCommandResult {
// Acquire Atlantis lock for this repo/dir/workspace.
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.BaseRepo.FullName, ctx.RepoRelDir))
if err != nil {
@@ -155,7 +173,7 @@ func (p *DefaultProjectCommandRunner) runSteps(steps []valid.Step, ctx models.Pr
return outputs, nil
}
func (p *DefaultProjectCommandRunner) Apply(ctx models.ProjectCommandContext) ProjectCommandResult {
func (p *DefaultProjectCommandRunner) doApply(ctx models.ProjectCommandContext) ProjectCommandResult {
repoDir, err := p.WorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)
if err != nil {
if os.IsNotExist(err) {

View File

@@ -80,10 +80,6 @@ func (p *DefaultProjectFinder) DetermineProjectsViaConfig(log *logging.SimpleLog
var projects []valid.Project
for _, project := range config.Projects {
log.Debug("checking if project at dir %q workspace %q was modified", project.Dir, project.Workspace)
if !project.Autoplan.Enabled {
log.Debug("autoplan disabled, ignoring")
continue
}
// Prepend project dir to when modified patterns because the patterns
// are relative to the project dirs but our list of modified files is
// relative to the repo root.

View File

@@ -37,6 +37,7 @@ type WorkingDir interface {
// GetWorkingDir returns the path to the workspace for this repo and pull.
// If workspace does not exist on disk, error will be of type os.IsNotExist.
GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error)
GetPullDir(r models.Repo, p models.PullRequest) (string, error)
// Delete deletes the workspace for this repo and pull.
Delete(r models.Repo, p models.PullRequest) error
DeleteForWorkspace(r models.Repo, p models.PullRequest, workspace string) error
@@ -133,6 +134,14 @@ func (w *FileWorkspace) GetWorkingDir(r models.Repo, p models.PullRequest, works
return repoDir, nil
}
func (w *FileWorkspace) GetPullDir(r models.Repo, p models.PullRequest) (string, error) {
dir := w.repoPullDir(r, p)
if _, err := os.Stat(dir); err != nil {
return "", err
}
return dir, nil
}
// Delete deletes the workspace for this repo and pull.
func (w *FileWorkspace) Delete(r models.Repo, p models.PullRequest) error {
return os.RemoveAll(w.repoPullDir(r, p))

View File

@@ -31,6 +31,7 @@ type WorkingDirLocker interface {
// an error if the workspace is already locked. The error is expected to
// be printed to the pull request.
TryLock(repoFullName string, workspace string, pullNum int) (func(), error)
TryLockPull(repoFullName string, pullNum int) (func(), error)
// Unlock deletes the lock for this repo, workspace and pull. If there was no
// lock it will do nothing.
Unlock(repoFullName, workspace string, pullNum int)
@@ -49,6 +50,11 @@ func NewDefaultWorkingDirLocker() *DefaultWorkingDirLocker {
}
}
func (d *DefaultWorkingDirLocker) TryLockPull(repoFullName string, pullNum int) (func(), error) {
// todo: implement
return func() {}, nil
}
func (d *DefaultWorkingDirLocker) TryLock(repoFullName string, workspace string, pullNum int) (func(), error) {
d.mutex.Lock()
defer d.mutex.Unlock()

View File

@@ -104,7 +104,7 @@ func (l *LocksController) DeleteLock(w http.ResponseWriter, r *http.Request) {
// Once the lock has been deleted, comment back on the pull request.
comment := fmt.Sprintf("**Warning**: The plan for dir: `%s` workspace: `%s` was **discarded** via the Atlantis UI.\n\n"+
"To `apply` you must run `plan` again.", lock.Project.Path, lock.Workspace)
"To `apply` this plan you must run `plan` again.", lock.Project.Path, lock.Workspace)
err = l.VCSClient.CreateComment(lock.Pull.BaseRepo, lock.Pull.Num, comment)
if err != nil {
l.respond(w, logging.Error, http.StatusInternalServerError, "Failed commenting on pull request: %s", err)

View File

@@ -245,6 +245,6 @@ func TestDeleteLock_CommentSuccess(t *testing.T) {
responseContains(t, w, http.StatusOK, "Deleted lock id \"id\"")
cp.VerifyWasCalled(Once()).CreateComment(pull.BaseRepo, pull.Num,
"**Warning**: The plan for dir: `path` workspace: `workspace` was **discarded** via the Atlantis UI.\n\n"+
"To `apply` you must run `plan` again.")
"To `apply` this plan you must run `plan` again.")
workingDir.VerifyWasCalledOnce().DeleteForWorkspace(pull.BaseRepo, pull, "workspace")
}