diff --git a/runatlantis.io/docs/policy-checking.md b/runatlantis.io/docs/policy-checking.md
index 25fd71f907..297614215e 100644
--- a/runatlantis.io/docs/policy-checking.md
+++ b/runatlantis.io/docs/policy-checking.md
@@ -17,10 +17,6 @@ Enabling "policy checking" in addition to the [mergeable apply requirement](http
![Policy Check Apply Status Failure](./images/policy-check-apply-status-failure.png)
-:::warning
-Without the mergeable requirement applies will still go through in the event of a policy failure.
-:::
-
Any failures need to either be addressed in a successive commit, or approved by a blessed owner. This approval is independent of the approval apply requirement which can coexist in the policy checking workflow. After an approval, the apply can proceed.
![Policy Check Approval](./images/policy-check-approval.png)
diff --git a/server/events/apply_command_runner.go b/server/events/apply_command_runner.go
index 2ccc75bf0f..7327c326dc 100644
--- a/server/events/apply_command_runner.go
+++ b/server/events/apply_command_runner.go
@@ -93,13 +93,6 @@ func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {
ctx.Log.Warn("unable to get mergeable status: %s. Continuing with mergeable assumed false", err)
}
- // TODO: This needs to be revisited and new PullMergeable like conditions should
- // be added to check against it.
- if a.anyFailedPolicyChecks(pull) {
- ctx.PullMergeable = false
- ctx.Log.Warn("when using policy checks all policies have to be approved or pass. Continuing with mergeable assumed false")
- }
-
ctx.Log.Info("pull request mergeable status: %t", ctx.PullMergeable)
if err = a.commitStatusUpdater.UpdateCombined(baseRepo, pull, models.PendingCommitStatus, cmd.CommandName()); err != nil {
@@ -182,16 +175,6 @@ func (a *ApplyCommandRunner) updateCommitStatus(ctx *CommandContext, pullStatus
}
}
-func (a *ApplyCommandRunner) anyFailedPolicyChecks(pull models.PullRequest) bool {
- policyCheckPullStatus, _ := a.DB.GetPullStatus(pull)
- if policyCheckPullStatus != nil && policyCheckPullStatus.StatusCount(models.ErroredPolicyCheckStatus) > 0 {
- return true
- }
-
- return false
-
-}
-
// applyAllDisabledComment is posted when apply all commands (i.e. "atlantis apply")
// are disabled and an apply all command is issued.
var applyAllDisabledComment = "**Error:** Running `atlantis apply` without flags is disabled." +
diff --git a/server/events/command_context.go b/server/events/command_context.go
index de48295dd0..a4d6ebb13a 100644
--- a/server/events/command_context.go
+++ b/server/events/command_context.go
@@ -46,5 +46,7 @@ type CommandContext struct {
// required the Atlantis status to be successful prior to merging.
PullMergeable bool
+ PullStatus *models.PullStatus
+
Trigger CommandTrigger
}
diff --git a/server/events/command_runner.go b/server/events/command_runner.go
index a851b85cbf..1167b85c71 100644
--- a/server/events/command_runner.go
+++ b/server/events/command_runner.go
@@ -113,6 +113,7 @@ type DefaultCommandRunner struct {
CommentCommandRunnerByCmd map[models.CommandName]CommentCommandRunner
Drainer *Drainer
PreWorkflowHooksCommandRunner PreWorkflowHooksCommandRunner
+ PullStatusFetcher PullStatusFetcher
}
// RunAutoplanCommand runs plan and policy_checks when a pull request is opened or updated.
@@ -127,12 +128,19 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo
log := c.buildLogger(baseRepo.FullName, pull.Num)
defer c.logPanics(baseRepo, pull.Num, log)
+ status, err := c.PullStatusFetcher.GetPullStatus(pull)
+
+ if err != nil {
+ log.Err("Unable to fetch pull status, this is likely a bug.", err)
+ }
+
ctx := &CommandContext{
- User: user,
- Log: log,
- Pull: pull,
- HeadRepo: headRepo,
- Trigger: Auto,
+ User: user,
+ Log: log,
+ Pull: pull,
+ HeadRepo: headRepo,
+ PullStatus: status,
+ Trigger: Auto,
}
if !c.validateCtxAndComment(ctx) {
return
@@ -141,7 +149,7 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo
return
}
- err := c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx)
+ err = c.PreWorkflowHooksCommandRunner.RunPreHooks(ctx)
if err != nil {
ctx.Log.Err("Error running pre-workflow hooks %s. Proceeding with %s command.", err, models.PlanCommand)
@@ -174,12 +182,19 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
return
}
+ status, err := c.PullStatusFetcher.GetPullStatus(pull)
+
+ if err != nil {
+ log.Err("Unable to fetch pull status, this is likely a bug.", err)
+ }
+
ctx := &CommandContext{
- User: user,
- Log: log,
- Pull: pull,
- HeadRepo: headRepo,
- Trigger: Comment,
+ User: user,
+ Log: log,
+ Pull: pull,
+ PullStatus: status,
+ HeadRepo: headRepo,
+ Trigger: Comment,
}
if !c.validateCtxAndComment(ctx) {
diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go
index 15d97be5ff..cf53479c98 100644
--- a/server/events/command_runner_test.go
+++ b/server/events/command_runner_test.go
@@ -123,6 +123,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
policyCheckCommandRunner,
autoMerger,
parallelPoolSize,
+ defaultBoltDB,
)
applyCommandRunner = events.NewApplyCommandRunner(
@@ -175,6 +176,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
AllowForkPRsFlag: "allow-fork-prs-flag",
Drainer: drainer,
PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner,
+ PullStatusFetcher: defaultBoltDB,
}
return vcsClient
}
@@ -531,16 +533,14 @@ func TestApplyMergeablityWhenPolicyCheckFails(t *testing.T) {
When(ch.VCSClient.PullIsMergeable(fixtures.GithubRepo, modelPull)).ThenReturn(true, nil)
When(projectCommandBuilder.BuildApplyCommands(matchers.AnyPtrToEventsCommandContext(), matchers.AnyPtrToEventsCommentCommand())).Then(func(args []Param) ReturnValues {
- ctx := args[0].(*events.CommandContext)
- Equals(t, false, ctx.PullMergeable)
-
return ReturnValues{
[]models.ProjectCommandContext{
{
- CommandName: models.ApplyCommand,
- ProjectName: "default",
- Workspace: "default",
- RepoRelDir: ".",
+ CommandName: models.ApplyCommand,
+ ProjectName: "default",
+ Workspace: "default",
+ RepoRelDir: ".",
+ ProjectPlanStatus: models.ErroredPolicyCheckStatus,
},
},
nil,
diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go
index b0c7f58c72..4613150236 100644
--- a/server/events/comment_parser.go
+++ b/server/events/comment_parser.go
@@ -57,6 +57,8 @@ type CommentParsing interface {
Parse(comment string, vcsHost models.VCSHostType) CommentParseResult
}
+//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_comment_building.go CommentBuilder
+
// CommentBuilder builds comment commands that can be used on pull requests.
type CommentBuilder interface {
// BuildPlanComment builds a plan comment for the specified args.
diff --git a/server/events/mocks/matchers/events_commentparseresult.go b/server/events/mocks/matchers/events_commentparseresult.go
index bcdd017764..194da76f3f 100644
--- a/server/events/mocks/matchers/events_commentparseresult.go
+++ b/server/events/mocks/matchers/events_commentparseresult.go
@@ -3,8 +3,9 @@ package matchers
import (
"github.com/petergtz/pegomock"
- events "github.com/runatlantis/atlantis/server/events"
"reflect"
+
+ events "github.com/runatlantis/atlantis/server/events"
)
func AnyEventsCommentParseResult() events.CommentParseResult {
@@ -18,3 +19,15 @@ func EqEventsCommentParseResult(value events.CommentParseResult) events.CommentP
var nullValue events.CommentParseResult
return nullValue
}
+
+func NotEqEventsCommentParseResult(value events.CommentParseResult) events.CommentParseResult {
+ pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
+ var nullValue events.CommentParseResult
+ return nullValue
+}
+
+func EventsCommentParseResultThat(matcher pegomock.ArgumentMatcher) events.CommentParseResult {
+ pegomock.RegisterMatcher(matcher)
+ var nullValue events.CommentParseResult
+ return nullValue
+}
diff --git a/server/events/mocks/matchers/models_vcshosttype.go b/server/events/mocks/matchers/models_vcshosttype.go
index f2fe24c138..a54447f7de 100644
--- a/server/events/mocks/matchers/models_vcshosttype.go
+++ b/server/events/mocks/matchers/models_vcshosttype.go
@@ -2,8 +2,9 @@
package matchers
import (
- "reflect"
"github.com/petergtz/pegomock"
+ "reflect"
+
models "github.com/runatlantis/atlantis/server/events/models"
)
@@ -18,3 +19,15 @@ func EqModelsVCSHostType(value models.VCSHostType) models.VCSHostType {
var nullValue models.VCSHostType
return nullValue
}
+
+func NotEqModelsVCSHostType(value models.VCSHostType) models.VCSHostType {
+ pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
+ var nullValue models.VCSHostType
+ return nullValue
+}
+
+func ModelsVCSHostTypeThat(matcher pegomock.ArgumentMatcher) models.VCSHostType {
+ pegomock.RegisterMatcher(matcher)
+ var nullValue models.VCSHostType
+ return nullValue
+}
diff --git a/server/events/mocks/matchers/slice_of_string.go b/server/events/mocks/matchers/slice_of_string.go
index 96f9b24ae2..f9281819dd 100644
--- a/server/events/mocks/matchers/slice_of_string.go
+++ b/server/events/mocks/matchers/slice_of_string.go
@@ -2,9 +2,8 @@
package matchers
import (
- "reflect"
"github.com/petergtz/pegomock"
-
+ "reflect"
)
func AnySliceOfString() []string {
@@ -18,3 +17,15 @@ func EqSliceOfString(value []string) []string {
var nullValue []string
return nullValue
}
+
+func NotEqSliceOfString(value []string) []string {
+ pegomock.RegisterMatcher(&pegomock.NotEqMatcher{Value: value})
+ var nullValue []string
+ return nullValue
+}
+
+func SliceOfStringThat(matcher pegomock.ArgumentMatcher) []string {
+ pegomock.RegisterMatcher(matcher)
+ var nullValue []string
+ return nullValue
+}
diff --git a/server/events/mocks/mock_comment_building.go b/server/events/mocks/mock_comment_building.go
new file mode 100644
index 0000000000..2d3fbbbcaa
--- /dev/null
+++ b/server/events/mocks/mock_comment_building.go
@@ -0,0 +1,166 @@
+// Code generated by pegomock. DO NOT EDIT.
+// Source: github.com/runatlantis/atlantis/server/events (interfaces: CommentBuilder)
+
+package mocks
+
+import (
+ pegomock "github.com/petergtz/pegomock"
+ "reflect"
+ "time"
+)
+
+type MockCommentBuilder struct {
+ fail func(message string, callerSkip ...int)
+}
+
+func NewMockCommentBuilder(options ...pegomock.Option) *MockCommentBuilder {
+ mock := &MockCommentBuilder{}
+ for _, option := range options {
+ option.Apply(mock)
+ }
+ return mock
+}
+
+func (mock *MockCommentBuilder) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
+func (mock *MockCommentBuilder) FailHandler() pegomock.FailHandler { return mock.fail }
+
+func (mock *MockCommentBuilder) BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) string {
+ if mock == nil {
+ panic("mock must not be nil. Use myMock := NewMockCommentBuilder().")
+ }
+ params := []pegomock.Param{repoRelDir, workspace, project, commentArgs}
+ result := pegomock.GetGenericMockFrom(mock).Invoke("BuildPlanComment", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})
+ var ret0 string
+ if len(result) != 0 {
+ if result[0] != nil {
+ ret0 = result[0].(string)
+ }
+ }
+ return ret0
+}
+
+func (mock *MockCommentBuilder) BuildApplyComment(repoRelDir string, workspace string, project string) string {
+ if mock == nil {
+ panic("mock must not be nil. Use myMock := NewMockCommentBuilder().")
+ }
+ params := []pegomock.Param{repoRelDir, workspace, project}
+ result := pegomock.GetGenericMockFrom(mock).Invoke("BuildApplyComment", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()})
+ var ret0 string
+ if len(result) != 0 {
+ if result[0] != nil {
+ ret0 = result[0].(string)
+ }
+ }
+ return ret0
+}
+
+func (mock *MockCommentBuilder) VerifyWasCalledOnce() *VerifierMockCommentBuilder {
+ return &VerifierMockCommentBuilder{
+ mock: mock,
+ invocationCountMatcher: pegomock.Times(1),
+ }
+}
+
+func (mock *MockCommentBuilder) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCommentBuilder {
+ return &VerifierMockCommentBuilder{
+ mock: mock,
+ invocationCountMatcher: invocationCountMatcher,
+ }
+}
+
+func (mock *MockCommentBuilder) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCommentBuilder {
+ return &VerifierMockCommentBuilder{
+ mock: mock,
+ invocationCountMatcher: invocationCountMatcher,
+ inOrderContext: inOrderContext,
+ }
+}
+
+func (mock *MockCommentBuilder) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCommentBuilder {
+ return &VerifierMockCommentBuilder{
+ mock: mock,
+ invocationCountMatcher: invocationCountMatcher,
+ timeout: timeout,
+ }
+}
+
+type VerifierMockCommentBuilder struct {
+ mock *MockCommentBuilder
+ invocationCountMatcher pegomock.InvocationCountMatcher
+ inOrderContext *pegomock.InOrderContext
+ timeout time.Duration
+}
+
+func (verifier *VerifierMockCommentBuilder) BuildPlanComment(repoRelDir string, workspace string, project string, commentArgs []string) *MockCommentBuilder_BuildPlanComment_OngoingVerification {
+ params := []pegomock.Param{repoRelDir, workspace, project, commentArgs}
+ methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildPlanComment", params, verifier.timeout)
+ return &MockCommentBuilder_BuildPlanComment_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
+}
+
+type MockCommentBuilder_BuildPlanComment_OngoingVerification struct {
+ mock *MockCommentBuilder
+ methodInvocations []pegomock.MethodInvocation
+}
+
+func (c *MockCommentBuilder_BuildPlanComment_OngoingVerification) GetCapturedArguments() (string, string, string, []string) {
+ repoRelDir, workspace, project, commentArgs := c.GetAllCapturedArguments()
+ return repoRelDir[len(repoRelDir)-1], workspace[len(workspace)-1], project[len(project)-1], commentArgs[len(commentArgs)-1]
+}
+
+func (c *MockCommentBuilder_BuildPlanComment_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 []string, _param3 [][]string) {
+ params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
+ if len(params) > 0 {
+ _param0 = make([]string, len(c.methodInvocations))
+ for u, param := range params[0] {
+ _param0[u] = param.(string)
+ }
+ _param1 = make([]string, len(c.methodInvocations))
+ for u, param := range params[1] {
+ _param1[u] = param.(string)
+ }
+ _param2 = make([]string, len(c.methodInvocations))
+ for u, param := range params[2] {
+ _param2[u] = param.(string)
+ }
+ _param3 = make([][]string, len(c.methodInvocations))
+ for u, param := range params[3] {
+ _param3[u] = param.([]string)
+ }
+ }
+ return
+}
+
+func (verifier *VerifierMockCommentBuilder) BuildApplyComment(repoRelDir string, workspace string, project string) *MockCommentBuilder_BuildApplyComment_OngoingVerification {
+ params := []pegomock.Param{repoRelDir, workspace, project}
+ methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "BuildApplyComment", params, verifier.timeout)
+ return &MockCommentBuilder_BuildApplyComment_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
+}
+
+type MockCommentBuilder_BuildApplyComment_OngoingVerification struct {
+ mock *MockCommentBuilder
+ methodInvocations []pegomock.MethodInvocation
+}
+
+func (c *MockCommentBuilder_BuildApplyComment_OngoingVerification) GetCapturedArguments() (string, string, string) {
+ repoRelDir, workspace, project := c.GetAllCapturedArguments()
+ return repoRelDir[len(repoRelDir)-1], workspace[len(workspace)-1], project[len(project)-1]
+}
+
+func (c *MockCommentBuilder_BuildApplyComment_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 []string) {
+ params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
+ if len(params) > 0 {
+ _param0 = make([]string, len(c.methodInvocations))
+ for u, param := range params[0] {
+ _param0[u] = param.(string)
+ }
+ _param1 = make([]string, len(c.methodInvocations))
+ for u, param := range params[1] {
+ _param1[u] = param.(string)
+ }
+ _param2 = make([]string, len(c.methodInvocations))
+ for u, param := range params[2] {
+ _param2[u] = param.(string)
+ }
+ }
+ return
+}
diff --git a/server/events/mocks/mock_comment_parsing.go b/server/events/mocks/mock_comment_parsing.go
index 24e4281ff4..f92f3abaad 100644
--- a/server/events/mocks/mock_comment_parsing.go
+++ b/server/events/mocks/mock_comment_parsing.go
@@ -4,12 +4,11 @@
package mocks
import (
- "reflect"
- "time"
-
pegomock "github.com/petergtz/pegomock"
events "github.com/runatlantis/atlantis/server/events"
models "github.com/runatlantis/atlantis/server/events/models"
+ "reflect"
+ "time"
)
type MockCommentParsing struct {
@@ -49,14 +48,14 @@ func (mock *MockCommentParsing) VerifyWasCalledOnce() *VerifierMockCommentParsin
}
}
-func (mock *MockCommentParsing) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockCommentParsing {
+func (mock *MockCommentParsing) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockCommentParsing {
return &VerifierMockCommentParsing{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
}
}
-func (mock *MockCommentParsing) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCommentParsing {
+func (mock *MockCommentParsing) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockCommentParsing {
return &VerifierMockCommentParsing{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
@@ -64,7 +63,7 @@ func (mock *MockCommentParsing) VerifyWasCalledInOrder(invocationCountMatcher pe
}
}
-func (mock *MockCommentParsing) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockCommentParsing {
+func (mock *MockCommentParsing) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockCommentParsing {
return &VerifierMockCommentParsing{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
@@ -74,7 +73,7 @@ func (mock *MockCommentParsing) VerifyWasCalledEventually(invocationCountMatcher
type VerifierMockCommentParsing struct {
mock *MockCommentParsing
- invocationCountMatcher pegomock.Matcher
+ invocationCountMatcher pegomock.InvocationCountMatcher
inOrderContext *pegomock.InOrderContext
timeout time.Duration
}
diff --git a/server/events/models/models.go b/server/events/models/models.go
index 4e449a3643..215d42540d 100644
--- a/server/events/models/models.go
+++ b/server/events/models/models.go
@@ -358,6 +358,8 @@ type ProjectCommandContext struct {
Log logging.SimpleLogging
// PullMergeable is true if the pull request for this project is able to be merged.
PullMergeable bool
+ // CurrentProjectPlanStatus is the status of the current project prior to this command.
+ ProjectPlanStatus ProjectPlanStatus
// Pull is the pull request we're responding to.
Pull PullRequest
// ProjectName is the name of the project set in atlantis.yaml. If there was
diff --git a/server/events/plan_command_runner.go b/server/events/plan_command_runner.go
index ae7c3d167e..c0547a5cc7 100644
--- a/server/events/plan_command_runner.go
+++ b/server/events/plan_command_runner.go
@@ -18,6 +18,7 @@ func NewPlanCommandRunner(
policyCheckCommandRunner *PolicyCheckCommandRunner,
autoMerger *AutoMerger,
parallelPoolSize int,
+ pullStatusFetcher PullStatusFetcher,
) *PlanCommandRunner {
return &PlanCommandRunner{
silenceVCSStatusNoPlans: silenceVCSStatusNoPlans,
@@ -32,6 +33,7 @@ func NewPlanCommandRunner(
policyCheckCommandRunner: policyCheckCommandRunner,
autoMerger: autoMerger,
parallelPoolSize: parallelPoolSize,
+ pullStatusFetcher: pullStatusFetcher,
}
}
@@ -50,6 +52,7 @@ type PlanCommandRunner struct {
policyCheckCommandRunner *PolicyCheckCommandRunner
autoMerger *AutoMerger
parallelPoolSize int
+ pullStatusFetcher PullStatusFetcher
}
func (p *PlanCommandRunner) runAutoplan(ctx *CommandContext) {
@@ -121,6 +124,13 @@ func (p *PlanCommandRunner) runAutoplan(ctx *CommandContext) {
!(result.HasErrors() || result.PlansDeleted) {
// Run policy_check command
ctx.Log.Info("Running policy_checks for all plans")
+
+ // refresh ctx's view of pull status since we just wrote to it.
+ // realistically each command should refresh this at the start,
+ // however, policy checking is weird since it's called within the plan command itself
+ // we need to better structure how this command works.
+ ctx.PullStatus = &pullStatus
+
p.policyCheckCommandRunner.Run(ctx, policyCheckCmds)
}
}
diff --git a/server/events/project_command_context_builder.go b/server/events/project_command_context_builder.go
index 40919264cd..13b43c9a19 100644
--- a/server/events/project_command_context_builder.go
+++ b/server/events/project_command_context_builder.go
@@ -147,6 +147,25 @@ func newProjectCommandContext(ctx *CommandContext,
parallelPlanEnabled bool,
verbose bool,
) models.ProjectCommandContext {
+
+ var projectPlanStatus models.ProjectPlanStatus
+
+ if ctx.PullStatus != nil {
+ for _, project := range ctx.PullStatus.Projects {
+
+ // if name is not used, let's match the directory
+ if projCfg.Name == "" && project.RepoRelDir == projCfg.RepoRelDir {
+ projectPlanStatus = project.Status
+ break
+ }
+
+ if projCfg.Name != "" && project.ProjectName == projCfg.Name {
+ projectPlanStatus = project.Status
+ break
+ }
+ }
+ }
+
return models.ProjectCommandContext{
CommandName: cmd,
ApplyCmd: applyCmd,
@@ -160,6 +179,7 @@ func newProjectCommandContext(ctx *CommandContext,
HeadRepo: ctx.HeadRepo,
Log: ctx.Log,
PullMergeable: ctx.PullMergeable,
+ ProjectPlanStatus: projectPlanStatus,
Pull: ctx.Pull,
ProjectName: projCfg.Name,
ApplyRequirements: projCfg.ApplyRequirements,
diff --git a/server/events/project_command_context_builder_test.go b/server/events/project_command_context_builder_test.go
index 79457f0dd2..c53408650d 100644
--- a/server/events/project_command_context_builder_test.go
+++ b/server/events/project_command_context_builder_test.go
@@ -1 +1,84 @@
package events_test
+
+import (
+ "testing"
+
+ . "github.com/petergtz/pegomock"
+ "github.com/runatlantis/atlantis/server/events"
+ "github.com/runatlantis/atlantis/server/events/mocks"
+ "github.com/runatlantis/atlantis/server/events/models"
+ "github.com/runatlantis/atlantis/server/events/yaml/valid"
+ "github.com/runatlantis/atlantis/server/logging"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestProjectCommandContextBuilder_PullStatus(t *testing.T) {
+
+ mockCommentBuilder := mocks.NewMockCommentBuilder()
+ subject := events.DefaultProjectCommandContextBuilder{
+ CommentBuilder: mockCommentBuilder,
+ }
+
+ projRepoRelDir := "dir1"
+ projWorkspace := "default"
+ projName := "project1"
+
+ projCfg := valid.MergedProjectCfg{
+ RepoRelDir: projRepoRelDir,
+ Workspace: projWorkspace,
+ Name: projName,
+ Workflow: valid.Workflow{
+ Name: valid.DefaultWorkflowName,
+ Apply: valid.DefaultApplyStage,
+ },
+ }
+
+ pullStatus := &models.PullStatus{
+ Projects: []models.ProjectStatus{},
+ }
+
+ commandCtx := &events.CommandContext{
+ Log: logging.NewNoopLogger(t),
+ PullStatus: pullStatus,
+ }
+
+ expectedApplyCmt := "Apply Comment"
+ expectedPlanCmt := "Plan Comment"
+
+ t.Run("with project name defined", func(t *testing.T) {
+ When(mockCommentBuilder.BuildPlanComment(projRepoRelDir, projWorkspace, projName, []string{})).ThenReturn(expectedPlanCmt)
+ When(mockCommentBuilder.BuildApplyComment(projRepoRelDir, projWorkspace, projName)).ThenReturn(expectedApplyCmt)
+
+ pullStatus.Projects = []models.ProjectStatus{
+ {
+ Status: models.ErroredPolicyCheckStatus,
+ ProjectName: "project1",
+ RepoRelDir: "dir1",
+ },
+ }
+
+ result := subject.BuildProjectContext(commandCtx, models.PlanCommand, projCfg, []string{}, "some/dir", false, false, false, false)
+
+ assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus)
+ })
+
+ t.Run("with no project name defined", func(t *testing.T) {
+ projCfg.Name = ""
+ When(mockCommentBuilder.BuildPlanComment(projRepoRelDir, projWorkspace, "", []string{})).ThenReturn(expectedPlanCmt)
+ When(mockCommentBuilder.BuildApplyComment(projRepoRelDir, projWorkspace, "")).ThenReturn(expectedApplyCmt)
+ pullStatus.Projects = []models.ProjectStatus{
+ {
+ Status: models.ErroredPlanStatus,
+ RepoRelDir: "dir2",
+ },
+ {
+ Status: models.ErroredPolicyCheckStatus,
+ RepoRelDir: "dir1",
+ },
+ }
+
+ result := subject.BuildProjectContext(commandCtx, models.PlanCommand, projCfg, []string{}, "some/dir", false, false, false, false)
+
+ assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus)
+ })
+}
diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go
index 3d3d72a832..496b995578 100644
--- a/server/events/project_command_runner.go
+++ b/server/events/project_command_runner.go
@@ -333,6 +333,11 @@ func (p *DefaultProjectCommandRunner) doApply(ctx models.ProjectCommandContext)
if !approved {
return "", "Pull request must be approved by at least one person other than the author before running apply.", nil
}
+ // this should come before mergeability check since mergeability is a superset of this check.
+ case valid.PoliciesPassedApplyReq:
+ if ctx.ProjectPlanStatus == models.ErroredPolicyCheckStatus {
+ return "", "All policies must pass for project before running apply", nil
+ }
case raw.MergeableApplyRequirement:
if !ctx.PullMergeable {
return "", "Pull request must be mergeable before running apply.", nil
diff --git a/server/events/pull_status_fetcher.go b/server/events/pull_status_fetcher.go
new file mode 100644
index 0000000000..126fed720e
--- /dev/null
+++ b/server/events/pull_status_fetcher.go
@@ -0,0 +1,8 @@
+package events
+
+import "github.com/runatlantis/atlantis/server/events/models"
+
+// PullStatusFetcher fetches our internal model of a pull requests status
+type PullStatusFetcher interface {
+ GetPullStatus(pull models.PullRequest) (*models.PullStatus, error)
+}
diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go
index 8968c92c70..21ffe793de 100644
--- a/server/events/yaml/parser_validator_test.go
+++ b/server/events/yaml/parser_validator_test.go
@@ -1191,8 +1191,7 @@ repos:
Repos: []valid.Repo{
defaultCfg.Repos[0],
{
- IDRegex: regexp.MustCompile("github.com/"),
- PreWorkflowHooks: []*valid.PreWorkflowHook{},
+ IDRegex: regexp.MustCompile("github.com/"),
},
},
Workflows: map[string]valid.Workflow{
@@ -1210,9 +1209,8 @@ repos:
Repos: []valid.Repo{
defaultCfg.Repos[0],
{
- ID: "github.com/owner/repo",
- PreWorkflowHooks: []*valid.PreWorkflowHook{},
- Workflow: defaultCfg.Repos[0].Workflow,
+ ID: "github.com/owner/repo",
+ Workflow: defaultCfg.Repos[0].Workflow,
},
},
Workflows: map[string]valid.Workflow{
@@ -1237,7 +1235,6 @@ workflows:
{
IDRegex: regexp.MustCompile(".*"),
BranchRegex: regexp.MustCompile(".*"),
- PreWorkflowHooks: []*valid.PreWorkflowHook{},
ApplyRequirements: []string{},
Workflow: &valid.Workflow{
Name: "default",
@@ -1430,7 +1427,6 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
{
IDRegex: regexp.MustCompile(".*"),
ApplyRequirements: []string{"mergeable", "approved"},
- PreWorkflowHooks: []*valid.PreWorkflowHook{},
Workflow: &customWorkflow,
AllowedWorkflows: []string{"custom"},
AllowedOverrides: []string{"workflow", "apply_requirements"},
@@ -1439,7 +1435,6 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) {
{
ID: "github.com/owner/repo",
IDRegex: nil,
- PreWorkflowHooks: []*valid.PreWorkflowHook{},
ApplyRequirements: nil,
AllowedOverrides: nil,
AllowCustomWorkflows: nil,
diff --git a/server/events/yaml/raw/global_cfg.go b/server/events/yaml/raw/global_cfg.go
index b48111505a..f0fefe84e8 100644
--- a/server/events/yaml/raw/global_cfg.go
+++ b/server/events/yaml/raw/global_cfg.go
@@ -86,6 +86,20 @@ func (g GlobalCfg) Validate() error {
func (g GlobalCfg) ToValid(defaultCfg valid.GlobalCfg) valid.GlobalCfg {
workflows := make(map[string]valid.Workflow)
+
+ // assumes: globalcfg is always initialized with one repo .*
+ applyReqs := defaultCfg.Repos[0].ApplyRequirements
+
+ var globalApplyReqs []string
+
+ for _, req := range applyReqs {
+ for _, nonOverrideableReq := range valid.NonOverrideableApplyReqs {
+ if req == nonOverrideableReq {
+ globalApplyReqs = append(globalApplyReqs, req)
+ }
+ }
+ }
+
for k, v := range g.Workflows {
validatedWorkflow := v.ToValid(k)
workflows[k] = validatedWorkflow
@@ -105,7 +119,7 @@ func (g GlobalCfg) ToValid(defaultCfg valid.GlobalCfg) valid.GlobalCfg {
var repos []valid.Repo
for _, r := range g.Repos {
- repos = append(repos, r.ToValid(workflows))
+ repos = append(repos, r.ToValid(workflows, globalApplyReqs))
}
repos = append(defaultCfg.Repos, repos...)
@@ -171,7 +185,7 @@ func (r Repo) Validate() error {
)
}
-func (r Repo) ToValid(workflows map[string]valid.Workflow) valid.Repo {
+func (r Repo) ToValid(workflows map[string]valid.Workflow, globalApplyReqs []string) valid.Repo {
var id string
var idRegex *regexp.Regexp
if r.HasRegexID() {
@@ -197,18 +211,33 @@ func (r Repo) ToValid(workflows map[string]valid.Workflow) valid.Repo {
workflow = &ptr
}
- preWorkflowHooks := make([]*valid.PreWorkflowHook, 0)
+ var preWorkflowHooks []*valid.PreWorkflowHook
if len(r.PreWorkflowHooks) > 0 {
for _, hook := range r.PreWorkflowHooks {
preWorkflowHooks = append(preWorkflowHooks, hook.ToValid())
}
}
+ var mergedApplyReqs []string
+
+ mergedApplyReqs = append(mergedApplyReqs, r.ApplyRequirements...)
+
+ // only add global reqs if they don't exist already.
+OUTER:
+ for _, globalReq := range globalApplyReqs {
+ for _, currReq := range r.ApplyRequirements {
+ if globalReq == currReq {
+ continue OUTER
+ }
+ }
+ mergedApplyReqs = append(mergedApplyReqs, globalReq)
+ }
+
return valid.Repo{
ID: id,
IDRegex: idRegex,
BranchRegex: branchRegex,
- ApplyRequirements: r.ApplyRequirements,
+ ApplyRequirements: mergedApplyReqs,
PreWorkflowHooks: preWorkflowHooks,
Workflow: workflow,
AllowedWorkflows: r.AllowedWorkflows,
diff --git a/server/events/yaml/valid/global_cfg.go b/server/events/yaml/valid/global_cfg.go
index fe6af76f82..f91ac4671d 100644
--- a/server/events/yaml/valid/global_cfg.go
+++ b/server/events/yaml/valid/global_cfg.go
@@ -11,6 +11,7 @@ import (
const MergeableApplyReq = "mergeable"
const ApprovedApplyReq = "approved"
+const PoliciesPassedApplyReq = "policies_passed"
const ApplyRequirementsKey = "apply_requirements"
const PreWorkflowHooksKey = "pre_workflow_hooks"
const WorkflowKey = "workflow"
@@ -19,6 +20,13 @@ const AllowedOverridesKey = "allowed_overrides"
const AllowCustomWorkflowsKey = "allow_custom_workflows"
const DefaultWorkflowName = "default"
+// NonOverrideableApplyReqs will get applied across all "repos" in the server side config.
+// If repo config is allowed overrides, they can override this.
+// TODO: Make this more customizable, not everyone wants this rigid workflow
+// maybe something along the lines of defining overridable/non-overrideable apply
+// requirements in the config and removing the flag to enable policy checking.
+var NonOverrideableApplyReqs []string = []string{PoliciesPassedApplyReq}
+
// GlobalCfg is the final parsed version of server-side repo config.
type GlobalCfg struct {
Repos []Repo
@@ -95,7 +103,40 @@ var DefaultPlanStage = Stage{
},
}
+// Deprecated: use NewGlobalCfgFromArgs
func NewGlobalCfgWithHooks(allowRepoCfg bool, mergeableReq bool, approvedReq bool, preWorkflowHooks []*PreWorkflowHook) GlobalCfg {
+ return NewGlobalCfgFromArgs(GlobalCfgArgs{
+ AllowRepoCfg: allowRepoCfg,
+ MergeableReq: mergeableReq,
+ ApprovedReq: approvedReq,
+ PreWorkflowHooks: preWorkflowHooks,
+ })
+}
+
+// NewGlobalCfg returns a global config that respects the parameters.
+// allowRepoCfg is true if users want to allow repos full config functionality.
+// mergeableReq is true if users want to set the mergeable apply requirement
+// for all repos.
+// approvedReq is true if users want to set the approved apply requirement
+// for all repos.
+// Deprecated: use NewGlobalCfgFromArgs
+func NewGlobalCfg(allowRepoCfg bool, mergeableReq bool, approvedReq bool) GlobalCfg {
+ return NewGlobalCfgFromArgs(GlobalCfgArgs{
+ AllowRepoCfg: allowRepoCfg,
+ MergeableReq: mergeableReq,
+ ApprovedReq: approvedReq,
+ })
+}
+
+type GlobalCfgArgs struct {
+ AllowRepoCfg bool
+ MergeableReq bool
+ ApprovedReq bool
+ PolicyCheckEnabled bool
+ PreWorkflowHooks []*PreWorkflowHook
+}
+
+func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg {
defaultWorkflow := Workflow{
Name: DefaultWorkflowName,
Apply: DefaultApplyStage,
@@ -107,15 +148,19 @@ func NewGlobalCfgWithHooks(allowRepoCfg bool, mergeableReq bool, approvedReq boo
applyReqs := []string{}
allowedOverrides := []string{}
allowedWorkflows := []string{}
- if mergeableReq {
+ if args.MergeableReq {
applyReqs = append(applyReqs, MergeableApplyReq)
}
- if approvedReq {
+ if args.ApprovedReq {
applyReqs = append(applyReqs, ApprovedApplyReq)
}
+ if args.PolicyCheckEnabled {
+ applyReqs = append(applyReqs, PoliciesPassedApplyReq)
+ }
+
allowCustomWorkflows := false
- if allowRepoCfg {
+ if args.AllowRepoCfg {
allowedOverrides = []string{ApplyRequirementsKey, WorkflowKey}
allowCustomWorkflows = true
}
@@ -126,7 +171,7 @@ func NewGlobalCfgWithHooks(allowRepoCfg bool, mergeableReq bool, approvedReq boo
IDRegex: regexp.MustCompile(".*"),
BranchRegex: regexp.MustCompile(".*"),
ApplyRequirements: applyReqs,
- PreWorkflowHooks: preWorkflowHooks,
+ PreWorkflowHooks: args.PreWorkflowHooks,
Workflow: &defaultWorkflow,
AllowedWorkflows: allowedWorkflows,
AllowedOverrides: allowedOverrides,
@@ -139,19 +184,6 @@ func NewGlobalCfgWithHooks(allowRepoCfg bool, mergeableReq bool, approvedReq boo
}
}
-// NewGlobalCfg returns a global config that respects the parameters.
-// allowRepoCfg is true if users want to allow repos full config functionality.
-// mergeableReq is true if users want to set the mergeable apply requirement
-// for all repos.
-// approvedReq is true if users want to set the approved apply requirement
-// for all repos.
-
-func NewGlobalCfg(allowRepoCfg bool, mergeableReq bool, approvedReq bool) GlobalCfg {
- preWorkflowHooks := make([]*PreWorkflowHook, 0)
-
- return NewGlobalCfgWithHooks(allowRepoCfg, mergeableReq, approvedReq, preWorkflowHooks)
-}
-
// IDMatches returns true if the repo ID otherID matches this config.
func (r Repo) IDMatches(otherID string) bool {
if r.ID != "" {
diff --git a/server/events/yaml/valid/global_cfg_test.go b/server/events/yaml/valid/global_cfg_test.go
index 95d04358d1..66628de9bb 100644
--- a/server/events/yaml/valid/global_cfg_test.go
+++ b/server/events/yaml/valid/global_cfg_test.go
@@ -121,9 +121,6 @@ func TestNewGlobalCfg(t *testing.T) {
if c.approvedReq {
exp.Repos[0].ApplyRequirements = append(exp.Repos[0].ApplyRequirements, "approved")
}
- if exp.Repos[0].PreWorkflowHooks == nil {
- exp.Repos[0].PreWorkflowHooks = []*valid.PreWorkflowHook{}
- }
Equals(t, exp, act)
diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go
index 7ee8fd649b..b70ae21321 100644
--- a/server/events_controller_e2e_test.go
+++ b/server/events_controller_e2e_test.go
@@ -483,24 +483,22 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) {
ExpReplies [][]string
}{
{
- Description: "failing policy approved by the owner",
- RepoDir: "policy-checks",
- ModifiedFiles: []string{"main.tf"},
+ Description: "1 failing policy and 1 passing policy ",
+ RepoDir: "policy-checks-multi-projects",
+ ModifiedFiles: []string{"dir1/main.tf,", "dir2/main.tf"},
ExpAutoplan: true,
Comments: []string{
- "atlantis approve_policies",
"atlantis apply",
},
ExpReplies: [][]string{
{"exp-output-autoplan.txt"},
{"exp-output-auto-policy-check.txt"},
- {"exp-output-approve-policies.txt"},
{"exp-output-apply.txt"},
{"exp-output-merge.txt"},
},
},
{
- Description: "failing policy without approval",
+ Description: "failing policy without policies passing",
RepoDir: "policy-checks",
ModifiedFiles: []string{"main.tf"},
ExpAutoplan: true,
@@ -514,6 +512,21 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) {
{"exp-output-merge.txt"},
},
},
+ {
+ Description: "failing policy additional apply requirements specified",
+ RepoDir: "policy-checks-apply-reqs",
+ ModifiedFiles: []string{"main.tf"},
+ ExpAutoplan: true,
+ Comments: []string{
+ "atlantis apply",
+ },
+ ExpReplies: [][]string{
+ {"exp-output-autoplan.txt"},
+ {"exp-output-auto-policy-check.txt"},
+ {"exp-output-apply-failed.txt"},
+ {"exp-output-merge.txt"},
+ },
+ },
{
Description: "failing policy approved by non owner",
RepoDir: "policy-checks-diff-owner",
@@ -550,6 +563,7 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) {
// Setup test dependencies.
w := httptest.NewRecorder()
When(vcsClient.PullIsMergeable(AnyRepo(), matchers.AnyModelsPullRequest())).ThenReturn(true, nil)
+ When(vcsClient.PullIsApproved(AnyRepo(), matchers.AnyModelsPullRequest())).ThenReturn(true, nil)
When(githubGetter.GetPullRequest(AnyRepo(), AnyInt())).ThenReturn(GitHubPullRequestParsed(headSHA), nil)
When(vcsClient.GetModifiedFiles(AnyRepo(), matchers.AnyModelsPullRequest())).ThenReturn(c.ModifiedFiles, nil)
@@ -630,13 +644,7 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks.
e2eGitlabGetter := mocks.NewMockGitlabMergeRequestGetter()
// Real dependencies.
- logger, err := logging.NewStructuredLogger()
-
- if err != nil {
- panic("Could not setup logger for e2e")
- }
-
- logger.SetLevel(logging.Error)
+ logger := logging.NewNoopLogger(t)
eventParser := &events.EventParser{
GithubUser: "github-user",
@@ -666,12 +674,20 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks.
defaultTFVersion := terraformClient.DefaultVersion()
locker := events.NewDefaultWorkingDirLocker()
parser := &yaml.ParserValidator{}
- globalCfg := valid.NewGlobalCfgWithHooks(true, false, false, []*valid.PreWorkflowHook{
- {
- StepName: "global_hook",
- RunCommand: "some dummy command",
+
+ globalCfgArgs := valid.GlobalCfgArgs{
+ AllowRepoCfg: true,
+ MergeableReq: false,
+ ApprovedReq: false,
+ PreWorkflowHooks: []*valid.PreWorkflowHook{
+ {
+ StepName: "global_hook",
+ RunCommand: "some dummy command",
+ },
},
- })
+ PolicyCheckEnabled: userConfig.EnablePolicyChecksFlag,
+ }
+ globalCfg := valid.NewGlobalCfgFromArgs(globalCfgArgs)
expCfgPath := filepath.Join(absRepoPath(t, repoDir), "repos.yaml")
if _, err := os.Stat(expCfgPath); err == nil {
globalCfg, err = parser.ParseGlobalCfg(expCfgPath, globalCfg)
@@ -784,6 +800,7 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks.
policyCheckCommandRunner,
autoMerger,
parallelPoolSize,
+ boltdb,
)
applyCommandRunner := events.NewApplyCommandRunner(
@@ -831,6 +848,7 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks.
CommentCommandRunnerByCmd: commentCommandRunnerByCmd,
Drainer: drainer,
PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner,
+ PullStatusFetcher: boltdb,
}
repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*")
diff --git a/server/server.go b/server/server.go
index 23098e4c11..58da901b28 100644
--- a/server/server.go
+++ b/server/server.go
@@ -344,7 +344,13 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
}
validator := &yaml.ParserValidator{}
- globalCfg := valid.NewGlobalCfg(userConfig.AllowRepoConfig, userConfig.RequireMergeable, userConfig.RequireApproval)
+ globalCfg := valid.NewGlobalCfgFromArgs(
+ valid.GlobalCfgArgs{
+ AllowRepoCfg: userConfig.AllowRepoConfig,
+ MergeableReq: userConfig.RequireMergeable,
+ ApprovedReq: userConfig.RequireApproval,
+ PolicyCheckEnabled: userConfig.EnablePolicyChecksFlag,
+ })
if userConfig.RepoConfig != "" {
globalCfg, err = validator.ParseGlobalCfg(userConfig.RepoConfig, globalCfg)
if err != nil {
@@ -504,6 +510,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
policyCheckCommandRunner,
autoMerger,
userConfig.ParallelPoolSize,
+ boltdb,
)
applyCommandRunner := events.NewApplyCommandRunner(
@@ -555,6 +562,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
DisableAutoplan: userConfig.DisableAutoplan,
Drainer: drainer,
PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner,
+ PullStatusFetcher: boltdb,
}
repoAllowlist, err := events.NewRepoAllowlistChecker(userConfig.RepoAllowlist)
if err != nil {
diff --git a/server/testfixtures/test-repos/policy-checks-apply-reqs/atlantis.yaml b/server/testfixtures/test-repos/policy-checks-apply-reqs/atlantis.yaml
new file mode 100644
index 0000000000..8435733cd2
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-apply-reqs/atlantis.yaml
@@ -0,0 +1,4 @@
+version: 3
+projects:
+- dir: .
+ workspace: default
diff --git a/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-apply-failed.txt b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-apply-failed.txt
new file mode 100644
index 0000000000..1f57a9176d
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-apply-failed.txt
@@ -0,0 +1,4 @@
+Ran Apply for dir: `.` workspace: `default`
+
+**Apply Failed**: All policies must pass for project before running apply
+
diff --git a/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-apply.txt b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-apply.txt
new file mode 100644
index 0000000000..e6e44deb94
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-apply.txt
@@ -0,0 +1,24 @@
+Ran Apply for dir: `.` workspace: `default`
+
+Show Output
+
+```diff
+null_resource.simple:
+null_resource.simple:
+
+Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
+
+The state of your infrastructure has been saved to the path
+below. This state is required to modify and destroy your
+infrastructure, so keep it safe. To inspect the complete state
+use the `terraform show` command.
+
+State path: terraform.tfstate
+
+Outputs:
+
+workspace = "default"
+
+```
+
+
diff --git a/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-approve-policies.txt b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-approve-policies.txt
new file mode 100644
index 0000000000..f5e100c23e
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-approve-policies.txt
@@ -0,0 +1,5 @@
+Approved Policies for 1 projects:
+
+1. dir: `.` workspace: `default`
+
+
diff --git a/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-auto-policy-check.txt b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-auto-policy-check.txt
new file mode 100644
index 0000000000..a922cceca2
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-auto-policy-check.txt
@@ -0,0 +1,15 @@
+Ran Policy Check for dir: `.` workspace: `default`
+
+**Policy Check Error**
+```
+exit status 1
+Checking plan against the following policies:
+ test_policy
+FAIL - - WARNING: Null Resource creation is prohibited.
+
+1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
+
+```
+* :heavy_check_mark: To **approve** failing policies either request an approval from approvers or address the failure by modifying the codebase.
+
+
diff --git a/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-autoplan.txt b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-autoplan.txt
new file mode 100644
index 0000000000..d278415b40
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-autoplan.txt
@@ -0,0 +1,36 @@
+Ran Plan for dir: `.` workspace: `default`
+
+Show Output
+
+```diff
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
++ create
+
+Terraform will perform the following actions:
+
+ # null_resource.simple[0] will be created
++ resource "null_resource" "simple" {
+ + id = (known after apply)
+ }
+
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+Changes to Outputs:
++ workspace = "default"
+
+```
+
+* :arrow_forward: To **apply** this plan, comment:
+ * `atlantis apply -d .`
+* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
+* :repeat: To **plan** this project again, comment:
+ * `atlantis plan -d .`
+
+
+---
+* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
+ * `atlantis apply`
+* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
+ * `atlantis unlock`
diff --git a/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-merge.txt b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-merge.txt
new file mode 100644
index 0000000000..872c5ee40c
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-apply-reqs/exp-output-merge.txt
@@ -0,0 +1,3 @@
+Locks and plans deleted for the projects and workspaces modified in this pull request:
+
+- dir: `.` workspace: `default`
diff --git a/server/testfixtures/test-repos/policy-checks-apply-reqs/main.tf b/server/testfixtures/test-repos/policy-checks-apply-reqs/main.tf
new file mode 100644
index 0000000000..582f9ea01d
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-apply-reqs/main.tf
@@ -0,0 +1,7 @@
+resource "null_resource" "simple" {
+ count = 1
+}
+
+output "workspace" {
+ value = terraform.workspace
+}
diff --git a/server/testfixtures/test-repos/policy-checks-apply-reqs/policies/policy.rego b/server/testfixtures/test-repos/policy-checks-apply-reqs/policies/policy.rego
new file mode 100644
index 0000000000..126c2e4591
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-apply-reqs/policies/policy.rego
@@ -0,0 +1,28 @@
+package main
+
+import input as tfplan
+
+deny[reason] {
+ num_deletes.null_resource > 0
+ reason := "WARNING: Null Resource creation is prohibited."
+}
+
+resource_types = {"null_resource"}
+
+resources[resource_type] = all {
+ some resource_type
+ resource_types[resource_type]
+ all := [name |
+ name := tfplan.resource_changes[_]
+ name.type == resource_type
+ ]
+}
+
+# number of deletions of resources of a given type
+num_deletes[resource_type] = num {
+ some resource_type
+ resource_types[resource_type]
+ all := resources[resource_type]
+ deletions := [res | res := all[_]; res.change.actions[_] == "create"]
+ num := count(deletions)
+}
diff --git a/server/testfixtures/test-repos/policy-checks-apply-reqs/repos.yaml b/server/testfixtures/test-repos/policy-checks-apply-reqs/repos.yaml
new file mode 100644
index 0000000000..32434be4e3
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-apply-reqs/repos.yaml
@@ -0,0 +1,12 @@
+repos:
+- id: /.*/
+ apply_requirements: [approved]
+policies:
+ owners:
+ users:
+ - runatlantis
+ policy_sets:
+ - name: test_policy
+ path: policies/policy.rego
+ source: local
+
diff --git a/server/testfixtures/test-repos/policy-checks-diff-owner/exp-output-apply-failed.txt b/server/testfixtures/test-repos/policy-checks-diff-owner/exp-output-apply-failed.txt
index fbb8325fc7..1f57a9176d 100644
--- a/server/testfixtures/test-repos/policy-checks-diff-owner/exp-output-apply-failed.txt
+++ b/server/testfixtures/test-repos/policy-checks-diff-owner/exp-output-apply-failed.txt
@@ -1,4 +1,4 @@
Ran Apply for dir: `.` workspace: `default`
-**Apply Failed**: Pull request must be mergeable before running apply.
+**Apply Failed**: All policies must pass for project before running apply
diff --git a/server/testfixtures/test-repos/policy-checks-diff-owner/repos.yaml b/server/testfixtures/test-repos/policy-checks-diff-owner/repos.yaml
index a535795f68..136cf258c9 100644
--- a/server/testfixtures/test-repos/policy-checks-diff-owner/repos.yaml
+++ b/server/testfixtures/test-repos/policy-checks-diff-owner/repos.yaml
@@ -1,6 +1,3 @@
-repos:
- - id: /.*/
- apply_requirements: [mergeable]
policies:
owners:
users:
diff --git a/server/testfixtures/test-repos/policy-checks-multi-projects/atlantis.yaml b/server/testfixtures/test-repos/policy-checks-multi-projects/atlantis.yaml
new file mode 100644
index 0000000000..006db31ba5
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-multi-projects/atlantis.yaml
@@ -0,0 +1,4 @@
+version: 3
+projects:
+- dir: dir1
+- dir: dir2
diff --git a/server/testfixtures/test-repos/policy-checks-multi-projects/dir1/main.tf b/server/testfixtures/test-repos/policy-checks-multi-projects/dir1/main.tf
new file mode 100644
index 0000000000..582f9ea01d
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-multi-projects/dir1/main.tf
@@ -0,0 +1,7 @@
+resource "null_resource" "simple" {
+ count = 1
+}
+
+output "workspace" {
+ value = terraform.workspace
+}
diff --git a/server/testfixtures/test-repos/policy-checks-multi-projects/dir2/main.tf b/server/testfixtures/test-repos/policy-checks-multi-projects/dir2/main.tf
new file mode 100644
index 0000000000..8813d4459c
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-multi-projects/dir2/main.tf
@@ -0,0 +1,7 @@
+resource "null_resource" "forbidden" {
+ count = 1
+}
+
+output "workspace" {
+ value = terraform.workspace
+}
diff --git a/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-apply.txt b/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-apply.txt
new file mode 100644
index 0000000000..8c3c812762
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-apply.txt
@@ -0,0 +1,34 @@
+Ran Apply for 2 projects:
+
+1. dir: `dir1` workspace: `default`
+1. dir: `dir2` workspace: `default`
+
+### 1. dir: `dir1` workspace: `default`
+Show Output
+
+```diff
+null_resource.simple:
+null_resource.simple:
+
+Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
+
+The state of your infrastructure has been saved to the path
+below. This state is required to modify and destroy your
+infrastructure, so keep it safe. To inspect the complete state
+use the `terraform show` command.
+
+State path: terraform.tfstate
+
+Outputs:
+
+workspace = "default"
+
+```
+
+
+---
+### 2. dir: `dir2` workspace: `default`
+**Apply Failed**: All policies must pass for project before running apply
+
+---
+
diff --git a/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-approve-policies.txt b/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-approve-policies.txt
new file mode 100644
index 0000000000..f5e100c23e
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-approve-policies.txt
@@ -0,0 +1,5 @@
+Approved Policies for 1 projects:
+
+1. dir: `.` workspace: `default`
+
+
diff --git a/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-auto-policy-check.txt b/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-auto-policy-check.txt
new file mode 100644
index 0000000000..5bc3834f5a
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-auto-policy-check.txt
@@ -0,0 +1,40 @@
+Ran Policy Check for 2 projects:
+
+1. dir: `dir1` workspace: `default`
+1. dir: `dir2` workspace: `default`
+
+### 1. dir: `dir1` workspace: `default`
+```diff
+Checking plan against the following policies:
+ test_policy
+
+1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions
+
+```
+
+* :arrow_forward: To **apply** this plan, comment:
+ * `atlantis apply -d dir1`
+* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
+* :repeat: To re-run policies **plan** this project again by commenting:
+ * `atlantis plan -d dir1`
+
+---
+### 2. dir: `dir2` workspace: `default`
+**Policy Check Error**
+```
+exit status 1
+Checking plan against the following policies:
+ test_policy
+FAIL - - WARNING: Forbidden Resource creation is prohibited.
+
+1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
+
+```
+* :heavy_check_mark: To **approve** failing policies either request an approval from approvers or address the failure by modifying the codebase.
+
+
+---
+* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
+ * `atlantis apply`
+* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
+ * `atlantis unlock`
diff --git a/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-autoplan.txt b/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-autoplan.txt
new file mode 100644
index 0000000000..77ef71ca99
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-autoplan.txt
@@ -0,0 +1,71 @@
+Ran Plan for 2 projects:
+
+1. dir: `dir1` workspace: `default`
+1. dir: `dir2` workspace: `default`
+
+### 1. dir: `dir1` workspace: `default`
+Show Output
+
+```diff
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
++ create
+
+Terraform will perform the following actions:
+
+ # null_resource.simple[0] will be created
++ resource "null_resource" "simple" {
+ + id = (known after apply)
+ }
+
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+Changes to Outputs:
++ workspace = "default"
+
+```
+
+* :arrow_forward: To **apply** this plan, comment:
+ * `atlantis apply -d dir1`
+* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
+* :repeat: To **plan** this project again, comment:
+ * `atlantis plan -d dir1`
+
+
+---
+### 2. dir: `dir2` workspace: `default`
+Show Output
+
+```diff
+
+An execution plan has been generated and is shown below.
+Resource actions are indicated with the following symbols:
++ create
+
+Terraform will perform the following actions:
+
+ # null_resource.forbidden[0] will be created
++ resource "null_resource" "forbidden" {
+ + id = (known after apply)
+ }
+
+Plan: 1 to add, 0 to change, 0 to destroy.
+
+Changes to Outputs:
++ workspace = "default"
+
+```
+
+* :arrow_forward: To **apply** this plan, comment:
+ * `atlantis apply -d dir2`
+* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
+* :repeat: To **plan** this project again, comment:
+ * `atlantis plan -d dir2`
+
+
+---
+* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
+ * `atlantis apply`
+* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
+ * `atlantis unlock`
diff --git a/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-merge.txt b/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-merge.txt
new file mode 100644
index 0000000000..1a12259187
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-multi-projects/exp-output-merge.txt
@@ -0,0 +1,4 @@
+Locks and plans deleted for the projects and workspaces modified in this pull request:
+
+- dir: `dir1` workspace: `default`
+- dir: `dir2` workspace: `default`
\ No newline at end of file
diff --git a/server/testfixtures/test-repos/policy-checks-multi-projects/policies/policy.rego b/server/testfixtures/test-repos/policy-checks-multi-projects/policies/policy.rego
new file mode 100644
index 0000000000..4b9e5254e5
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-multi-projects/policies/policy.rego
@@ -0,0 +1,28 @@
+package main
+
+import input as tfplan
+
+deny[reason] {
+ num_creates[_] > 0
+ reason := "WARNING: Forbidden Resource creation is prohibited."
+}
+
+resource_names = {"forbidden"}
+
+resources[resource_name] = all {
+ some resource_name
+ resource_names[resource_name]
+ all := [res |
+ res := tfplan.resource_changes[_]
+ res.name == resource_name
+ ]
+}
+
+# number of creations of resources of a given name
+num_creates[resource_name] = num {
+ some resource_name
+ resource_names[resource_name]
+ all := resources[resource_name]
+ creations := [res | res := all[_]; res.change.actions[_] == "create"]
+ num := count(creations)
+}
diff --git a/server/testfixtures/test-repos/policy-checks-multi-projects/repos.yaml b/server/testfixtures/test-repos/policy-checks-multi-projects/repos.yaml
new file mode 100644
index 0000000000..56821996c2
--- /dev/null
+++ b/server/testfixtures/test-repos/policy-checks-multi-projects/repos.yaml
@@ -0,0 +1,9 @@
+policies:
+ owners:
+ users:
+ - runatlantis
+ policy_sets:
+ - name: test_policy
+ path: ../policies/policy.rego
+ source: local
+
diff --git a/server/testfixtures/test-repos/policy-checks/exp-output-apply-failed.txt b/server/testfixtures/test-repos/policy-checks/exp-output-apply-failed.txt
index fbb8325fc7..1f57a9176d 100644
--- a/server/testfixtures/test-repos/policy-checks/exp-output-apply-failed.txt
+++ b/server/testfixtures/test-repos/policy-checks/exp-output-apply-failed.txt
@@ -1,4 +1,4 @@
Ran Apply for dir: `.` workspace: `default`
-**Apply Failed**: Pull request must be mergeable before running apply.
+**Apply Failed**: All policies must pass for project before running apply
diff --git a/server/testfixtures/test-repos/policy-checks/repos.yaml b/server/testfixtures/test-repos/policy-checks/repos.yaml
index b1a44de4ca..a5fa0cb9e2 100644
--- a/server/testfixtures/test-repos/policy-checks/repos.yaml
+++ b/server/testfixtures/test-repos/policy-checks/repos.yaml
@@ -1,6 +1,3 @@
-repos:
- - id: /.*/
- apply_requirements: [mergeable]
policies:
owners:
users: