Make policy checks its own apply requirement. (#61) (#1499)

* Make policy checks its own apply requirement. (#61)

* Remove warning from docs.
This commit is contained in:
Nish Krishnan
2021-04-13 12:46:47 -07:00
committed by GitHub
parent 7e052d3bc9
commit df106ded11
47 changed files with 857 additions and 109 deletions

View File

@@ -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)

View File

@@ -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." +

View File

@@ -46,5 +46,7 @@ type CommandContext struct {
// required the Atlantis status to be successful prior to merging.
PullMergeable bool
PullStatus *models.PullStatus
Trigger CommandTrigger
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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)
})
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 != "" {

View File

@@ -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)

View File

@@ -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("*")

View File

@@ -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 {

View File

@@ -0,0 +1,4 @@
version: 3
projects:
- dir: .
workspace: default

View File

@@ -0,0 +1,4 @@
Ran Apply for dir: `.` workspace: `default`
**Apply Failed**: All policies must pass for project before running apply

View File

@@ -0,0 +1,24 @@
Ran Apply for dir: `.` workspace: `default`
<details><summary>Show Output</summary>
```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"
```
</details>

View File

@@ -0,0 +1,5 @@
Approved Policies for 1 projects:
1. dir: `.` workspace: `default`

View File

@@ -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 - <redacted plan file> - 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.

View File

@@ -0,0 +1,36 @@
Ran Plan for dir: `.` workspace: `default`
<details><summary>Show Output</summary>
```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 .`
</details>
---
* :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`

View File

@@ -0,0 +1,3 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:
- dir: `.` workspace: `default`

View File

@@ -0,0 +1,7 @@
resource "null_resource" "simple" {
count = 1
}
output "workspace" {
value = terraform.workspace
}

View File

@@ -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)
}

View File

@@ -0,0 +1,12 @@
repos:
- id: /.*/
apply_requirements: [approved]
policies:
owners:
users:
- runatlantis
policy_sets:
- name: test_policy
path: policies/policy.rego
source: local

View File

@@ -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

View File

@@ -1,6 +1,3 @@
repos:
- id: /.*/
apply_requirements: [mergeable]
policies:
owners:
users:

View File

@@ -0,0 +1,4 @@
version: 3
projects:
- dir: dir1
- dir: dir2

View File

@@ -0,0 +1,7 @@
resource "null_resource" "simple" {
count = 1
}
output "workspace" {
value = terraform.workspace
}

View File

@@ -0,0 +1,7 @@
resource "null_resource" "forbidden" {
count = 1
}
output "workspace" {
value = terraform.workspace
}

View File

@@ -0,0 +1,34 @@
Ran Apply for 2 projects:
1. dir: `dir1` workspace: `default`
1. dir: `dir2` workspace: `default`
### 1. dir: `dir1` workspace: `default`
<details><summary>Show Output</summary>
```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"
```
</details>
---
### 2. dir: `dir2` workspace: `default`
**Apply Failed**: All policies must pass for project before running apply
---

View File

@@ -0,0 +1,5 @@
Approved Policies for 1 projects:
1. dir: `.` workspace: `default`

View File

@@ -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 - <redacted plan file> - 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`

View File

@@ -0,0 +1,71 @@
Ran Plan for 2 projects:
1. dir: `dir1` workspace: `default`
1. dir: `dir2` workspace: `default`
### 1. dir: `dir1` workspace: `default`
<details><summary>Show Output</summary>
```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`
</details>
---
### 2. dir: `dir2` workspace: `default`
<details><summary>Show Output</summary>
```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`
</details>
---
* :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`

View File

@@ -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`

View File

@@ -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)
}

View File

@@ -0,0 +1,9 @@
policies:
owners:
users:
- runatlantis
policy_sets:
- name: test_policy
path: ../policies/policy.rego
source: local

View File

@@ -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

View File

@@ -1,6 +1,3 @@
repos:
- id: /.*/
apply_requirements: [mergeable]
policies:
owners:
users: