Add parallel plans via repo config

This commit is contained in:
Marc Barlo
2020-02-07 14:21:53 -08:00
parent 885ad266bb
commit f2609fd70b
6 changed files with 156 additions and 61 deletions

View File

@@ -15,6 +15,7 @@ package events
import (
"fmt"
"sync"
"github.com/google/go-github/v28/github"
"github.com/mcdafydd/go-azuredevops/azuredevops"
@@ -134,7 +135,15 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo
return
}
result := c.runProjectCmds(projectCmds, models.PlanCommand)
// Run our plan commands in parallel if enabled
var result CommandResult
if c.parallelPlansEnabled(ctx, projectCmds) {
ctx.Log.Info("Running plans in parallel")
result = c.runProjectCmdsParallel(projectCmds, models.PlanCommand)
} else {
result = c.runProjectCmds(projectCmds, models.PlanCommand)
}
if c.automergeEnabled(ctx, projectCmds) && result.HasErrors() {
ctx.Log.Info("deleting plans because there were errors and automerge requires all plans succeed")
c.deletePlans(ctx)
@@ -244,7 +253,14 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead
return
}
result := c.runProjectCmds(projectCmds, cmd.Name)
// Run our plan commands in parallel if enabled
var result CommandResult
if cmd.Name == models.PlanCommand && c.parallelPlansEnabled(ctx, projectCmds) {
ctx.Log.Info("Running plans in parallel")
result = c.runProjectCmdsParallel(projectCmds, cmd.Name)
} else {
result = c.runProjectCmds(projectCmds, cmd.Name)
}
if cmd.Name == models.PlanCommand && c.automergeEnabled(ctx, projectCmds) && result.HasErrors() {
ctx.Log.Info("deleting plans because there were errors and automerge requires all plans succeed")
c.deletePlans(ctx)
@@ -329,6 +345,41 @@ func (c *DefaultCommandRunner) automerge(ctx *CommandContext, pullStatus models.
}
}
func (c *DefaultCommandRunner) runProjectCmdsParallel(cmds []models.ProjectCommandContext, cmdName models.CommandName) CommandResult {
var results []models.ProjectResult
var wg sync.WaitGroup
mux := &sync.Mutex{}
wg.Add(len(cmds))
for _, pCmd := range cmds {
pCmd := pCmd
var execute func()
switch cmdName {
case models.PlanCommand:
execute = func() {
defer wg.Done()
res := c.ProjectCommandRunner.Plan(pCmd)
mux.Lock()
results = append(results, res)
mux.Unlock()
}
case models.ApplyCommand:
execute = func() {
defer wg.Done()
res := c.ProjectCommandRunner.Apply(pCmd)
mux.Lock()
results = append(results, res)
mux.Unlock()
}
}
go execute()
}
wg.Wait()
return CommandResult{ProjectResults: results}
}
func (c *DefaultCommandRunner) runProjectCmds(cmds []models.ProjectCommandContext, cmdName models.CommandName) CommandResult {
var results []models.ProjectResult
for _, pCmd := range cmds {
@@ -477,6 +528,11 @@ func (c *DefaultCommandRunner) automergeEnabled(ctx *CommandContext, projectCmds
(len(projectCmds) > 0 && projectCmds[0].AutomergeEnabled)
}
// parallelPlansEnabled returns true if parallel plans is enabled in this context.
func (c *DefaultCommandRunner) parallelPlansEnabled(ctx *CommandContext, projectCmds []models.ProjectCommandContext) bool {
return len(projectCmds) > 0 && projectCmds[0].ParallelPlansEnabled
}
// automergeComment is the comment that gets posted when Atlantis automatically
// merges the PR.
var automergeComment = `Automatically merging because all plans have been successfully applied.`

View File

@@ -305,9 +305,11 @@ type ProjectCommandContext struct {
// ApplyRequirements is the list of requirements that must be satisfied
// before we will run the apply stage.
ApplyRequirements []string
// AutoplanEnabled is true if automerge is enabled for the repo that this
// AutomergeEnabled is true if automerge is enabled for the repo that this
// project is in.
AutomergeEnabled bool
// ParallelPlansEnabled is true if parallel plans is enabled for this project.
ParallelPlansEnabled bool
// AutoplanEnabled is true if autoplanning is enabled for this project.
AutoplanEnabled bool
// BaseRepo is the repository that the pull request will be merged into.

View File

@@ -25,6 +25,8 @@ const (
DefaultWorkspace = "default"
// DefaultAutomergeEnabled is the default for the automerge setting.
DefaultAutomergeEnabled = false
// DefaultParallelPlansEnabled is the default for the parallel plans setting.
DefaultParallelPlansEnabled = false
)
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder
@@ -141,7 +143,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
for _, mp := range matchingProjects {
ctx.Log.Debug("determining config for project at dir: %q workspace: %q", mp.Dir, mp.Workspace)
mergedCfg := p.GlobalCfg.MergeProjectCfg(ctx.Log, ctx.BaseRepo.ID(), mp, repoCfg)
projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, mergedCfg, commentFlags, repoCfg.Automerge, verbose, repoDir))
projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, mergedCfg, commentFlags, repoCfg.Automerge, repoCfg.ParallelPlans, verbose, repoDir))
}
} else {
// If there is no config file, then we'll plan each project that
@@ -152,7 +154,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
for _, mp := range modifiedProjects {
ctx.Log.Debug("determining config for project at dir: %q", mp.Path)
pCfg := p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.BaseRepo.ID(), mp.Path, DefaultWorkspace)
projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, pCfg, commentFlags, DefaultAutomergeEnabled, verbose, repoDir))
projCtxs = append(projCtxs, p.buildCtx(ctx, models.PlanCommand, pCfg, commentFlags, DefaultAutomergeEnabled, repoCfg.ParallelPlans, verbose, repoDir))
}
}
@@ -283,10 +285,12 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(
}
automerge := DefaultAutomergeEnabled
parallelPlans := DefaultParallelPlansEnabled
if repoCfgPtr != nil {
automerge = repoCfgPtr.Automerge
parallelPlans = repoCfgPtr.ParallelPlans
}
return p.buildCtx(ctx, cmd, projCfg, commentFlags, automerge, verbose, repoDir), nil
return p.buildCtx(ctx, cmd, projCfg, commentFlags, automerge, parallelPlans, verbose, repoDir), nil
}
// getCfg returns the atlantis.yaml config (if it exists) for this project. If
@@ -375,6 +379,7 @@ func (p *DefaultProjectCommandBuilder) buildCtx(ctx *CommandContext,
projCfg valid.MergedProjectCfg,
commentArgs []string,
automergeEnabled bool,
parallelPlansEnabled bool,
verbose bool,
absRepoDir string) models.ProjectCommandContext {
@@ -393,25 +398,26 @@ func (p *DefaultProjectCommandBuilder) buildCtx(ctx *CommandContext,
}
return models.ProjectCommandContext{
ApplyCmd: p.CommentBuilder.BuildApplyComment(projCfg.RepoRelDir, projCfg.Workspace, projCfg.Name),
BaseRepo: ctx.BaseRepo,
EscapedCommentArgs: p.escapeArgs(commentArgs),
AutomergeEnabled: automergeEnabled,
AutoplanEnabled: projCfg.AutoplanEnabled,
Steps: steps,
HeadRepo: ctx.HeadRepo,
Log: ctx.Log,
PullMergeable: ctx.PullMergeable,
Pull: ctx.Pull,
ProjectName: projCfg.Name,
ApplyRequirements: projCfg.ApplyRequirements,
RePlanCmd: p.CommentBuilder.BuildPlanComment(projCfg.RepoRelDir, projCfg.Workspace, projCfg.Name, commentArgs),
RepoRelDir: projCfg.RepoRelDir,
RepoConfigVersion: projCfg.RepoCfgVersion,
TerraformVersion: projCfg.TerraformVersion,
User: ctx.User,
Verbose: verbose,
Workspace: projCfg.Workspace,
ApplyCmd: p.CommentBuilder.BuildApplyComment(projCfg.RepoRelDir, projCfg.Workspace, projCfg.Name),
BaseRepo: ctx.BaseRepo,
EscapedCommentArgs: p.escapeArgs(commentArgs),
AutomergeEnabled: automergeEnabled,
ParallelPlansEnabled: parallelPlansEnabled,
AutoplanEnabled: projCfg.AutoplanEnabled,
Steps: steps,
HeadRepo: ctx.HeadRepo,
Log: ctx.Log,
PullMergeable: ctx.PullMergeable,
Pull: ctx.Pull,
ProjectName: projCfg.Name,
ApplyRequirements: projCfg.ApplyRequirements,
RePlanCmd: p.CommentBuilder.BuildPlanComment(projCfg.RepoRelDir, projCfg.Workspace, projCfg.Name, commentArgs),
RepoRelDir: projCfg.RepoRelDir,
RepoConfigVersion: projCfg.RepoCfgVersion,
TerraformVersion: projCfg.TerraformVersion,
User: ctx.User,
Verbose: verbose,
Workspace: projCfg.Workspace,
}
}

View File

@@ -10,12 +10,16 @@ import (
// DefaultAutomerge is the default setting for automerge.
const DefaultAutomerge = false
// DefaultParallelPlans is the default setting for parallel plans
const DefaultParallelPlans = false
// RepoCfg is the raw schema for repo-level atlantis.yaml config.
type RepoCfg struct {
Version *int `yaml:"version,omitempty"`
Projects []Project `yaml:"projects,omitempty"`
Workflows map[string]Workflow `yaml:"workflows,omitempty"`
Automerge *bool `yaml:"automerge,omitempty"`
Version *int `yaml:"version,omitempty"`
Projects []Project `yaml:"projects,omitempty"`
Workflows map[string]Workflow `yaml:"workflows,omitempty"`
Automerge *bool `yaml:"automerge,omitempty"`
ParallelPlans *bool `yaml:"parallel_plans,omitempty"`
}
func (r RepoCfg) Validate() error {
@@ -52,10 +56,16 @@ func (r RepoCfg) ToValid() valid.RepoCfg {
automerge = *r.Automerge
}
parallelPlans := DefaultParallelPlans
if r.ParallelPlans != nil {
parallelPlans = *r.ParallelPlans
}
return valid.RepoCfg{
Version: *r.Version,
Projects: validProjects,
Workflows: validWorkflows,
Automerge: automerge,
Version: *r.Version,
Projects: validProjects,
Workflows: validWorkflows,
Automerge: automerge,
ParallelPlans: parallelPlans,
}
}

View File

@@ -111,11 +111,22 @@ func TestConfig_UnmarshalYAML(t *testing.T) {
},
expErr: "yaml: unmarshal errors:\n line 2: cannot unmarshal !!str `notabool` into bool",
},
{
description: "parallel plans not a boolean",
input: "version: 3\nparallel_plans: notabool",
exp: raw.RepoCfg{
Version: nil,
Projects: nil,
Workflows: nil,
},
expErr: "yaml: unmarshal errors:\n line 2: cannot unmarshal !!str `notabool` into bool",
},
{
description: "should use values if set",
input: `
version: 3
automerge: true
parallel_plans: true
projects:
- dir: mydir
workspace: myworkspace
@@ -132,8 +143,9 @@ workflows:
apply:
steps: []`,
exp: raw.RepoCfg{
Version: Int(3),
Automerge: Bool(true),
Version: Int(3),
Automerge: Bool(true),
ParallelPlans: Bool(true),
Projects: []raw.Project{
{
Dir: String("mydir"),
@@ -236,38 +248,43 @@ func TestConfig_ToValid(t *testing.T) {
},
},
{
description: "automerge omitted",
description: "automerge and parallel_plans omitted",
input: raw.RepoCfg{
Version: Int(2),
},
exp: valid.RepoCfg{
Version: 2,
Automerge: false,
Workflows: map[string]valid.Workflow{},
Version: 2,
Automerge: false,
ParallelPlans: false,
Workflows: map[string]valid.Workflow{},
},
},
{
description: "automerge true",
description: "automerge and parallel_plans true",
input: raw.RepoCfg{
Version: Int(2),
Automerge: Bool(true),
Version: Int(2),
Automerge: Bool(true),
ParallelPlans: Bool(true),
},
exp: valid.RepoCfg{
Version: 2,
Automerge: true,
Workflows: map[string]valid.Workflow{},
Version: 2,
Automerge: true,
ParallelPlans: true,
Workflows: map[string]valid.Workflow{},
},
},
{
description: "automerge false",
description: "automerge and parallel_plans false",
input: raw.RepoCfg{
Version: Int(2),
Automerge: Bool(false),
Version: Int(2),
Automerge: Bool(false),
ParallelPlans: Bool(false),
},
exp: valid.RepoCfg{
Version: 2,
Automerge: false,
Workflows: map[string]valid.Workflow{},
Version: 2,
Automerge: false,
ParallelPlans: false,
Workflows: map[string]valid.Workflow{},
},
},
{
@@ -282,8 +299,9 @@ func TestConfig_ToValid(t *testing.T) {
},
},
exp: valid.RepoCfg{
Version: 2,
Automerge: false,
Version: 2,
Automerge: false,
ParallelPlans: false,
Workflows: map[string]valid.Workflow{
"myworkflow": {
Name: "myworkflow",
@@ -302,8 +320,9 @@ func TestConfig_ToValid(t *testing.T) {
{
description: "everything set",
input: raw.RepoCfg{
Version: Int(2),
Automerge: Bool(true),
Version: Int(2),
Automerge: Bool(true),
ParallelPlans: Bool(true),
Workflows: map[string]raw.Workflow{
"myworkflow": {
Apply: &raw.Stage{
@@ -329,8 +348,9 @@ func TestConfig_ToValid(t *testing.T) {
},
},
exp: valid.RepoCfg{
Version: 2,
Automerge: true,
Version: 2,
Automerge: true,
ParallelPlans: true,
Workflows: map[string]valid.Workflow{
"myworkflow": {
Name: "myworkflow",

View File

@@ -7,10 +7,11 @@ import version "github.com/hashicorp/go-version"
// RepoCfg is the atlantis.yaml config after it's been parsed and validated.
type RepoCfg struct {
// Version is the version of the atlantis YAML file.
Version int
Projects []Project
Workflows map[string]Workflow
Automerge bool
Version int
Projects []Project
Workflows map[string]Workflow
Automerge bool
ParallelPlans bool
}
func (r RepoCfg) FindProjectsByDirWorkspace(repoRelDir string, workspace string) []Project {