Skip to content

Commit

Permalink
Skip cloning PR repository in case of no projects configured in repo …
Browse files Browse the repository at this point in the history
…config file were changed
  • Loading branch information
Quan Hoang committed Jun 8, 2020
1 parent 25cd66d commit e66a3a2
Show file tree
Hide file tree
Showing 19 changed files with 274 additions and 111 deletions.
5 changes: 5 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const (
SilenceForkPRErrorsFlag = "silence-fork-pr-errors"
SilenceVCSStatusNoPlans = "silence-vcs-status-no-plans"
SilenceWhitelistErrorsFlag = "silence-whitelist-errors"
SkipCloneNoChanges = "skip-clone-no-changes"
SlackTokenFlag = "slack-token"
SSLCertFileFlag = "ssl-cert-file"
SSLKeyFileFlag = "ssl-key-file"
Expand Down Expand Up @@ -294,6 +295,10 @@ var boolFlags = map[string]boolFlag{
" This writes secrets to disk and should only be enabled in a secure environment.",
defaultValue: false,
},
SkipCloneNoChanges: {
description: "Skips cloning the PR repo if there are no projects were changed in the PR.",
defaultValue: false,
},
}
var intFlags = map[string]intFlag{
PortFlag: {
Expand Down
1 change: 1 addition & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ var testFlags = map[string]interface{}{
SilenceForkPRErrorsFlag: true,
SilenceWhitelistErrorsFlag: true,
SilenceVCSStatusNoPlans: true,
SkipCloneNoChanges: true,
SlackTokenFlag: "slack-token",
SSLCertFileFlag: "cert-file",
SSLKeyFileFlag: "key-file",
Expand Down
62 changes: 44 additions & 18 deletions server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ type ProjectCommandBuilder interface {
// This class combines the data from the comment and any atlantis.yaml file or
// Atlantis server config and then generates a set of contexts.
type DefaultProjectCommandBuilder struct {
ParserValidator *yaml.ParserValidator
ProjectFinder ProjectFinder
VCSClient vcs.Client
WorkingDir WorkingDir
WorkingDirLocker WorkingDirLocker
GlobalCfg valid.GlobalCfg
PendingPlanFinder *DefaultPendingPlanFinder
CommentBuilder CommentBuilder
ParserValidator *yaml.ParserValidator
ProjectFinder ProjectFinder
VCSClient vcs.Client
WorkingDir WorkingDir
WorkingDirLocker WorkingDirLocker
GlobalCfg valid.GlobalCfg
PendingPlanFinder *DefaultPendingPlanFinder
CommentBuilder CommentBuilder
SkipCloneNoChanges bool
}

// See ProjectCommandBuilder.BuildAutoplanCommands.
Expand Down Expand Up @@ -100,8 +101,41 @@ func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *CommandContext, c
// buildPlanAllCommands builds plan contexts for all projects we determine were
// modified in this ctx.
func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, commentFlags []string, verbose bool) ([]models.ProjectCommandContext, error) {
// We'll need the list of modified files.
modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.BaseRepo, ctx.Pull)

if err != nil {
return nil, err
}
ctx.Log.Debug("%d files were modified in this pull request", len(modifiedFiles))

if p.SkipCloneNoChanges && p.VCSClient.IsSupportDownloadSingleFile(ctx.BaseRepo) {
hasRepoCfg, repoCfgData, err := p.VCSClient.DownloadRepoConfigFile(ctx.Pull)
if err != nil {
return nil, errors.Wrapf(err, "downloading %s", yaml.AtlantisYAMLFilename)
}

if hasRepoCfg {
repoCfg, err := p.ParserValidator.ParseRepoCfg(repoCfgData, "", p.GlobalCfg, ctx.BaseRepo.ID())
if err != nil {
return nil, errors.Wrapf(err, "parsing %s", yaml.AtlantisYAMLFilename)
}
ctx.Log.Info("successfully parsed remote %s file", yaml.AtlantisYAMLFilename)
matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, "")
if err != nil {
return nil, err
}
ctx.Log.Info("%d projects are changed on MR %q based on their when_modified config", len(matchingProjects), ctx.Pull.Num)
if len(matchingProjects) == 0 {
ctx.Log.Info("skipping repo clone since no project was modified")
return []models.ProjectCommandContext{}, nil
}
}
}

// Need to lock the workspace we're about to clone to.
workspace := DefaultWorkspace

unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, ctx.Pull.Num, workspace)
if err != nil {
ctx.Log.Warn("workspace was locked")
Expand All @@ -110,18 +144,10 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
ctx.Log.Debug("got workspace lock")
defer unlockFn()

// We'll need the list of modified files.
modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.BaseRepo, ctx.Pull)
if err != nil {
return nil, err
}
ctx.Log.Debug("%d files were modified in this pull request", len(modifiedFiles))

repoDir, _, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, workspace)
if err != nil {
return nil, err
}

