diff --git a/cmd/server.go b/cmd/server.go index b0405768f3..a809326d25 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -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" @@ -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: { diff --git a/cmd/server_test.go b/cmd/server_test.go index 7a197b1e77..72da9abc67 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -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", diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index f88085da43..c740108aa8 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -48,14 +48,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. @@ -96,8 +97,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") @@ -106,18 +140,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 { @@ -128,7 +154,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) } @@ -306,7 +332,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 } diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go index eb020ec3a6..b57e677d39 100644 --- a/server/events/project_command_builder_internal_test.go +++ b/server/events/project_command_builder_internal_test.go @@ -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. diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index e87ce20ad7..8d956628bd 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -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{ @@ -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 @@ -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( @@ -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( @@ -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{ @@ -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 @@ -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( diff --git a/server/events/vcs/azuredevops_client.go b/server/events/vcs/azuredevops_client.go index 1e75f8f9c7..1c98782ab7 100644 --- a/server/events/vcs/azuredevops_client.go +++ b/server/events/vcs/azuredevops_client.go @@ -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") +} diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index b8bcb8d16a..84c4c85f5f 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -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") +} diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go index 10653439e4..c323c48c35 100644 --- a/server/events/vcs/bitbucketserver/client.go +++ b/server/events/vcs/bitbucketserver/client.go @@ -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") +} diff --git a/server/events/vcs/client.go b/server/events/vcs/client.go index 6cd35b39a4..15bef550a7 100644 --- a/server/events/vcs/client.go +++ b/server/events/vcs/client.go @@ -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 } diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go index ce0370b0f4..2e6d1059d8 100644 --- a/server/events/vcs/github_client.go +++ b/server/events/vcs/github_client.go @@ -15,7 +15,10 @@ package vcs import ( "context" + "encoding/base64" "fmt" + "github.com/runatlantis/atlantis/server/events/yaml" + "net/http" "net/url" "strings" @@ -324,3 +327,29 @@ func (g *GithubClient) MergePull(pull models.PullRequest) error { func (g *GithubClient) MarkdownPullLink(pull models.PullRequest) (string, error) { return fmt.Sprintf("#%d", pull.Num), nil } + +// 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 (g *GithubClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + opt := github.RepositoryContentGetOptions{Ref: pull.HeadBranch} + 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 +} diff --git a/server/events/vcs/gitlab_client.go b/server/events/vcs/gitlab_client.go index b29388813c..d657157605 100644 --- a/server/events/vcs/gitlab_client.go +++ b/server/events/vcs/gitlab_client.go @@ -15,7 +15,9 @@ package vcs import ( "fmt" + "github.com/runatlantis/atlantis/server/events/yaml" "net" + "net/http" "net/url" "strings" @@ -263,3 +265,25 @@ func MustConstraint(constraint string) version.Constraints { } return c } + +// 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 (g *GitlabClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + opt := gitlab.GetRawFileOptions{Ref: gitlab.String(pull.HeadBranch)} + + 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 +} diff --git a/server/events/vcs/mocks/mock_client.go b/server/events/vcs/mocks/mock_client.go index 62e022f1f3..7c94bb840e 100644 --- a/server/events/vcs/mocks/mock_client.go +++ b/server/events/vcs/mocks/mock_client.go @@ -161,6 +161,14 @@ func (mock *MockClient) MarkdownPullLink(pull models.PullRequest) (string, error return ret0, ret1 } +func (mock *MockClient) IsSupportDownloadSingleFile(repo models.Repo) bool { + return false +} + +func (mock *MockClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) { + return false, []byte{}, nil +} + func (mock *MockClient) VerifyWasCalledOnce() *VerifierMockClient { return &VerifierMockClient{ mock: mock, diff --git a/server/events/vcs/not_configured_vcs_client.go b/server/events/vcs/not_configured_vcs_client.go index 6a5b3d747b..40223904b8 100644 --- a/server/events/vcs/not_configured_vcs_client.go +++ b/server/events/vcs/not_configured_vcs_client.go @@ -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() +} diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index ed25c4d60d..1441724fe0 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -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) +} diff --git a/server/events/yaml/parser_validator.go b/server/events/yaml/parser_validator.go index 0e2bd84a24..3bbbfda8cd 100644 --- a/server/events/yaml/parser_validator.go +++ b/server/events/yaml/parser_validator.go @@ -44,17 +44,24 @@ func (p *ParserValidator) HasRepoCfg(absRepoDir string) (bool, error) { // ParseRepoCfg returns the parsed and validated atlantis.yaml config for the // repo at absRepoDir. // If there was no config file, it will return an os.IsNotExist(error). -func (p *ParserValidator) ParseRepoCfg(absRepoDir string, globalCfg valid.GlobalCfg, repoID string) (valid.RepoCfg, error) { - configFile := p.repoCfgPath(absRepoDir, AtlantisYAMLFilename) - configData, err := ioutil.ReadFile(configFile) // nolint: gosec - - if err != nil { - if !os.IsNotExist(err) { - return valid.RepoCfg{}, errors.Wrapf(err, "unable to read %s file", AtlantisYAMLFilename) +func (p *ParserValidator) ParseRepoCfg(repoCfgData []byte, absRepoDir string, globalCfg valid.GlobalCfg, repoID string) (valid.RepoCfg, error) { + var configData []byte + var err error + + if len(repoCfgData) > 0 { + configData = repoCfgData + } else { + configFile := p.repoCfgPath(absRepoDir, AtlantisYAMLFilename) + configData, err = ioutil.ReadFile(configFile) // nolint: gosec + + if err != nil { + if !os.IsNotExist(err) { + return valid.RepoCfg{}, errors.Wrapf(err, "unable to read %s file", AtlantisYAMLFilename) + } + // Don't wrap os.IsNotExist errors because we want our callers to be + // able to detect if it's a NotExist err. + return valid.RepoCfg{}, err } - // Don't wrap os.IsNotExist errors because we want our callers to be - // able to detect if it's a NotExist err. - return valid.RepoCfg{}, err } var rawConfig raw.RepoCfg diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go index fd687317b8..214ea22790 100644 --- a/server/events/yaml/parser_validator_test.go +++ b/server/events/yaml/parser_validator_test.go @@ -46,7 +46,7 @@ func TestHasRepoCfg_InvalidFileExtension(t *testing.T) { func TestParseRepoCfg_DirDoesNotExist(t *testing.T) { r := yaml.ParserValidator{} - _, err := r.ParseRepoCfg("/not/exist", globalCfg, "") + _, err := r.ParseRepoCfg([]byte{}, "/not/exist", globalCfg, "") Assert(t, os.IsNotExist(err), "exp not exist err") } @@ -54,7 +54,7 @@ func TestParseRepoCfg_FileDoesNotExist(t *testing.T) { tmpDir, cleanup := TempDir(t) defer cleanup() r := yaml.ParserValidator{} - _, err := r.ParseRepoCfg(tmpDir, globalCfg, "") + _, err := r.ParseRepoCfg([]byte{}, tmpDir, globalCfg, "") Assert(t, os.IsNotExist(err), "exp not exist err") } @@ -65,7 +65,7 @@ func TestParseRepoCfg_BadPermissions(t *testing.T) { Ok(t, err) r := yaml.ParserValidator{} - _, err = r.ParseRepoCfg(tmpDir, globalCfg, "") + _, err = r.ParseRepoCfg([]byte{}, tmpDir, globalCfg, "") ErrContains(t, "unable to read atlantis.yaml file: ", err) } @@ -99,7 +99,7 @@ func TestParseCfgs_InvalidYAML(t *testing.T) { err := ioutil.WriteFile(confPath, []byte(c.input), 0600) Ok(t, err) r := yaml.ParserValidator{} - _, err = r.ParseRepoCfg(tmpDir, globalCfg, "") + _, err = r.ParseRepoCfg([]byte{}, tmpDir, globalCfg, "") ErrContains(t, c.expErr, err) _, err = r.ParseGlobalCfg(confPath, valid.NewGlobalCfg(false, false, false)) ErrContains(t, c.expErr, err) @@ -845,7 +845,7 @@ workflows: Ok(t, err) r := yaml.ParserValidator{} - act, err := r.ParseRepoCfg(tmpDir, globalCfg, "") + act, err := r.ParseRepoCfg([]byte{}, tmpDir, globalCfg, "") if c.expErr != "" { ErrEquals(t, c.expErr, err) return @@ -873,7 +873,7 @@ workflows: Ok(t, err) r := yaml.ParserValidator{} - _, err = r.ParseRepoCfg(tmpDir, valid.NewGlobalCfg(false, false, false), "repo_id") + _, err = r.ParseRepoCfg([]byte{}, tmpDir, valid.NewGlobalCfg(false, false, false), "repo_id") ErrEquals(t, "repo config not allowed to set 'workflow' key: server-side config needs 'allowed_overrides: [workflow]'", err) } @@ -1337,7 +1337,7 @@ func TestParseRepoCfg_V2ShellParsing(t *testing.T) { Ok(t, ioutil.WriteFile(v3Path, []byte("version: 3\n"+cfg), 0600)) p := &yaml.ParserValidator{} - v2Cfg, err := p.ParseRepoCfg(v2Dir, valid.NewGlobalCfg(true, false, false), "") + v2Cfg, err := p.ParseRepoCfg([]byte{}, v2Dir, valid.NewGlobalCfg(true, false, false), "") if c.expV2Err != "" { ErrEquals(t, c.expV2Err, err) } else { @@ -1346,7 +1346,7 @@ func TestParseRepoCfg_V2ShellParsing(t *testing.T) { Equals(t, c.expV2, v2Cfg.Workflows["custom"].Apply.Steps[0].RunCommand) } - v3Cfg, err := p.ParseRepoCfg(v3Dir, valid.NewGlobalCfg(true, false, false), "") + v3Cfg, err := p.ParseRepoCfg([]byte{}, v3Dir, valid.NewGlobalCfg(true, false, false), "") Ok(t, err) Equals(t, c.in, v3Cfg.Workflows["custom"].Plan.Steps[0].RunCommand) Equals(t, c.in, v3Cfg.Workflows["custom"].Apply.Steps[0].RunCommand) diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go index be4fda14d8..ab916ab303 100644 --- a/server/events_controller_e2e_test.go +++ b/server/events_controller_e2e_test.go @@ -457,14 +457,15 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks. AllowForkPRs: allowForkPRs, AllowForkPRsFlag: "allow-fork-prs", ProjectCommandBuilder: &events.DefaultProjectCommandBuilder{ - ParserValidator: parser, - ProjectFinder: &events.DefaultProjectFinder{}, - VCSClient: e2eVCSClient, - WorkingDir: workingDir, - WorkingDirLocker: locker, - PendingPlanFinder: &events.DefaultPendingPlanFinder{}, - CommentBuilder: commentParser, - GlobalCfg: globalCfg, + ParserValidator: parser, + ProjectFinder: &events.DefaultProjectFinder{}, + VCSClient: e2eVCSClient, + WorkingDir: workingDir, + WorkingDirLocker: locker, + PendingPlanFinder: &events.DefaultPendingPlanFinder{}, + CommentBuilder: commentParser, + GlobalCfg: globalCfg, + SkipCloneNoChanges: false, }, DB: boltdb, PendingPlanFinder: &events.DefaultPendingPlanFinder{}, diff --git a/server/server.go b/server/server.go index 831bf44622..9149ff3816 100644 --- a/server/server.go +++ b/server/server.go @@ -323,14 +323,15 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { SilenceVCSStatusNoPlans: userConfig.SilenceVCSStatusNoPlans, DisableApplyAll: userConfig.DisableApplyAll, ProjectCommandBuilder: &events.DefaultProjectCommandBuilder{ - ParserValidator: validator, - ProjectFinder: &events.DefaultProjectFinder{}, - VCSClient: vcsClient, - WorkingDir: workingDir, - WorkingDirLocker: workingDirLocker, - GlobalCfg: globalCfg, - PendingPlanFinder: pendingPlanFinder, - CommentBuilder: commentParser, + ParserValidator: validator, + ProjectFinder: &events.DefaultProjectFinder{}, + VCSClient: vcsClient, + WorkingDir: workingDir, + WorkingDirLocker: workingDirLocker, + GlobalCfg: globalCfg, + PendingPlanFinder: pendingPlanFinder, + CommentBuilder: commentParser, + SkipCloneNoChanges: userConfig.SkipCloneNoChanges, }, ProjectCommandRunner: &events.DefaultProjectCommandRunner{ Locker: projectLocker, diff --git a/server/user_config.go b/server/user_config.go index 2524636707..94715ab2f1 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -49,6 +49,7 @@ type UserConfig struct { // are found. SilenceVCSStatusNoPlans bool `mapstructure:"silence-vcs-status-no-plans"` SilenceWhitelistErrors bool `mapstructure:"silence-whitelist-errors"` + SkipCloneNoChanges bool `mapstructure:"skip-clone-no-changes"` SlackToken string `mapstructure:"slack-token"` SSLCertFile string `mapstructure:"ssl-cert-file"` SSLKeyFile string `mapstructure:"ssl-key-file"`