feat: when using order group, abort plan/apply if any fail (#3323)

* feat: when using order group, abort plan/apply if any fail

* feat: add 'abort_on_execution_order_fail' flag on repo level

* feat: use runProjectCmdsParallelGroups in version_command_runner

* chore: add plan tests

---------

Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com>
This commit is contained in:
oysteingraendsen
2023-05-12 19:24:33 +02:00
committed by GitHub
parent 7a4e9474d5
commit 4e867fc36e
14 changed files with 753 additions and 34 deletions

View File

@@ -50,6 +50,7 @@ automerge: true
delete_source_branch_on_merge: true
parallel_plan: true
parallel_apply: true
abort_on_execution_order_fail: true
projects:
- name: my-project-name
branch: /main/
@@ -64,6 +65,7 @@ projects:
plan_requirements: [mergeable, approved, undiverged]
apply_requirements: [mergeable, approved, undiverged]
import_requirements: [mergeable, approved, undiverged]
execution_order_group: 1
workflow: myworkflow
workflows:
myworkflow:
@@ -259,6 +261,7 @@ to be allowed to set this key. See [Server-Side Repo Config Use Cases](server-si
### Order of planning/applying
```yaml
version: 3
abort_on_execution_order_fail: true
projects:
- dir: project1
execution_order_group: 2
@@ -268,7 +271,10 @@ projects:
With this config above, Atlantis runs planning/applying for project2 first, then for project1.
Several projects can have same `execution_order_group`. Any order in one group isn't guaranteed.
`parallel_plan` and `parallel_apply` respect these order groups, so parallel planning/applying works
in each group one by one.
in each group one by one.
If any plan/apply fails and `abort_on_execution_order_fail` is set to true on a repo level, all the
following groups will be aborted. For this example, if project2 fails then project1 will not run.
### Custom Backend Config
See [Custom Workflow Use Cases: Custom Backend Config](custom-workflows.html#custom-backend-config)

View File

@@ -25,6 +25,9 @@ const DefaultDeleteSourceBranchOnMerge = false
// DefaultEmojiReaction is the default emoji reaction for repos
const DefaultEmojiReaction = ""
// DefaultAbortOnExcecutionOrderFail being false is the default setting for abort on execution group failiures
const DefaultAbortOnExcecutionOrderFail = false
// RepoCfg is the raw schema for repo-level atlantis.yaml config.
type RepoCfg struct {
Version *int `yaml:"version,omitempty"`
@@ -37,6 +40,7 @@ type RepoCfg struct {
DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty"`
EmojiReaction *string `yaml:"emoji_reaction,omitempty"`
AllowedRegexpPrefixes []string `yaml:"allowed_regexp_prefixes,omitempty"`
AbortOnExcecutionOrderFail *bool `yaml:"abort_on_execution_order_fail,omitempty"`
}
func (r RepoCfg) Validate() error {
@@ -88,6 +92,11 @@ func (r RepoCfg) ToValid() valid.RepoCfg {
emojiReaction = *r.EmojiReaction
}
abortOnExcecutionOrderFail := DefaultAbortOnExcecutionOrderFail
if r.AbortOnExcecutionOrderFail != nil {
abortOnExcecutionOrderFail = *r.AbortOnExcecutionOrderFail
}
return valid.RepoCfg{
Version: *r.Version,
Projects: validProjects,
@@ -99,5 +108,6 @@ func (r RepoCfg) ToValid() valid.RepoCfg {
DeleteSourceBranchOnMerge: r.DeleteSourceBranchOnMerge,
AllowedRegexpPrefixes: r.AllowedRegexpPrefixes,
EmojiReaction: emojiReaction,
AbortOnExcecutionOrderFail: abortOnExcecutionOrderFail,
}
}

View File

@@ -259,43 +259,48 @@ func TestConfig_ToValid(t *testing.T) {
},
},
{
description: "automerge and parallel_apply omitted",
description: "automerge, parallel_apply and abort_on_execution_order_fail omitted",
input: raw.RepoCfg{
Version: Int(2),
},
exp: valid.RepoCfg{
Version: 2,
Automerge: false,
ParallelApply: false,
Workflows: map[string]valid.Workflow{},
Version: 2,
Automerge: false,
ParallelApply: false,
AbortOnExcecutionOrderFail: false,
Workflows: map[string]valid.Workflow{},
},
},
{
description: "automerge and parallel_apply true",
description: "automerge, parallel_apply and abort_on_execution_order_fail true",
input: raw.RepoCfg{
Version: Int(2),
Automerge: Bool(true),
ParallelApply: Bool(true),
Version: Int(2),
Automerge: Bool(true),
ParallelApply: Bool(true),
AbortOnExcecutionOrderFail: Bool(true),
},
exp: valid.RepoCfg{
Version: 2,
Automerge: true,
ParallelApply: true,
Workflows: map[string]valid.Workflow{},
Version: 2,
Automerge: true,
ParallelApply: true,
AbortOnExcecutionOrderFail: true,
Workflows: map[string]valid.Workflow{},
},
},
{
description: "automerge and parallel_apply false",
description: "automerge, parallel_apply and abort_on_execution_order_fail false",
input: raw.RepoCfg{
Version: Int(2),
Automerge: Bool(false),
ParallelApply: Bool(false),
Version: Int(2),
Automerge: Bool(false),
ParallelApply: Bool(false),
AbortOnExcecutionOrderFail: Bool(false),
},
exp: valid.RepoCfg{
Version: 2,
Automerge: false,
ParallelApply: false,
Workflows: map[string]valid.Workflow{},
Version: 2,
Automerge: false,
ParallelApply: false,
AbortOnExcecutionOrderFail: false,
Workflows: map[string]valid.Workflow{},
},
},
{

View File

@@ -26,6 +26,7 @@ type RepoCfg struct {
RepoLocking *bool
EmojiReaction string
AllowedRegexpPrefixes []string
AbortOnExcecutionOrderFail bool
}
func (r RepoCfg) FindProjectsByDirWorkspace(repoRelDir string, workspace string) []Project {

View File

@@ -161,7 +161,7 @@ func (a *ApplyCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
var result command.Result
if a.isParallelEnabled(projectCmds) {
ctx.Log.Info("Running applies in parallel")
result = runProjectCmdsParallelGroups(projectCmds, a.prjCmdRunner.Apply, a.parallelPoolSize)
result = runProjectCmdsParallelGroups(ctx, projectCmds, a.prjCmdRunner.Apply, a.parallelPoolSize)
} else {
result = runProjectCmds(projectCmds, a.prjCmdRunner.Apply)
}

View File

@@ -215,3 +215,296 @@ func TestApplyCommandRunner_IsSilenced(t *testing.T) {
})
}
}
func TestApplyCommandRunner_ExecutionOrder(t *testing.T) {
logger := logging.NewNoopLogger(t)
RegisterMockTestingT(t)
cases := []struct {
Description string
ProjectContexts []command.ProjectContext
ProjectResults []command.ProjectResult
RunnerInvokeMatch []*EqMatcher
ExpComment string
}{
{
Description: "When first apply fails, the second don't run",
ProjectContexts: []command.ProjectContext{
{
ExecutionOrderGroup: 0,
ProjectName: "First",
ParallelApplyEnabled: true,
AbortOnExcecutionOrderFail: true,
},
{
ExecutionOrderGroup: 1,
ProjectName: "Second",
ParallelApplyEnabled: true,
AbortOnExcecutionOrderFail: true,
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Apply,
ApplySuccess: "Great success!",
},
{
Command: command.Apply,
Error: errors.New("Shabang!"),
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Once(),
},
ExpComment: "Ran Apply for 2 projects:\n\n" +
"1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n\n### 1. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### " +
"2. dir: `` workspace: ``\n**Apply Error**\n```\nShabang!\n```\n\n---",
},
{
Description: "When first apply fails, the second not will run",
ProjectContexts: []command.ProjectContext{
{
ExecutionOrderGroup: 0,
ProjectName: "First",
ParallelApplyEnabled: true,
AbortOnExcecutionOrderFail: true,
},
{
ExecutionOrderGroup: 1,
ProjectName: "Second",
ParallelApplyEnabled: true,
AbortOnExcecutionOrderFail: true,
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Apply,
Error: errors.New("Shabang!"),
},
{
Command: command.Apply,
ApplySuccess: "Great success!",
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Never(),
},
ExpComment: "Ran Apply for dir: `` workspace: ``\n\n**Apply Error**\n```\nShabang!\n```",
},
{
Description: "When both in a group of two succeeds, the following two will run",
ProjectContexts: []command.ProjectContext{
{
ExecutionOrderGroup: 0,
ProjectName: "First",
ParallelApplyEnabled: true,
AbortOnExcecutionOrderFail: true,
},
{
ExecutionOrderGroup: 0,
ProjectName: "Second",
AbortOnExcecutionOrderFail: true,
},
{
ExecutionOrderGroup: 1,
ProjectName: "Third",
AbortOnExcecutionOrderFail: true,
},
{
ExecutionOrderGroup: 1,
ProjectName: "Fourth",
AbortOnExcecutionOrderFail: true,
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Apply,
ApplySuccess: "Great success!",
},
{
Command: command.Apply,
Error: errors.New("Shabang!"),
},
{
Command: command.Apply,
ApplySuccess: "Great success!",
},
{
Command: command.Apply,
ApplySuccess: "Great success!",
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Once(),
Never(),
Never(),
},
ExpComment: "Ran Apply for 2 projects:\n\n" +
"1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n\n### 1. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### " +
"2. dir: `` workspace: ``\n**Apply Error**\n```\nShabang!\n```\n\n---",
},
{
Description: "When one out of two fails, the following two will not run",
ProjectContexts: []command.ProjectContext{
{
ExecutionOrderGroup: 0,
ProjectName: "First",
ParallelApplyEnabled: true,
AbortOnExcecutionOrderFail: true,
},
{
ExecutionOrderGroup: 0,
ProjectName: "Second",
AbortOnExcecutionOrderFail: true,
},
{
ExecutionOrderGroup: 1,
ProjectName: "Third",
AbortOnExcecutionOrderFail: true,
},
{
ExecutionOrderGroup: 1,
AbortOnExcecutionOrderFail: true,
ProjectName: "Fourth",
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Apply,
ApplySuccess: "Great success!",
},
{
Command: command.Apply,
ApplySuccess: "Great success!",
},
{
Command: command.Apply,
Error: errors.New("Shabang!"),
},
{
Command: command.Apply,
ApplySuccess: "Great success!",
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Once(),
Once(),
Once(),
},
ExpComment: "Ran Apply for 4 projects:\n\n" +
"1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n\n### 1. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### " +
"2. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---\n### " +
"3. dir: `` workspace: ``\n**Apply Error**\n```\nShabang!\n```\n\n---\n### " +
"4. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---",
},
{
Description: "Don't block when parallel is not set",
ProjectContexts: []command.ProjectContext{
{
ExecutionOrderGroup: 0,
ProjectName: "First",
AbortOnExcecutionOrderFail: true,
},
{
ExecutionOrderGroup: 1,
ProjectName: "Second",
AbortOnExcecutionOrderFail: true,
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Apply,
Error: errors.New("Shabang!"),
},
{
Command: command.Apply,
ApplySuccess: "Great success!",
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Once(),
},
ExpComment: "Ran Apply for 2 projects:\n\n" +
"1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n\n### 1. dir: `` workspace: ``\n**Apply Error**\n```\nShabang!\n```\n\n---\n### " +
"2. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---",
},
{
Description: "Don't block when abortOnExcecutionOrderFail is not set",
ProjectContexts: []command.ProjectContext{
{
ExecutionOrderGroup: 0,
ProjectName: "First",
},
{
ExecutionOrderGroup: 1,
ProjectName: "Second",
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Apply,
Error: errors.New("Shabang!"),
},
{
Command: command.Apply,
ApplySuccess: "Great success!",
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Once(),
},
ExpComment: "Ran Apply for 2 projects:\n\n" +
"1. dir: `` workspace: ``\n1. dir: `` workspace: ``\n\n### 1. dir: `` workspace: ``\n**Apply Error**\n```\nShabang!\n```\n\n---\n### " +
"2. dir: `` workspace: ``\n```diff\nGreat success!\n```\n\n---",
},
}
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
vcsClient := setup(t)
scopeNull, _, _ := metrics.NewLoggingScope(logger, "atlantis")
pull := &github.PullRequest{
State: github.String("open"),
}
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}
cmd := &events.CommentCommand{Name: command.Apply}
ctx := &command.Context{
User: testdata.User,
Log: logging.NewNoopLogger(t),
Scope: scopeNull,
Pull: modelPull,
HeadRepo: testdata.GithubRepo,
Trigger: command.CommentTrigger,
}
When(githubGetter.GetPullRequest(testdata.GithubRepo, testdata.Pull.Num)).ThenReturn(pull, nil)
When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)
When(projectCommandBuilder.BuildApplyCommands(ctx, cmd)).ThenReturn(c.ProjectContexts, nil)
for i := range c.ProjectContexts {
When(projectCommandRunner.Apply(c.ProjectContexts[i])).ThenReturn(c.ProjectResults[i])
}
applyCommandRunner.Run(ctx, cmd)
for i := range c.ProjectContexts {
projectCommandRunner.VerifyWasCalled(c.RunnerInvokeMatch[i]).Apply(c.ProjectContexts[i])
}
vcsClient.VerifyWasCalledOnce().CreateComment(
testdata.GithubRepo, modelPull.Num, c.ExpComment, "apply",
)
})
}
}

View File

@@ -111,6 +111,8 @@ type ProjectContext struct {
JobID string
// The index of order group. Before planning/applying it will use to sort projects. Default is 0.
ExecutionOrderGroup int
// If plans/applies should be aborted if any prior plan/apply fails
AbortOnExcecutionOrderFail bool
}
// SetProjectScopeTags adds ProjectContext tags to a new returned scope.

View File

@@ -130,7 +130,7 @@ func (p *PlanCommandRunner) runAutoplan(ctx *command.Context) {
var result command.Result
if p.isParallelEnabled(projectCmds) {
ctx.Log.Info("Running plans in parallel")
result = runProjectCmdsParallelGroups(projectCmds, p.prjCmdRunner.Plan, p.parallelPoolSize)
result = runProjectCmdsParallelGroups(ctx, projectCmds, p.prjCmdRunner.Plan, p.parallelPoolSize)
} else {
result = runProjectCmds(projectCmds, p.prjCmdRunner.Plan)
}
@@ -250,7 +250,7 @@ func (p *PlanCommandRunner) run(ctx *command.Context, cmd *CommentCommand) {
var result command.Result
if p.isParallelEnabled(projectCmds) {
ctx.Log.Info("Running plans in parallel")
result = runProjectCmdsParallelGroups(projectCmds, p.prjCmdRunner.Plan, p.parallelPoolSize)
result = runProjectCmdsParallelGroups(ctx, projectCmds, p.prjCmdRunner.Plan, p.parallelPoolSize)
} else {
result = runProjectCmds(projectCmds, p.prjCmdRunner.Plan)
}

View File

@@ -1,8 +1,10 @@
package events_test
import (
"errors"
"testing"
"github.com/google/go-github/v52/github"
. "github.com/petergtz/pegomock"
"github.com/runatlantis/atlantis/server/core/db"
"github.com/runatlantis/atlantis/server/events"
@@ -150,3 +152,360 @@ func TestPlanCommandRunner_IsSilenced(t *testing.T) {
})
}
}
func TestPlanCommandRunner_ExecutionOrder(t *testing.T) {
logger := logging.NewNoopLogger(t)
RegisterMockTestingT(t)
cases := []struct {
Description string
ProjectContexts []command.ProjectContext
ProjectResults []command.ProjectResult
RunnerInvokeMatch []*EqMatcher
PrevPlanStored bool
}{
{
Description: "When first plan fails, the second don't run",
ProjectContexts: []command.ProjectContext{
{
CommandName: command.Plan,
ExecutionOrderGroup: 0,
Workspace: "first",
ProjectName: "First",
ParallelPlanEnabled: true,
AbortOnExcecutionOrderFail: true,
},
{
CommandName: command.Plan,
ExecutionOrderGroup: 1,
Workspace: "second",
ProjectName: "Second",
ParallelPlanEnabled: true,
AbortOnExcecutionOrderFail: true,
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "true",
},
},
{
Command: command.Plan,
Error: errors.New("Shabang!"),
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Once(),
},
},
{
Description: "When first fails, the second will not run",
ProjectContexts: []command.ProjectContext{
{
CommandName: command.Plan,
ExecutionOrderGroup: 0,
ProjectName: "First",
ParallelPlanEnabled: true,
AbortOnExcecutionOrderFail: true,
},
{
CommandName: command.Plan,
ExecutionOrderGroup: 1,
ProjectName: "Second",
ParallelPlanEnabled: true,
AbortOnExcecutionOrderFail: true,
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Plan,
Error: errors.New("Shabang!"),
},
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{},
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Never(),
},
},
{
Description: "When first fails by autorun, the second will not run",
ProjectContexts: []command.ProjectContext{
{
CommandName: command.Plan,
AutoplanEnabled: true,
ExecutionOrderGroup: 0,
ProjectName: "First",
ParallelPlanEnabled: true,
AbortOnExcecutionOrderFail: true,
},
{
CommandName: command.Plan,
AutoplanEnabled: true,
ExecutionOrderGroup: 1,
ProjectName: "Second",
ParallelPlanEnabled: true,
AbortOnExcecutionOrderFail: true,
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Plan,
Error: errors.New("Shabang!"),
},
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{},
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Never(),
},
},
{
Description: "When both in a group of two succeeds, the following two will run",
ProjectContexts: []command.ProjectContext{
{
CommandName: command.Plan,
ExecutionOrderGroup: 0,
ProjectName: "First",
ParallelPlanEnabled: true,
AbortOnExcecutionOrderFail: true,
},
{
CommandName: command.Plan,
ExecutionOrderGroup: 0,
ProjectName: "Second",
AbortOnExcecutionOrderFail: true,
},
{
CommandName: command.Plan,
ExecutionOrderGroup: 1,
ProjectName: "Third",
AbortOnExcecutionOrderFail: true,
},
{
CommandName: command.Plan,
ExecutionOrderGroup: 1,
ProjectName: "Fourth",
AbortOnExcecutionOrderFail: true,
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "true",
},
},
{
Command: command.Plan,
Error: errors.New("Shabang!"),
},
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "true",
},
},
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "true",
},
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Once(),
Never(),
Never(),
},
},
{
Description: "When one out of two fails, the following two will not run",
ProjectContexts: []command.ProjectContext{
{
CommandName: command.Plan,
ExecutionOrderGroup: 0,
ProjectName: "First",
ParallelPlanEnabled: true,
AbortOnExcecutionOrderFail: true,
},
{
CommandName: command.Plan,
ExecutionOrderGroup: 0,
ProjectName: "Second",
AbortOnExcecutionOrderFail: true,
},
{
CommandName: command.Plan,
ExecutionOrderGroup: 1,
ProjectName: "Third",
AbortOnExcecutionOrderFail: true,
},
{
CommandName: command.Plan,
ExecutionOrderGroup: 1,
AbortOnExcecutionOrderFail: true,
ProjectName: "Fourth",
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "true",
},
},
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "true",
},
},
{
Command: command.Plan,
Error: errors.New("Shabang!"),
},
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "true",
},
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Once(),
Once(),
Once(),
},
},
{
Description: "Don't block when parallel is not set",
ProjectContexts: []command.ProjectContext{
{
CommandName: command.Plan,
ExecutionOrderGroup: 0,
ProjectName: "First",
AbortOnExcecutionOrderFail: true,
},
{
CommandName: command.Plan,
ExecutionOrderGroup: 1,
ProjectName: "Second",
AbortOnExcecutionOrderFail: true,
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Plan,
Error: errors.New("Shabang!"),
},
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "true",
},
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Once(),
},
},
{
Description: "Don't block when abortOnExcecutionOrderFail is not set",
ProjectContexts: []command.ProjectContext{
{
CommandName: command.Plan,
ExecutionOrderGroup: 0,
ProjectName: "First",
},
{
CommandName: command.Plan,
ExecutionOrderGroup: 1,
ProjectName: "Second",
},
},
ProjectResults: []command.ProjectResult{
{
Command: command.Plan,
Error: errors.New("Shabang!"),
},
{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "true",
},
},
},
RunnerInvokeMatch: []*EqMatcher{
Once(),
Once(),
},
},
}
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
// vcsClient := setup(t)
tmp := t.TempDir()
db, err := db.New(tmp)
Ok(t, err)
vcsClient := setup(t, func(tc *TestConfig) {
tc.backend = db
})
scopeNull, _, _ := metrics.NewLoggingScope(logger, "atlantis")
pull := &github.PullRequest{
State: github.String("open"),
}
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}
cmd := &events.CommentCommand{Name: command.Plan}
ctx := &command.Context{
User: testdata.User,
Log: logging.NewNoopLogger(t),
Scope: scopeNull,
Pull: modelPull,
HeadRepo: testdata.GithubRepo,
Trigger: command.CommentTrigger,
}
When(githubGetter.GetPullRequest(testdata.GithubRepo, testdata.Pull.Num)).ThenReturn(pull, nil)
When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)
When(projectCommandBuilder.BuildPlanCommands(ctx, cmd)).ThenReturn(c.ProjectContexts, nil)
// When(projectCommandBuilder.BuildPlanCommands(ctx, cmd)).Then(func(args []Param) ReturnValues {
// return ReturnValues{[]command.ProjectContext{{CommandName: command.Plan}}, nil}
// })
for i := range c.ProjectContexts {
When(projectCommandRunner.Plan(c.ProjectContexts[i])).ThenReturn(c.ProjectResults[i])
}
planCommandRunner.Run(ctx, cmd)
type RepoModel interface{ models.Repo }
for i := range c.ProjectContexts {
projectCommandRunner.VerifyWasCalled(c.RunnerInvokeMatch[i]).Plan(c.ProjectContexts[i])
}
vcsClient.VerifyWasCalledOnce().CreateComment(
AnyRepo(), EqInt(modelPull.Num), AnyString(), EqString("plan"),
)
})
}
}

View File

@@ -36,6 +36,8 @@ const (
DefaultParallelPlanEnabled = false
// DefaultDeleteSourceBranchOnMerge being false is the default setting whether or not to remove a source branch on merge
DefaultDeleteSourceBranchOnMerge = false
// DefaultAbortOnExcecutionOrderFail being false is the default setting for abort on execution group failiures
DefaultAbortOnExcecutionOrderFail = false
)
func NewInstrumentedProjectCommandBuilder(
@@ -384,6 +386,7 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex
repoCfg.ParallelApply,
repoCfg.ParallelPlan,
verbose,
repoCfg.AbortOnExcecutionOrderFail,
p.TerraformExecutor,
)...)
}
@@ -407,10 +410,12 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex
automerge := DefaultAutomergeEnabled
parallelApply := DefaultParallelApplyEnabled
parallelPlan := DefaultParallelPlanEnabled
abortOnExcecutionOrderFail := DefaultAbortOnExcecutionOrderFail
if hasRepoCfg {
automerge = repoCfg.Automerge
parallelApply = repoCfg.ParallelApply
parallelPlan = repoCfg.ParallelPlan
abortOnExcecutionOrderFail = repoCfg.AbortOnExcecutionOrderFail
}
pCfg := p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), mp.Path, pWorkspace)
@@ -426,6 +431,7 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex
parallelApply,
parallelPlan,
verbose,
abortOnExcecutionOrderFail,
p.TerraformExecutor,
)...)
}
@@ -700,10 +706,12 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Conte
automerge := DefaultAutomergeEnabled
parallelApply := DefaultParallelApplyEnabled
parallelPlan := DefaultParallelPlanEnabled
abortOnExcecutionOrderFail := DefaultAbortOnExcecutionOrderFail
if repoCfgPtr != nil {
automerge = repoCfgPtr.Automerge
parallelApply = repoCfgPtr.ParallelApply
parallelPlan = repoCfgPtr.ParallelPlan
abortOnExcecutionOrderFail = *&repoCfgPtr.AbortOnExcecutionOrderFail
}
if len(matchingProjects) > 0 {
@@ -728,6 +736,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Conte
parallelApply,
parallelPlan,
verbose,
abortOnExcecutionOrderFail,
p.TerraformExecutor,
)...)
}
@@ -751,6 +760,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Conte
parallelApply,
parallelPlan,
verbose,
abortOnExcecutionOrderFail,
p.TerraformExecutor,
)...)
}

View File

@@ -38,7 +38,7 @@ type ProjectCommandContextBuilder interface {
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose bool, terraformClient terraform.Client,
automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool, terraformClient terraform.Client,
) []command.ProjectContext
}
@@ -58,12 +58,13 @@ func (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext(
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose bool, terraformClient terraform.Client,
automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool,
terraformClient terraform.Client,
) (projectCmds []command.ProjectContext) {
cb.ProjectCounter.Inc(1)
cmds := cb.ProjectCommandContextBuilder.BuildProjectContext(
ctx, cmdName, subCmdName, prjCfg, commentFlags, repoDir, automerge, parallelApply, parallelPlan, verbose, terraformClient,
ctx, cmdName, subCmdName, prjCfg, commentFlags, repoDir, automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail, terraformClient,
)
projectCmds = []command.ProjectContext{}
@@ -91,7 +92,8 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose bool, terraformClient terraform.Client,
automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool,
terraformClient terraform.Client,
) (projectCmds []command.ProjectContext) {
ctx.Log.Debug("Building project command context for %s", cmdName)
@@ -139,6 +141,7 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(
parallelApply,
parallelPlan,
verbose,
abortOnExcecutionOrderFail,
ctx.Scope,
ctx.PullRequestStatus,
)
@@ -160,7 +163,8 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose bool, terraformClient terraform.Client,
automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool,
terraformClient terraform.Client,
) (projectCmds []command.ProjectContext) {
ctx.Log.Debug("PolicyChecks are enabled")
@@ -181,6 +185,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
parallelApply,
parallelPlan,
verbose,
abortOnExcecutionOrderFail,
terraformClient,
)
@@ -202,6 +207,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
parallelApply,
parallelPlan,
verbose,
abortOnExcecutionOrderFail,
ctx.Scope,
ctx.PullRequestStatus,
))
@@ -225,6 +231,7 @@ func newProjectCommandContext(ctx *command.Context,
parallelApplyEnabled bool,
parallelPlanEnabled bool,
verbose bool,
abortOnExcecutionOrderFail bool,
scope tally.Scope,
pullStatus models.PullReqStatus,
) command.ProjectContext {
@@ -287,6 +294,7 @@ func newProjectCommandContext(ctx *command.Context,
PullReqStatus: pullStatus,
JobID: uuid.New().String(),
ExecutionOrderGroup: projCfg.ExecutionOrderGroup,
AbortOnExcecutionOrderFail: abortOnExcecutionOrderFail,
}
}

View File

@@ -62,7 +62,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) {
},
}
result := subject.BuildProjectContext(commandCtx, command.Plan, "", projCfg, []string{}, "some/dir", false, false, false, false, terraformClient)
result := subject.BuildProjectContext(commandCtx, command.Plan, "", projCfg, []string{}, "some/dir", false, false, false, false, false, terraformClient)
assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus)
})
@@ -81,7 +81,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) {
},
}
result := subject.BuildProjectContext(commandCtx, command.Plan, "", projCfg, []string{}, "some/dir", false, false, false, false, terraformClient)
result := subject.BuildProjectContext(commandCtx, command.Plan, "", projCfg, []string{}, "some/dir", false, false, false, false, false, terraformClient)
assert.Equal(t, models.ErroredPolicyCheckStatus, result[0].ProjectPlanStatus)
})
@@ -101,9 +101,29 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) {
},
}
result := subject.BuildProjectContext(commandCtx, command.Plan, "", projCfg, []string{}, "some/dir", false, true, false, false, terraformClient)
result := subject.BuildProjectContext(commandCtx, command.Plan, "", projCfg, []string{}, "some/dir", false, true, false, false, false, terraformClient)
assert.True(t, result[0].ParallelApplyEnabled)
assert.False(t, result[0].ParallelPlanEnabled)
})
t.Run("when AbortOnExcecutionOrderFail is set to true", func(t *testing.T) {
projCfg.Name = "Apply Comment"
When(mockCommentBuilder.BuildPlanComment(projRepoRelDir, projWorkspace, "", []string{})).ThenReturn(expectedPlanCmt)
When(mockCommentBuilder.BuildApplyComment(projRepoRelDir, projWorkspace, "", false)).ThenReturn(expectedApplyCmt)
pullStatus.Projects = []models.ProjectStatus{
{
Status: models.ErroredPlanStatus,
RepoRelDir: "dir2",
},
{
Status: models.ErroredPolicyCheckStatus,
RepoRelDir: "dir1",
},
}
result := subject.BuildProjectContext(commandCtx, command.Plan, "", projCfg, []string{}, "some/dir", false, false, false, false, true, terraformClient)
assert.True(t, result[0].AbortOnExcecutionOrderFail)
})
}

View File

@@ -72,6 +72,7 @@ func splitByExecutionOrderGroup(cmds []command.ProjectContext) [][]command.Proje
}
func runProjectCmdsParallelGroups(
ctx *command.Context,
cmds []command.ProjectContext,
runnerFunc prjCmdRunnerFunc,
poolSize int,
@@ -81,6 +82,10 @@ func runProjectCmdsParallelGroups(
for _, group := range groups {
res := runProjectCmdsParallel(group, runnerFunc, poolSize)
results = append(results, res.ProjectResults...)
if res.HasErrors() && group[0].AbortOnExcecutionOrderFail {
ctx.Log.Info("abort on execution order when failed")
break
}
}
return command.Result{ProjectResults: results}

View File

@@ -47,7 +47,7 @@ func (v *VersionCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
var result command.Result
if v.isParallelEnabled(projectCmds) {
ctx.Log.Info("Running version in parallel")
result = runProjectCmdsParallel(projectCmds, v.prjCmdRunner.Version, v.parallelPoolSize)
result = runProjectCmdsParallelGroups(ctx, projectCmds, v.prjCmdRunner.Version, v.parallelPoolSize)
} else {
result = runProjectCmds(projectCmds, v.prjCmdRunner.Version)
}