diff --git a/server/events/command_runner.go b/server/events/command_runner.go index a72bb9e9f..497c27bdf 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -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) { diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 6d96008ce..59a9eb197 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -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()) -} diff --git a/server/events/mocks/matchers/events_projectresult.go b/server/events/mocks/matchers/events_projectresult.go new file mode 100644 index 000000000..2006d2242 --- /dev/null +++ b/server/events/mocks/matchers/events_projectresult.go @@ -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 +} diff --git a/server/events/mocks/mock_project_command_builder.go b/server/events/mocks/mock_project_command_builder.go index f779acab3..236743e81 100644 --- a/server/events/mocks/mock_project_command_builder.go +++ b/server/events/mocks/mock_project_command_builder.go @@ -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])) diff --git a/server/events/mocks/mock_project_command_runner.go b/server/events/mocks/mock_project_command_runner.go index cf5555e8d..3f15b35fb 100644 --- a/server/events/mocks/mock_project_command_runner.go +++ b/server/events/mocks/mock_project_command_runner.go @@ -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 diff --git a/server/events/mocks/mock_working_dir.go b/server/events/mocks/mock_working_dir.go index 8a84ea236..3f7b8f0e9 100644 --- a/server/events/mocks/mock_working_dir.go +++ b/server/events/mocks/mock_working_dir.go @@ -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) diff --git a/server/events/models/models.go b/server/events/models/models.go index 668f26da3..0a55621c6 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -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 } diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 2f504a9bb..d07ef3e97 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -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 diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index 7be434b26..86a3adcb6 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -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) diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index 1da06d1ad..f8210bd8a 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -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) { diff --git a/server/events/project_finder.go b/server/events/project_finder.go index 07dfcd6c6..893f8ea96 100644 --- a/server/events/project_finder.go +++ b/server/events/project_finder.go @@ -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. diff --git a/server/events/working_dir.go b/server/events/working_dir.go index f8423af46..25521d7ad 100644 --- a/server/events/working_dir.go +++ b/server/events/working_dir.go @@ -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)) diff --git a/server/events/working_dir_locker.go b/server/events/working_dir_locker.go index 6c5559b41..c503f9bdf 100644 --- a/server/events/working_dir_locker.go +++ b/server/events/working_dir_locker.go @@ -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() diff --git a/server/locks_controller.go b/server/locks_controller.go index 751cee31a..fc11eb044 100644 --- a/server/locks_controller.go +++ b/server/locks_controller.go @@ -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) diff --git a/server/locks_controller_test.go b/server/locks_controller_test.go index c7d6c1fa2..ce6929e49 100644 --- a/server/locks_controller_test.go +++ b/server/locks_controller_test.go @@ -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") }