mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-28 22:28:25 +00:00
* Make policy checks its own apply requirement. (#61) * Remove warning from docs.
This commit is contained in:
@@ -17,10 +17,6 @@ Enabling "policy checking" in addition to the [mergeable apply requirement](http
|
||||
|
||||

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

|
||||
|
||||
@@ -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." +
|
||||
|
||||
@@ -46,5 +46,7 @@ type CommandContext struct {
|
||||
// required the Atlantis status to be successful prior to merging.
|
||||
PullMergeable bool
|
||||
|
||||
PullStatus *models.PullStatus
|
||||
|
||||
Trigger CommandTrigger
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
166
server/events/mocks/mock_comment_building.go
Normal file
166
server/events/mocks/mock_comment_building.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
8
server/events/pull_status_fetcher.go
Normal file
8
server/events/pull_status_fetcher.go
Normal 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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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("*")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
version: 3
|
||||
projects:
|
||||
- dir: .
|
||||
workspace: default
|
||||
@@ -0,0 +1,4 @@
|
||||
Ran Apply for dir: `.` workspace: `default`
|
||||
|
||||
**Apply Failed**: All policies must pass for project before running apply
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Approved Policies for 1 projects:
|
||||
|
||||
1. dir: `.` workspace: `default`
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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`
|
||||
@@ -0,0 +1,3 @@
|
||||
Locks and plans deleted for the projects and workspaces modified in this pull request:
|
||||
|
||||
- dir: `.` workspace: `default`
|
||||
@@ -0,0 +1,7 @@
|
||||
resource "null_resource" "simple" {
|
||||
count = 1
|
||||
}
|
||||
|
||||
output "workspace" {
|
||||
value = terraform.workspace
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
repos:
|
||||
- id: /.*/
|
||||
apply_requirements: [approved]
|
||||
policies:
|
||||
owners:
|
||||
users:
|
||||
- runatlantis
|
||||
policy_sets:
|
||||
- name: test_policy
|
||||
path: policies/policy.rego
|
||||
source: local
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
repos:
|
||||
- id: /.*/
|
||||
apply_requirements: [mergeable]
|
||||
policies:
|
||||
owners:
|
||||
users:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
version: 3
|
||||
projects:
|
||||
- dir: dir1
|
||||
- dir: dir2
|
||||
@@ -0,0 +1,7 @@
|
||||
resource "null_resource" "simple" {
|
||||
count = 1
|
||||
}
|
||||
|
||||
output "workspace" {
|
||||
value = terraform.workspace
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
resource "null_resource" "forbidden" {
|
||||
count = 1
|
||||
}
|
||||
|
||||
output "workspace" {
|
||||
value = terraform.workspace
|
||||
}
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
Approved Policies for 1 projects:
|
||||
|
||||
1. dir: `.` workspace: `default`
|
||||
|
||||
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
policies:
|
||||
owners:
|
||||
users:
|
||||
- runatlantis
|
||||
policy_sets:
|
||||
- name: test_policy
|
||||
path: ../policies/policy.rego
|
||||
source: local
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
repos:
|
||||
- id: /.*/
|
||||
apply_requirements: [mergeable]
|
||||
policies:
|
||||
owners:
|
||||
users:
|
||||
|
||||
Reference in New Issue
Block a user