Skip to content

Commit

Permalink
feat: add new flag --silence-no-projects (#1469)
Browse files Browse the repository at this point in the history
* feat: add new flag --silence-no-projects

* refactor: add tests, clean up command_runner logic

* fix: attempt at fixing PolicyCheck e2e test

* docs: add silence-no-projects flag to server-configuration.md

* docs: fix grammar

* fix: requested changes: commit status resets, misc fixes

* fix: status check comments to actually reflect their commands

* fix: logic bug in autoplan commit reset

* fix: tests make check-lint errors
  • Loading branch information
GenPage authored Apr 19, 2021
1 parent 3bb6f21 commit 25103d7
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 78 deletions.
5 changes: 5 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const (
RepoAllowlistFlag = "repo-allowlist"
RequireApprovalFlag = "require-approval"
RequireMergeableFlag = "require-mergeable"
SilenceNoProjectsFlag = "silence-no-projects"
SilenceForkPRErrorsFlag = "silence-fork-pr-errors"
SilenceVCSStatusNoPlans = "silence-vcs-status-no-plans"
SilenceAllowlistErrorsFlag = "silence-allowlist-errors"
Expand Down Expand Up @@ -324,6 +325,10 @@ var boolFlags = map[string]boolFlag{
defaultValue: false,
hidden: true,
},
SilenceNoProjectsFlag: {
description: "Silences Atlants from responding to PRs when it finds no projects.",
defaultValue: false,
},
SilenceForkPRErrorsFlag: {
description: "Silences the posting of fork pull requests not allowed error comments.",
defaultValue: false,
Expand Down
1 change: 1 addition & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ var testFlags = map[string]interface{}{
RepoAllowlistFlag: "github.com/runatlantis/atlantis",
RequireApprovalFlag: true,
RequireMergeableFlag: true,
SilenceNoProjectsFlag: false,
SilenceForkPRErrorsFlag: true,
SilenceAllowlistErrorsFlag: true,
SilenceVCSStatusNoPlans: true,
Expand Down
9 changes: 9 additions & 0 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,15 @@ Values are chosen in this order:
Some users find this useful because they prefer to add the Atlantis webhook
at an organization level rather than on each repo.

* ### `--silence-no-projects`
```bash
atlantis server --silence-no-projects
```
`--silence-no-projects` will tell Atlantis to ignore PRs if none of the modified files are part of a project defined in the `atlantis.yaml` file.

This is useful when running multiple Atlantis servers against a single repository so you can
delegate work to each Atlantis server. Also useful when used with pre_workflow_hooks to dynamically generate an `atlantis.yaml` file.

* ### `--skip-clone-no-changes`
```bash
atlantis server --skip-clone-no-changes
Expand Down
57 changes: 41 additions & 16 deletions server/events/apply_command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,23 @@ func NewApplyCommandRunner(
dbUpdater *DBUpdater,
db *db.BoltDB,
parallelPoolSize int,
SilenceNoProjects bool,
silenceVCSStatusNoProjects bool,
) *ApplyCommandRunner {
return &ApplyCommandRunner{
vcsClient: vcsClient,
DisableApplyAll: disableApplyAll,
locker: applyCommandLocker,
commitStatusUpdater: commitStatusUpdater,
prjCmdBuilder: prjCommandBuilder,
prjCmdRunner: prjCmdRunner,
autoMerger: autoMerger,
pullUpdater: pullUpdater,
dbUpdater: dbUpdater,
DB: db,
parallelPoolSize: parallelPoolSize,
vcsClient: vcsClient,
DisableApplyAll: disableApplyAll,
locker: applyCommandLocker,
commitStatusUpdater: commitStatusUpdater,
prjCmdBuilder: prjCommandBuilder,
prjCmdRunner: prjCmdRunner,
autoMerger: autoMerger,
pullUpdater: pullUpdater,
dbUpdater: dbUpdater,
DB: db,
parallelPoolSize: parallelPoolSize,
SilenceNoProjects: SilenceNoProjects,
silenceVCSStatusNoProjects: silenceVCSStatusNoProjects,
}
}

Expand All @@ -47,6 +51,12 @@ type ApplyCommandRunner struct {
pullUpdater *PullUpdater
dbUpdater *DBUpdater
parallelPoolSize int
// SilenceNoProjects is whether Atlantis should respond to PRs if no projects
// are found
SilenceNoProjects bool
// SilenceVCSStatusNoPlans is whether any plan should set commit status if no projects
// are found
silenceVCSStatusNoProjects bool
}

func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {
Expand All @@ -56,7 +66,7 @@ func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {

locked, err := a.IsLocked()
// CheckApplyLock falls back to DisableApply flag if fetching the lock
// raises an erro r
// raises an error
// We will log failure as warning
if err != nil {
ctx.Log.Warn("checking global apply lock: %s", err)
Expand All @@ -80,6 +90,10 @@ func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {
return
}

if err = a.commitStatusUpdater.UpdateCombined(baseRepo, pull, models.PendingCommitStatus, cmd.CommandName()); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}

// Get the mergeable status before we set any build statuses of our own.
// We do this here because when we set a "Pending" status, if users have
// required the Atlantis status checks to pass, then we've now changed
Expand All @@ -95,10 +109,6 @@ func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {

ctx.Log.Info("pull request mergeable status: %t", ctx.PullMergeable)

if err = a.commitStatusUpdater.UpdateCombined(baseRepo, pull, models.PendingCommitStatus, cmd.CommandName()); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}

var projectCmds []models.ProjectCommandContext
projectCmds, err = a.prjCmdBuilder.BuildApplyCommands(ctx, cmd)

Expand All @@ -110,6 +120,21 @@ func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {
return
}

// If there are no projects to apply, don't respond to the PR and ignore
if len(projectCmds) == 0 && a.SilenceNoProjects {
ctx.Log.Info("determined there was no project to run apply in.")
if !a.silenceVCSStatusNoProjects {
// If there were no projects modified, we set successful commit statuses
// with 0/0 projects applied successfully because some users require
// the Atlantis status to be passing for all pull requests.
ctx.Log.Debug("setting VCS status to success with no projects found")
if err := a.commitStatusUpdater.UpdateCombinedCount(baseRepo, pull, models.SuccessCommitStatus, models.ApplyCommand, 0, 0); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}
}
return
}

// Only run commands in parallel if enabled
var result CommandResult
if a.isParallelEnabled(projectCmds) {
Expand Down
36 changes: 29 additions & 7 deletions server/events/approve_policies_command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ func NewApprovePoliciesCommandRunner(
prjCommandRunner ProjectApprovePoliciesCommandRunner,
pullUpdater *PullUpdater,
dbUpdater *DBUpdater,
SilenceNoProjects bool,
silenceVCSStatusNoProjects bool,
) *ApprovePoliciesCommandRunner {
return &ApprovePoliciesCommandRunner{
commitStatusUpdater: commitStatusUpdater,
prjCmdBuilder: prjCommandBuilder,
prjCmdRunner: prjCommandRunner,
pullUpdater: pullUpdater,
dbUpdater: dbUpdater,
commitStatusUpdater: commitStatusUpdater,
prjCmdBuilder: prjCommandBuilder,
prjCmdRunner: prjCommandRunner,
pullUpdater: pullUpdater,
dbUpdater: dbUpdater,
SilenceNoProjects: SilenceNoProjects,
silenceVCSStatusNoProjects: silenceVCSStatusNoProjects,
}
}

Expand All @@ -28,25 +32,43 @@ type ApprovePoliciesCommandRunner struct {
dbUpdater *DBUpdater
prjCmdBuilder ProjectApprovePoliciesCommandBuilder
prjCmdRunner ProjectApprovePoliciesCommandRunner
// SilenceNoProjects is whether Atlantis should respond to PRs if no projects
// are found
SilenceNoProjects bool
silenceVCSStatusNoProjects bool
}

func (a *ApprovePoliciesCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {
baseRepo := ctx.Pull.BaseRepo
pull := ctx.Pull

if err := a.commitStatusUpdater.UpdateCombined(baseRepo, pull, models.PendingCommitStatus, models.PolicyCheckCommand); err != nil {
if err := a.commitStatusUpdater.UpdateCombined(baseRepo, pull, models.PendingCommitStatus, models.ApprovePoliciesCommand); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}

projectCmds, err := a.prjCmdBuilder.BuildApprovePoliciesCommands(ctx, cmd)
if err != nil {
if statusErr := a.commitStatusUpdater.UpdateCombined(ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, models.PolicyCheckCommand); statusErr != nil {
if statusErr := a.commitStatusUpdater.UpdateCombined(ctx.Pull.BaseRepo, ctx.Pull, models.FailedCommitStatus, models.ApprovePoliciesCommand); statusErr != nil {
ctx.Log.Warn("unable to update commit status: %s", statusErr)
}
a.pullUpdater.updatePull(ctx, cmd, CommandResult{Error: err})
return
}

if len(projectCmds) == 0 && a.SilenceNoProjects {
ctx.Log.Info("determined there was no project to run approve_policies in")
if !a.silenceVCSStatusNoProjects {
// If there were no projects modified, we set successful commit statuses
// with 0/0 projects approve_policies successfully because some users require
// the Atlantis status to be passing for all pull requests.
ctx.Log.Debug("setting VCS status to success with no projects found")
if err := a.commitStatusUpdater.UpdateCombinedCount(ctx.Pull.BaseRepo, ctx.Pull, models.SuccessCommitStatus, models.ApprovePoliciesCommand, 0, 0); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}
}
return
}

result := a.buildApprovePolicyCommandResults(ctx, projectCmds)

a.pullUpdater.updatePull(
Expand Down
87 changes: 86 additions & 1 deletion server/events/command_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,18 @@ func setup(t *testing.T) *vcsmocks.MockClient {
}

parallelPoolSize := 1
SilenceNoProjects := false
policyCheckCommandRunner = events.NewPolicyCheckCommandRunner(
dbUpdater,
pullUpdater,
commitUpdater,
projectCommandRunner,
parallelPoolSize,
false,
)

planCommandRunner = events.NewPlanCommandRunner(
false,
false,
vcsClient,
pendingPlanFinder,
Expand All @@ -123,6 +126,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
policyCheckCommandRunner,
autoMerger,
parallelPoolSize,
SilenceNoProjects,
defaultBoltDB,
)

Expand All @@ -138,6 +142,8 @@ func setup(t *testing.T) *vcsmocks.MockClient {
dbUpdater,
defaultBoltDB,
parallelPoolSize,
SilenceNoProjects,
false,
)

approvePoliciesCommandRunner = events.NewApprovePoliciesCommandRunner(
Expand All @@ -146,11 +152,14 @@ func setup(t *testing.T) *vcsmocks.MockClient {
projectCommandRunner,
pullUpdater,
dbUpdater,
SilenceNoProjects,
false,
)

unlockCommandRunner = events.NewUnlockCommandRunner(
deleteLockCommand,
vcsClient,
SilenceNoProjects,
)

commentCommandRunnerByCmd := map[models.CommandName]events.CommentCommandRunner{
Expand Down Expand Up @@ -259,6 +268,82 @@ func TestRunCommentCommand_ForkPRDisabled_SilenceEnabled(t *testing.T) {
vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString())
}

func TestRunCommentCommandPlan_NoProjects_SilenceEnabled(t *testing.T) {
t.Log("if a plan command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project")
vcsClient := setup(t)
planCommandRunner.SilenceNoProjects = true
var pull github.PullRequest
modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState}
When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil)
When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil)

ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.PlanCommand})
vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString())
commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
matchers.EqModelsCommitStatus(models.SuccessCommitStatus),
matchers.EqModelsCommandName(models.PlanCommand),
EqInt(0),
EqInt(0),
)
}

func TestRunCommentCommandApply_NoProjects_SilenceEnabled(t *testing.T) {
t.Log("if an apply command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project")
vcsClient := setup(t)
applyCommandRunner.SilenceNoProjects = true
var pull github.PullRequest
modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState}
When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil)
When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil)

ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.ApplyCommand})
vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString())
commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
matchers.EqModelsCommitStatus(models.SuccessCommitStatus),
matchers.EqModelsCommandName(models.ApplyCommand),
EqInt(0),
EqInt(0),
)
}

func TestRunCommentCommandApprovePolicy_NoProjects_SilenceEnabled(t *testing.T) {
t.Log("if an approve_policy command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project")
vcsClient := setup(t)
approvePoliciesCommandRunner.SilenceNoProjects = true
var pull github.PullRequest
modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState}
When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil)
When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil)

ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.ApprovePoliciesCommand})
vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString())
commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
matchers.EqModelsCommitStatus(models.SuccessCommitStatus),
matchers.EqModelsCommandName(models.ApprovePoliciesCommand),
EqInt(0),
EqInt(0),
)
}

func TestRunCommentCommandUnlock_NoProjects_SilenceEnabled(t *testing.T) {
t.Log("if an unlock command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project")
vcsClient := setup(t)
unlockCommandRunner.SilenceNoProjects = true
var pull github.PullRequest
modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState}
When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(&pull, nil)
When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil)

ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.UnlockCommand})
vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString())
}

func TestRunCommentCommand_DisableApplyAllDisabled(t *testing.T) {
t.Log("if \"atlantis apply\" is run and this is disabled atlantis should" +
" comment saying that this is not allowed")
Expand Down Expand Up @@ -339,7 +424,7 @@ func TestRunUnlockCommandFail_VCSComment(t *testing.T) {
modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState, Num: fixtures.Pull.Num}
When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil)
When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil)
When(deleteLockCommand.DeleteLocksByPull(fixtures.GithubRepo.FullName, fixtures.Pull.Num)).ThenReturn(errors.New("err"))
When(deleteLockCommand.DeleteLocksByPull(fixtures.GithubRepo.FullName, fixtures.Pull.Num)).ThenReturn(0, errors.New("err"))

ch.RunCommentCommand(fixtures.GithubRepo, &fixtures.GithubRepo, nil, fixtures.User, fixtures.Pull.Num, &events.CommentCommand{Name: models.UnlockCommand})

Expand Down
15 changes: 8 additions & 7 deletions server/events/delete_lock_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
// DeleteLockCommand is the first step after a command request has been parsed.
type DeleteLockCommand interface {
DeleteLock(id string) (*models.ProjectLock, error)
DeleteLocksByPull(repoFullName string, pullNum int) error
DeleteLocksByPull(repoFullName string, pullNum int) (int, error)
}

// DefaultDeleteLockCommand deletes a specific lock after a request from the LocksController.
Expand All @@ -39,22 +39,23 @@ func (l *DefaultDeleteLockCommand) DeleteLock(id string) (*models.ProjectLock, e
}

// DeleteLocksByPull handles deleting all locks for the pull request
func (l *DefaultDeleteLockCommand) DeleteLocksByPull(repoFullName string, pullNum int) error {
func (l *DefaultDeleteLockCommand) DeleteLocksByPull(repoFullName string, pullNum int) (int, error) {
locks, err := l.Locker.UnlockByPull(repoFullName, pullNum)
numLocks := len(locks)
if err != nil {
return err
return numLocks, err
}
if len(locks) == 0 {
if numLocks == 0 {
l.Logger.Debug("No locks found for pull")
return nil
return numLocks, nil
}

for i := 0; i < len(locks); i++ {
for i := 0; i < numLocks; i++ {
lock := locks[i]
l.deleteWorkingDir(lock)
}

return nil
return numLocks, nil
}

func (l *DefaultDeleteLockCommand) deleteWorkingDir(lock models.ProjectLock) {
Expand Down
Loading

0 comments on commit 25103d7

Please sign in to comment.