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: