mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-09 04:58:34 +00:00
Merge pull request #1158 from runatlantis/skip-clone
Skip cloning PR repository in case of no projects were changed
This commit is contained in:
@@ -81,6 +81,7 @@ const (
|
||||
SilenceAllowlistErrorsFlag = "silence-allowlist-errors"
|
||||
// SilenceWhitelistErrorsFlag is deprecated for SilenceAllowlistErrorsFlag.
|
||||
SilenceWhitelistErrorsFlag = "silence-whitelist-errors"
|
||||
SkipCloneNoChanges = "skip-clone-no-changes"
|
||||
SlackTokenFlag = "slack-token"
|
||||
SSLCertFileFlag = "ssl-cert-file"
|
||||
SSLKeyFileFlag = "ssl-key-file"
|
||||
@@ -319,6 +320,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: {
|
||||
|
||||
@@ -86,6 +86,7 @@ var testFlags = map[string]interface{}{
|
||||
SilenceForkPRErrorsFlag: true,
|
||||
SilenceAllowlistErrorsFlag: true,
|
||||
SilenceVCSStatusNoPlans: true,
|
||||
SkipCloneNoChanges: true,
|
||||
SlackTokenFlag: "slack-token",
|
||||
SSLCertFileFlag: "cert-file",
|
||||
SSLKeyFileFlag: "key-file",
|
||||
|
||||
@@ -3,6 +3,7 @@ package matchers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/petergtz/pegomock"
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package matchers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/petergtz/pegomock"
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package matchers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/petergtz/pegomock"
|
||||
logging "github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
pegomock "github.com/petergtz/pegomock"
|
||||
models "github.com/runatlantis/atlantis/server/events/models"
|
||||
logging "github.com/runatlantis/atlantis/server/logging"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MockWorkingDir struct {
|
||||
|
||||
@@ -53,14 +53,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.
|
||||
@@ -101,8 +102,43 @@ 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.SupportsSingleFileDownload(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.ParseRepoCfgData(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
|
||||
}
|
||||
// NOTE: We discard this work here and end up doing it again after
|
||||
// cloning to ensure all the return values are set properly with
|
||||
// the actual clone directory.
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
@@ -111,18 +147,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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
@@ -887,3 +894,43 @@ projects:
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test that we don't clone the repo if there were no changes based on the atlantis.yaml file.
|
||||
func TestDefaultProjectCommandBuilder_SkipCloneNoChanges(t *testing.T) {
|
||||
atlantisYAML := `
|
||||
version: 3
|
||||
projects:
|
||||
- dir: dir1`
|
||||
|
||||
RegisterMockTestingT(t)
|
||||
vcsClient := vcsmocks.NewMockClient()
|
||||
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"main.tf"}, nil)
|
||||
When(vcsClient.SupportsSingleFileDownload(matchers.AnyModelsRepo())).ThenReturn(true)
|
||||
When(vcsClient.DownloadRepoConfigFile(matchers.AnyModelsPullRequest())).ThenReturn(true, []byte(atlantisYAML), nil)
|
||||
workingDir := mocks.NewMockWorkingDir()
|
||||
|
||||
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),
|
||||
SkipCloneNoChanges: true,
|
||||
}
|
||||
|
||||
var actCtxs []models.ProjectCommandContext
|
||||
var err error
|
||||
actCtxs, err = builder.BuildAutoplanCommands(&events.CommandContext{
|
||||
BaseRepo: models.Repo{},
|
||||
HeadRepo: models.Repo{},
|
||||
Pull: models.PullRequest{},
|
||||
User: models.User{},
|
||||
Log: nil,
|
||||
PullMergeable: true,
|
||||
})
|
||||
Ok(t, err)
|
||||
Equals(t, 0, len(actCtxs))
|
||||
workingDir.VerifyWasCalled(Never()).Clone(matchers.AnyPtrToLoggingSimpleLogger(), matchers.AnyModelsRepo(), matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())
|
||||
}
|
||||
|
||||
@@ -120,11 +120,22 @@ func (p *DefaultProjectFinder) DetermineProjectsViaConfig(log *logging.SimpleLog
|
||||
}
|
||||
if match {
|
||||
log.Debug("file %q matched pattern", file)
|
||||
_, err := os.Stat(filepath.Join(absRepoDir, project.Dir))
|
||||
if err == nil {
|
||||
projects = append(projects, project)
|
||||
// If we're checking using an atlantis.yaml file we downloaded
|
||||
// directly from the repo (when doing a no-clone check) then
|
||||
// absRepoDir will be empty. Since we didn't clone the repo
|
||||
// yet we can't do this check. If there was a file modified
|
||||
// in a deleted directory then when we finally do clone the repo
|
||||
// we'll call this function again and then we'll detect the
|
||||
// directory was deleted.
|
||||
if absRepoDir != "" {
|
||||
_, err := os.Stat(filepath.Join(absRepoDir, project.Dir))
|
||||
if err == nil {
|
||||
projects = append(projects, project)
|
||||
} else {
|
||||
log.Debug("project at dir %q not included because dir does not exist", project.Dir)
|
||||
}
|
||||
} else {
|
||||
log.Debug("project at dir %q not included because dir does not exist", project.Dir)
|
||||
projects = append(projects, project)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -364,3 +364,11 @@ func SplitAzureDevopsRepoFullName(repoFullName string) (owner string, project st
|
||||
}
|
||||
return repoFullName[:lastSlashIdx], "", repoFullName[lastSlashIdx+1:]
|
||||
}
|
||||
|
||||
func (g *AzureDevopsClient) SupportsSingleFileDownload(repo models.Repo) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *AzureDevopsClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
|
||||
return false, []byte{}, fmt.Errorf("Not Implemented")
|
||||
}
|
||||
|
||||
@@ -243,3 +243,14 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b
|
||||
}
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
func (b *Client) SupportsSingleFileDownload(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")
|
||||
}
|
||||
|
||||
@@ -311,3 +311,14 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b
|
||||
}
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
func (b *Client) SupportsSingleFileDownload(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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
SupportsSingleFileDownload(repo models.Repo) bool
|
||||
}
|
||||
|
||||
@@ -15,17 +15,19 @@ package vcs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs/common"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
|
||||
"github.com/Laisky/graphql"
|
||||
"github.com/google/go-github/v31/github"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs/common"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
"github.com/shurcooL/githubv4"
|
||||
)
|
||||
|
||||
@@ -384,3 +386,29 @@ func (g *GithubClient) ExchangeCode(code string) (*GithubAppTemporarySecrets, er
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
// 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) SupportsSingleFileDownload(repo models.Repo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -16,9 +16,12 @@ package vcs
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/yaml"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/vcs/common"
|
||||
|
||||
version "github.com/hashicorp/go-version"
|
||||
@@ -269,3 +272,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.FullName, 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) SupportsSingleFileDownload(repo models.Repo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
20
server/events/vcs/mocks/matchers/slice_of_byte.go
Normal file
20
server/events/vcs/mocks/matchers/slice_of_byte.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
package matchers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"github.com/petergtz/pegomock"
|
||||
|
||||
)
|
||||
|
||||
func AnySliceOfByte() []byte {
|
||||
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*([]byte))(nil)).Elem()))
|
||||
var nullValue []byte
|
||||
return nullValue
|
||||
}
|
||||
|
||||
func EqSliceOfByte(value []byte) []byte {
|
||||
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
|
||||
var nullValue []byte
|
||||
return nullValue
|
||||
}
|
||||
@@ -161,6 +161,44 @@ func (mock *MockClient) MarkdownPullLink(pull models.PullRequest) (string, error
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
func (mock *MockClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockClient().")
|
||||
}
|
||||
params := []pegomock.Param{pull}
|
||||
result := pegomock.GetGenericMockFrom(mock).Invoke("DownloadRepoConfigFile", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*[]byte)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
|
||||
var ret0 bool
|
||||
var ret1 []byte
|
||||
var ret2 error
|
||||
if len(result) != 0 {
|
||||
if result[0] != nil {
|
||||
ret0 = result[0].(bool)
|
||||
}
|
||||
if result[1] != nil {
|
||||
ret1 = result[1].([]byte)
|
||||
}
|
||||
if result[2] != nil {
|
||||
ret2 = result[2].(error)
|
||||
}
|
||||
}
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
func (mock *MockClient) SupportsSingleFileDownload(repo models.Repo) bool {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockClient().")
|
||||
}
|
||||
params := []pegomock.Param{repo}
|
||||
result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsSingleFileDownload", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})
|
||||
var ret0 bool
|
||||
if len(result) != 0 {
|
||||
if result[0] != nil {
|
||||
ret0 = result[0].(bool)
|
||||
}
|
||||
}
|
||||
return ret0
|
||||
}
|
||||
|
||||
func (mock *MockClient) VerifyWasCalledOnce() *VerifierMockClient {
|
||||
return &VerifierMockClient{
|
||||
mock: mock,
|
||||
@@ -461,3 +499,57 @@ func (c *MockClient_MarkdownPullLink_OngoingVerification) GetAllCapturedArgument
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockClient) DownloadRepoConfigFile(pull models.PullRequest) *MockClient_DownloadRepoConfigFile_OngoingVerification {
|
||||
params := []pegomock.Param{pull}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DownloadRepoConfigFile", params, verifier.timeout)
|
||||
return &MockClient_DownloadRepoConfigFile_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockClient_DownloadRepoConfigFile_OngoingVerification struct {
|
||||
mock *MockClient
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockClient_DownloadRepoConfigFile_OngoingVerification) GetCapturedArguments() models.PullRequest {
|
||||
pull := c.GetAllCapturedArguments()
|
||||
return pull[len(pull)-1]
|
||||
}
|
||||
|
||||
func (c *MockClient_DownloadRepoConfigFile_OngoingVerification) GetAllCapturedArguments() (_param0 []models.PullRequest) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]models.PullRequest, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(models.PullRequest)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockClient) SupportsSingleFileDownload(repo models.Repo) *MockClient_SupportsSingleFileDownload_OngoingVerification {
|
||||
params := []pegomock.Param{repo}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsSingleFileDownload", params, verifier.timeout)
|
||||
return &MockClient_SupportsSingleFileDownload_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockClient_SupportsSingleFileDownload_OngoingVerification struct {
|
||||
mock *MockClient
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockClient_SupportsSingleFileDownload_OngoingVerification) GetCapturedArguments() models.Repo {
|
||||
repo := c.GetAllCapturedArguments()
|
||||
return repo[len(repo)-1]
|
||||
}
|
||||
|
||||
func (c *MockClient_SupportsSingleFileDownload_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo) {
|
||||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
|
||||
if len(params) > 0 {
|
||||
_param0 = make([]models.Repo, len(c.methodInvocations))
|
||||
for u, param := range params[0] {
|
||||
_param0[u] = param.(models.Repo)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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) SupportsSingleFileDownload(repo models.Repo) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *NotConfiguredVCSClient) DownloadRepoConfigFile(pull models.PullRequest) (bool, []byte, error) {
|
||||
return true, []byte{}, a.err()
|
||||
}
|
||||
|
||||
@@ -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) SupportsSingleFileDownload(repo models.Repo) bool {
|
||||
return d.clients[repo.VCSHost.Type].SupportsSingleFileDownload(repo)
|
||||
}
|
||||
|
||||
@@ -56,9 +56,12 @@ func (p *ParserValidator) ParseRepoCfg(absRepoDir string, globalCfg valid.Global
|
||||
// able to detect if it's a NotExist err.
|
||||
return valid.RepoCfg{}, err
|
||||
}
|
||||
return p.ParseRepoCfgData(configData, globalCfg, repoID)
|
||||
}
|
||||
|
||||
func (p *ParserValidator) ParseRepoCfgData(repoCfgData []byte, globalCfg valid.GlobalCfg, repoID string) (valid.RepoCfg, error) {
|
||||
var rawConfig raw.RepoCfg
|
||||
if err := yaml.UnmarshalStrict(configData, &rawConfig); err != nil {
|
||||
if err := yaml.UnmarshalStrict(repoCfgData, &rawConfig); err != nil {
|
||||
return valid.RepoCfg{}, err
|
||||
}
|
||||
|
||||
@@ -83,7 +86,7 @@ func (p *ParserValidator) ParseRepoCfg(absRepoDir string, globalCfg valid.Global
|
||||
}
|
||||
}
|
||||
|
||||
err = globalCfg.ValidateRepoCfg(validConfig, repoID)
|
||||
err := globalCfg.ValidateRepoCfg(validConfig, repoID)
|
||||
return validConfig, err
|
||||
}
|
||||
|
||||
|
||||
@@ -476,14 +476,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{},
|
||||
|
||||
@@ -370,14 +370,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,
|
||||
|
||||
@@ -58,6 +58,7 @@ type UserConfig struct {
|
||||
SilenceAllowlistErrors bool `mapstructure:"silence-allowlist-errors"`
|
||||
// SilenceWhitelistErrors is deprecated in favour of SilenceAllowlistErrors
|
||||
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"`
|
||||
|
||||
Reference in New Issue
Block a user