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 May 18, 2020
1 parent 39c5ad8 commit 1382d49
Show file tree
Hide file tree
Showing 19 changed files with 212 additions and 60 deletions.
5 changes: 5 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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 @@ -289,6 +290,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 @@ -82,6 +82,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
85 changes: 56 additions & 29 deletions server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type DefaultProjectCommandBuilder struct {
GlobalCfg valid.GlobalCfg
PendingPlanFinder *DefaultPendingPlanFinder
CommentBuilder CommentBuilder
SkipCloneNoChanges bool
}

// See ProjectCommandBuilder.BuildAutoplanCommands.
Expand Down Expand Up @@ -96,39 +97,64 @@ 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) {
// 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")
return nil, err
}
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 {
return nil, errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir)
}
// 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")
return nil, err
}
ctx.Log.Debug("got workspace lock")
defer unlockFn()

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 {
return nil, errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir)
}

var projCtxs []models.ProjectCommandContext
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 @@ -159,6 +185,7 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext,
return projCtxs, nil
}


// buildProjectPlanCommand builds a plan context for a single project.
// cmd must be for only one project.
func (p *DefaultProjectCommandBuilder) buildProjectPlanCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) {
Expand Down Expand Up @@ -306,7 +333,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
1 change: 1 addition & 0 deletions server/events/project_command_builder_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ projects:
PendingPlanFinder: &DefaultPendingPlanFinder{},
CommentBuilder: &CommentParser{},
GlobalCfg: globalCfg,
SkipCloneNoChanges: false,
}

// We run a test for each type of command.
Expand Down
7 changes: 7 additions & 0 deletions server/events/project_command_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ projects:
PendingPlanFinder: &events.DefaultPendingPlanFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(false, false, false),
SkipCloneNoChanges: false,
}

ctxs, err := builder.BuildAutoplanCommands(&events.CommandContext{
Expand Down Expand Up @@ -365,6 +366,7 @@ projects:
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
SkipCloneNoChanges: false,
}

var actCtxs []models.ProjectCommandContext
Expand Down Expand Up @@ -498,6 +500,7 @@ projects:
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
SkipCloneNoChanges: false,
}

ctxs, err := builder.BuildPlanCommands(
Expand Down Expand Up @@ -570,6 +573,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {
PendingPlanFinder: &events.DefaultPendingPlanFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(false, false, false),
SkipCloneNoChanges: false,
}

ctxs, err := builder.BuildApplyCommands(
Expand Down Expand Up @@ -637,6 +641,7 @@ projects:
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
SkipCloneNoChanges: false,
}

ctx := &events.CommandContext{
Expand Down Expand Up @@ -699,6 +704,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {
ProjectFinder: &events.DefaultProjectFinder{},
CommentBuilder: &events.CommentParser{},
GlobalCfg: valid.NewGlobalCfg(true, false, false),
SkipCloneNoChanges: false,
}

var actCtxs []models.ProjectCommandContext
Expand Down Expand Up @@ -863,6 +869,7 @@ projects:
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")
}
8 changes: 8 additions & 0 deletions server/events/vcs/bitbucketcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,11 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b
}
return respBody, nil
}

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

func (b *Client) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
return false, []byte{}, fmt.Errorf("Not Implemented")
}
8 changes: 8 additions & 0 deletions server/events/vcs/bitbucketserver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,11 @@ 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
}