// Parse config file if it exists.
hasRepoCfg, err := p.ParserValidator.HasRepoCfg(repoDir)
if err != nil {
Expand All @@ -132,7 +158,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
if hasRepoCfg {
// If there's a repo cfg then we'll use it to figure out which projects
// should be planed.
repoCfg, err := p.ParserValidator.ParseRepoCfg(repoDir, p.GlobalCfg, ctx.BaseRepo.ID())
repoCfg, err := p.ParserValidator.ParseRepoCfg([]byte{}, repoDir, p.GlobalCfg, ctx.BaseRepo.ID())
if err != nil {
return nil, errors.Wrapf(err, "parsing %s", yaml.AtlantisYAMLFilename)
}
Expand Down Expand Up @@ -314,7 +340,7 @@ func (p *DefaultProjectCommandBuilder) getCfg(ctx *CommandContext, projectName s
}

var repoConfig valid.RepoCfg
repoConfig, err = p.ParserValidator.ParseRepoCfg(repoDir, p.GlobalCfg, ctx.BaseRepo.ID())
repoConfig, err = p.ParserValidator.ParseRepoCfg([]byte{}, repoDir, p.GlobalCfg, ctx.BaseRepo.ID())
if err != nil {
return
}
Expand Down
17 changes: 9 additions & 8 deletions server/events/project_command_builder_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,14 +563,15 @@ projects:
}

builder := &DefaultProjectCommandBuilder{
WorkingDirLocker: NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: parser,
VCSClient: vcsClient,
ProjectFinder: &DefaultProjectFinder{},
PendingPlanFinder: &DefaultPendingPlanFinder{},
CommentBuilder: &CommentParser{},
GlobalCfg: globalCfg,
WorkingDirLocker: NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: parser,
VCSClient: vcsClient,
ProjectFinder: &DefaultProjectFinder{},
PendingPlanFinder: &DefaultPendingPlanFinder{},
CommentBuilder: &CommentParser{},
GlobalCfg: globalCfg,
SkipCloneNoChanges: false,
}

// We run a test for each type of command.
Expand Down
109 changes: 58 additions & 51 deletions server/events/project_command_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,15 @@ projects:
}

builder := &events.DefaultProjectCommandBuilder{
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: vcsClient,
ProjectFinder: &events.DefaultProjectFinder{},
PendingPlanFinder: &events.DefaultPendingPlanFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(false, false, false),
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: vcsClient,
ProjectFinder: &events.DefaultProjectFinder{},
PendingPlanFinder: &events.DefaultPendingPlanFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(false, false, false),
SkipCloneNoChanges: false,
}

ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{
Expand Down Expand Up @@ -358,13 +359,14 @@ projects:
}

builder := &events.DefaultProjectCommandBuilder{
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: vcsClient,
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: vcsClient,
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
SkipCloneNoChanges: false,
}

var actCtxs []models.ProjectCommandContext
Expand Down Expand Up @@ -491,13 +493,14 @@ projects:
}

builder := &events.DefaultProjectCommandBuilder{
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: vcsClient,
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: vcsClient,
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
SkipCloneNoChanges: false,
}

ctxs, err := builder.BuildPlanCommands(
Expand Down Expand Up @@ -562,14 +565,15 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {
ThenReturn(tmpDir, nil)

builder := &events.DefaultProjectCommandBuilder{
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: nil,
ProjectFinder: &events.DefaultProjectFinder{},
PendingPlanFinder: &events.DefaultPendingPlanFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(false, false, false),
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: nil,
ProjectFinder: &events.DefaultProjectFinder{},
PendingPlanFinder: &events.DefaultPendingPlanFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(false, false, false),
SkipCloneNoChanges: false,
}

ctxs, err := builder.BuildApplyCommands(
Expand Down Expand Up @@ -630,13 +634,14 @@ projects:
AnyString())).ThenReturn(repoDir, nil)

builder := &events.DefaultProjectCommandBuilder{
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: nil,
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: nil,
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
SkipCloneNoChanges: false,
}

ctx := &events.CommandContext{
Expand Down Expand Up @@ -692,13 +697,14 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"main.tf"}, nil)

builder := &events.DefaultProjectCommandBuilder{
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: vcsClient,
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
ParserValidator: &yaml.ParserValidator{},
VCSClient: vcsClient,
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
SkipCloneNoChanges: false,
}

var actCtxs []models.ProjectCommandContext
Expand Down Expand Up @@ -856,13 +862,14 @@ projects:
AnyString())).ThenReturn(tmpDir, nil)

builder := &events.DefaultProjectCommandBuilder{
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
VCSClient: vcsClient,
ParserValidator: &yaml.ParserValidator{},
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
WorkingDir: workingDir,
VCSClient: vcsClient,
ParserValidator: &yaml.ParserValidator{},
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
SkipCloneNoChanges: false,
}

actCtxs, err := builder.BuildPlanCommands(
Expand Down
8 changes: 8 additions & 0 deletions server/events/vcs/azuredevops_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,11 @@ func SplitAzureDevopsRepoFullName(repoFullName string) (owner string, project st
}
return repoFullName[:lastSlashIdx], "", repoFullName[lastSlashIdx+1:]
}

func (g *AzureDevopsClient) IsSupportDownloadSingleFile(repo models.Repo) bool {
return false
}

func (g *AzureDevopsClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
return false, []byte{}, fmt.Errorf("Not Implemented")
}
11 changes: 11 additions & 0 deletions server/events/vcs/bitbucketcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,14 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b
}
return respBody, nil
}

func (b *Client) IsSupportDownloadSingleFile(models.Repo) bool {
return false
}

// DownloadRepoConfigFile return `atlantis.yaml` content from VCS (which support fetch a single file from repository)
// The first return value indicate that repo contain atlantis.yaml or not
// if BaseRepo had one repo config file, its content will placed on the second return value
func (b *Client) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
return false, []byte{}, fmt.Errorf("Not Implemented")
}
11 changes: 11 additions & 0 deletions server/events/vcs/bitbucketserver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,14 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b
}
return respBody, nil
}

func (b *Client) IsSupportDownloadSingleFile(repo models.Repo) bool {
return false
}

// DownloadRepoConfigFile return `atlantis.yaml` content from VCS (which support fetch a single file from repository)
// The first return value indicate that repo contain atlantis.yaml or not
// if BaseRepo had one repo config file, its content will placed on the second return value
func (b *Client) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
return false, []byte{}, fmt.Errorf("not implemented")
}
6 changes: 6 additions & 0 deletions server/events/vcs/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ type Client interface {
UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error
MergePull(pull models.PullRequest) error
MarkdownPullLink(pull models.PullRequest) (string, error)

// DownloadRepoConfigFile return `atlantis.yaml` content from VCS (which support fetch a single file from repository)
// The first return value indicate that repo contain atlantis.yaml or not
// if BaseRepo had one repo config file, its content will placed on the second return value
DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error)
IsSupportDownloadSingleFile(repo models.Repo) bool
}
Loading

0 comments on commit e66a3a2

Please sign in to comment.