Skip to content

Commit

Permalink
[ORCA-554] Add support for limiting number of projects per PR. (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
nishkrishnan authored and Nish Krishnan committed Jun 24, 2021
1 parent ee2e5ed commit 0b379f6
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 2 deletions.
9 changes: 9 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/vcs/bitbucketcloud"
"github.com/runatlantis/atlantis/server/events/yaml/valid"
"github.com/runatlantis/atlantis/server/logging"
Expand Down Expand Up @@ -76,6 +77,7 @@ const (
HidePrevPlanComments = "hide-prev-plan-comments"
LogLevelFlag = "log-level"
ParallelPoolSize = "parallel-pool-size"
MaxProjectsPerPR = "max-projects-per-pr"
StatsNamespace = "stats-namespace"
AllowDraftPRs = "allow-draft-prs"
PortFlag = "port"
Expand Down Expand Up @@ -381,6 +383,10 @@ var intFlags = map[string]intFlag{
description: "Max size of the wait group that runs parallel plans and applies (if enabled).",
defaultValue: DefaultParallelPoolSize,
},
MaxProjectsPerPR: {
description: "Max number of projects to operate on in a given pull request.",
defaultValue: events.InfiniteProjectsPerPR,
},
PortFlag: {
description: "Port to bind to.",
defaultValue: DefaultPort,
Expand Down Expand Up @@ -624,6 +630,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) {
if c.TFEHostname == "" {
c.TFEHostname = DefaultTFEHostname
}
if c.MaxProjectsPerPR == 0 {
c.MaxProjectsPerPR = events.InfiniteProjectsPerPR
}
}

func (s *ServerCmd) validate(userConfig server.UserConfig) error {
Expand Down
46 changes: 45 additions & 1 deletion server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ const (
DefaultParallelPlanEnabled = false
// DefaultDeleteSourceBranchOnMerge being false is the default setting whether or not to remove a source branch on merge
DefaultDeleteSourceBranchOnMerge = false
// InfiniteProjectLimitPerPR is the default setting for number of projects per PR.
// this is set to -1 to signify no limit.
InfiniteProjectsPerPR = -1
)

func NewProjectCommandBuilder(
Expand All @@ -47,7 +50,43 @@ func NewProjectCommandBuilder(
scope stats.Scope,
logger logging.SimpleLogging,
) ProjectCommandBuilder {
projectCommandBuilder := &DefaultProjectCommandBuilder{
return NewProjectCommandBuilderWithLimit(
policyChecksSupported,
parserValidator,
projectFinder,
vcsClient,
workingDir,
workingDirLocker,
globalCfg,
pendingPlanFinder,
commentBuilder,
skipCloneNoChanges,
EnableRegExpCmd,
AutoplanFileList,
scope,
logger,
InfiniteProjectsPerPR,
)
}

func NewProjectCommandBuilderWithLimit(
policyChecksSupported bool,
parserValidator *yaml.ParserValidator,
projectFinder ProjectFinder,
vcsClient vcs.Client,
workingDir WorkingDir,
workingDirLocker WorkingDirLocker,
globalCfg valid.GlobalCfg,
pendingPlanFinder *DefaultPendingPlanFinder,
commentBuilder CommentBuilder,
skipCloneNoChanges bool,
EnableRegExpCmd bool,
AutoplanFileList string,
scope stats.Scope,
logger logging.SimpleLogging,
limit int,
) ProjectCommandBuilder {
var projectCommandBuilder ProjectCommandBuilder = &DefaultProjectCommandBuilder{
ParserValidator: parserValidator,
ProjectFinder: projectFinder,
VCSClient: vcsClient,
Expand All @@ -65,6 +104,11 @@ func NewProjectCommandBuilder(
),
}

projectCommandBuilder = &SizeLimitedProjectCommandBuilder{
Limit: limit,
ProjectCommandBuilder: projectCommandBuilder,
}

return &InstrumentedProjectCommandBuilder{
ProjectCommandBuilder: projectCommandBuilder,
Logger: logger,
Expand Down
48 changes: 48 additions & 0 deletions server/events/size_limited_project_command_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package events

import (
"fmt"

"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events/models"
)

type SizeLimitedProjectCommandBuilder struct {
Limit int
ProjectCommandBuilder
}

func (b *SizeLimitedProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext) ([]models.ProjectCommandContext, error) {
projects, err := b.ProjectCommandBuilder.BuildAutoplanCommands(ctx)

if err != nil {
return projects, err
}

return projects, b.CheckAgainstLimit(projects)
}

func (b *SizeLimitedProjectCommandBuilder) BuildPlanCommands(ctx *CommandContext, comment *CommentCommand) ([]models.ProjectCommandContext, error) {
projects, err := b.ProjectCommandBuilder.BuildPlanCommands(ctx, comment)

if err != nil {
return projects, err
}

return projects, b.CheckAgainstLimit(projects)
}

func (b *SizeLimitedProjectCommandBuilder) CheckAgainstLimit(projects []models.ProjectCommandContext) error {
if b.Limit != InfiniteProjectsPerPR && len(projects) > b.Limit {
return errors.New(
fmt.Sprintf(
"Number of projects cannot exceed %d. This can either be caused by:\n"+
"1) GH failure in recognizing the diff\n"+
"2) Pull Request batch is too large for the given Atlantis instance\n\n"+
"Please break this pull request into smaller batches and try again.",
b.Limit,
),
)
}
return nil
}
143 changes: 143 additions & 0 deletions server/events/size_limited_project_command_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
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/testing"
)

func TestSizeLimitedProjectCommandBuilder_autoplan(t *testing.T) {
RegisterMockTestingT(t)

delegate := mocks.NewMockProjectCommandBuilder()

ctx := &events.CommandContext{}

project1 := models.ProjectCommandContext{
ProjectName: "test1",
}

project2 := models.ProjectCommandContext{
ProjectName: "test2",
}

expectedResult := []models.ProjectCommandContext{project1, project2}

t.Run("Limit Defined and Breached", func(t *testing.T) {
subject := &events.SizeLimitedProjectCommandBuilder{
Limit: 1,
ProjectCommandBuilder: delegate,
}

When(delegate.BuildAutoplanCommands(ctx)).ThenReturn(expectedResult, nil)

_, err := subject.BuildAutoplanCommands(ctx)

ErrEquals(t, `Number of projects cannot exceed 1. This can either be caused by:
1) GH failure in recognizing the diff
2) Pull Request batch is too large for the given Atlantis instance
Please break this pull request into smaller batches and try again.`, err)
})

t.Run("Limit defined and not breached", func(t *testing.T) {
subject := &events.SizeLimitedProjectCommandBuilder{
Limit: 2,
ProjectCommandBuilder: delegate,
}

When(delegate.BuildAutoplanCommands(ctx)).ThenReturn(expectedResult, nil)

result, err := subject.BuildAutoplanCommands(ctx)

Ok(t, err)

Assert(t, len(result) == len(expectedResult), "size is expected")
})

t.Run("Limit not defined", func(t *testing.T) {
subject := &events.SizeLimitedProjectCommandBuilder{
Limit: events.InfiniteProjectsPerPR,
ProjectCommandBuilder: delegate,
}

When(delegate.BuildAutoplanCommands(ctx)).ThenReturn(expectedResult, nil)

result, err := subject.BuildAutoplanCommands(ctx)

Ok(t, err)

Assert(t, len(result) == len(expectedResult), "size is expected")
})
}

func TestSizeLimitedProjectCommandBuilder_planComment(t *testing.T) {
RegisterMockTestingT(t)

delegate := mocks.NewMockProjectCommandBuilder()

ctx := &events.CommandContext{}

comment := &events.CommentCommand{}

project1 := models.ProjectCommandContext{
ProjectName: "test1",
}

project2 := models.ProjectCommandContext{
ProjectName: "test2",
}

expectedResult := []models.ProjectCommandContext{project1, project2}

t.Run("Limit Defined and Breached", func(t *testing.T) {
subject := &events.SizeLimitedProjectCommandBuilder{
Limit: 1,
ProjectCommandBuilder: delegate,
}

When(delegate.BuildPlanCommands(ctx, comment)).ThenReturn(expectedResult, nil)

_, err := subject.BuildPlanCommands(ctx, comment)

ErrEquals(t, `Number of projects cannot exceed 1. This can either be caused by:
1) GH failure in recognizing the diff
2) Pull Request batch is too large for the given Atlantis instance
Please break this pull request into smaller batches and try again.`, err)
})

t.Run("Limit defined and not breached", func(t *testing.T) {
subject := &events.SizeLimitedProjectCommandBuilder{
Limit: 2,
ProjectCommandBuilder: delegate,
}

When(delegate.BuildPlanCommands(ctx, comment)).ThenReturn(expectedResult, nil)

result, err := subject.BuildPlanCommands(ctx, comment)

Ok(t, err)

Assert(t, len(result) == len(expectedResult), "size is expected")
})

t.Run("Limit not defined", func(t *testing.T) {
subject := &events.SizeLimitedProjectCommandBuilder{
Limit: events.InfiniteProjectsPerPR,
ProjectCommandBuilder: delegate,
}

When(delegate.BuildPlanCommands(ctx, comment)).ThenReturn(expectedResult, nil)

result, err := subject.BuildPlanCommands(ctx, comment)

Ok(t, err)

Assert(t, len(result) == len(expectedResult), "size is expected")
})
}
3 changes: 2 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
WorkingDir: workingDir,
PreWorkflowHookRunner: runtime.DefaultPreWorkflowHookRunner{},
}
projectCommandBuilder := events.NewProjectCommandBuilder(
projectCommandBuilder := events.NewProjectCommandBuilderWithLimit(
policyChecksEnabled,
validator,
&events.DefaultProjectFinder{},
Expand All @@ -441,6 +441,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
userConfig.AutoplanFileList,
statsScope,
logger,
userConfig.MaxProjectsPerPR,
)

showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfVersion)
Expand Down
1 change: 1 addition & 0 deletions server/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type UserConfig struct {
HidePrevPlanComments bool `mapstructure:"hide-prev-plan-comments"`
LogLevel string `mapstructure:"log-level"`
ParallelPoolSize int `mapstructure:"parallel-pool-size"`
MaxProjectsPerPR int `mapstructure:"max-projects-per-pr"`
StatsNamespace string `mapstructure:"stats-namespace"`
PlanDrafts bool `mapstructure:"allow-draft-prs"`
Port int `mapstructure:"port"`
Expand Down

0 comments on commit 0b379f6

Please sign in to comment.