func (b *Client) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
return false, []byte{}, fmt.Errorf("not implemented")
}
3 changes: 3 additions & 0 deletions server/events/vcs/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ 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(pull models.PullRequest) (bool, []byte, error)
IsSupportDownloadSingleFile(repo models.Repo) bool
}
30 changes: 29 additions & 1 deletion server/events/vcs/github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ package vcs
import (
"context"
"fmt"
"net/url"
"github.com/runatlantis/atlantis/server/events/yaml"
"net/http"
"net/url"
"strings"
"encoding/base64"

"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs/common"
Expand Down Expand Up @@ -324,3 +327,28 @@ func (g *GithubClient) MergePull(pull models.PullRequest) error {
func (g *GithubClient) MarkdownPullLink(pull models.PullRequest) (string, error) {
return fmt.Sprintf("#%d", pull.Num), nil
}


func (g *GithubClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
opt := github.RepositoryContentGetOptions{Ref: pull.BaseBranch}
fileContent, _, resp, err := g.client.Repositories.GetContents(g.ctx, pull.BaseRepo.Owner, pull.BaseRepo.Name, yaml.AtlantisYAMLFilename, &opt)

if resp.StatusCode == http.StatusNotFound {
return false, []byte{}, nil
}
if err != nil {
return true, []byte{}, err
}

decodedData, err := base64.StdEncoding.DecodeString(*fileContent.Content)
if err != nil {
return true, []byte{}, err
}

return true, decodedData, nil
}

func (g *GithubClient) IsSupportDownloadSingleFile(repo models.Repo) bool {
return true
}

42 changes: 32 additions & 10 deletions server/events/vcs/gitlab_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,21 @@
package vcs

import (
"fmt"
"net"
"net/url"
"strings"
"fmt"
"github.com/runatlantis/atlantis/server/events/yaml"
"net"
"net/http"
"net/url"
"strings"

"github.com/runatlantis/atlantis/server/events/vcs/common"
"github.com/runatlantis/atlantis/server/events/vcs/common"

version "github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/logging"
version "github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/logging"

"github.com/runatlantis/atlantis/server/events/models"
gitlab "github.com/xanzy/go-gitlab"
"github.com/runatlantis/atlantis/server/events/models"
gitlab "github.com/xanzy/go-gitlab"
)

type GitlabClient struct {
Expand Down Expand Up @@ -263,3 +265,23 @@ func MustConstraint(constraint string) version.Constraints {
}
return c
}


func (g *GitlabClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
opt := gitlab.GetRawFileOptions{Ref: gitlab.String(pull.BaseBranch)}

bytes, resp, err := g.Client.RepositoryFiles.GetRawFile(pull.BaseRepo.ID(), yaml.AtlantisYAMLFilename, &opt)
if resp.StatusCode == http.StatusNotFound {
return false, []byte{}, nil
}

if err != nil {
return true, []byte{}, err
}

return true, bytes, nil
}

func (g *GitlabClient) IsSupportDownloadSingleFile(repo models.Repo) bool {
return true
}
8 changes: 8 additions & 0 deletions server/events/vcs/mocks/mock_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions server/events/vcs/not_configured_vcs_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,11 @@ func (a *NotConfiguredVCSClient) MarkdownPullLink(pull models.PullRequest) (stri
func (a *NotConfiguredVCSClient) err() error {
return fmt.Errorf("atlantis was not configured to support repos from %s", a.Host.String())
}

func (a *NotConfiguredVCSClient) IsSupportDownloadSingleFile(repo models.Repo) bool {
return false
}

func (a *NotConfiguredVCSClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
return true, []byte{}, a.err()
}
8 changes: 8 additions & 0 deletions server/events/vcs/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,11 @@ func (d *ClientProxy) MergePull(pull models.PullRequest) error {
func (d *ClientProxy) MarkdownPullLink(pull models.PullRequest) (string, error) {
return d.clients[pull.BaseRepo.VCSHost.Type].MarkdownPullLink(pull)
}

func (d *ClientProxy) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
return d.clients[pull.BaseRepo.VCSHost.Type].DownloadRepoConfigFile(pull)
}

func (d *ClientProxy) IsSupportDownloadSingleFile(repo models.Repo) bool {
return d.clients[repo.VCSHost.Type].IsSupportDownloadSingleFile(repo)
}
Loading

0 comments on commit 1382d49

Please sign in to comment.