feat: --silence-no-projects to comments with -d and -p (#2969)

* Expand --silence-no-projects to targeted commands

* Cover other commands, test+docs

* Add more tests, refactor doc

* fix tests

* fix dangling pending VCS statuses

* fix: codeql yaml

* fix: lint
This commit is contained in:
Adam Zahumenský
2023-01-24 15:26:24 +01:00
committed by GitHub
parent 898bcec40e
commit 953de00f80
14 changed files with 615 additions and 33 deletions

View File

@@ -26,7 +26,6 @@ on:
jobs:
analyze:
if: github.event.pull_request.draft == false
name: Analyze
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest

View File

@@ -873,6 +873,9 @@ and set `--autoplan-modules` to `false`.
ATLANTIS_SILENCE_NO_PROJECTS=true
```
`--silence-no-projects` will tell Atlantis to ignore PRs if none of the modified files are part of a project defined in the `atlantis.yaml` file.
This flag ensures an Atlantis server only responds to its explicitly declared projects.
This has no effect if projects are undefined in the repo level `atlantis.yaml`.
This also silences targeted commands (eg. `atlantis plan -d mydir` or `atlantis apply -p myproj`) so if the project is not in the repo config `atlantis.yaml`, these commands will not run or report back in a comment.
This is useful when running multiple Atlantis servers against a single repository so you can
delegate work to each Atlantis server. Also useful when used with pre_workflow_hooks to dynamically generate an `atlantis.yaml` file.

View File

@@ -1177,6 +1177,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
statsScope,
logger,
terraformClient,
@@ -1327,6 +1328,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
e2ePullReqStatusFetcher,
projectCommandBuilder,
projectCommandRunner,
silenceNoProjects,
)
stateCommandRunner := events.NewStateCommandRunner(

View File

@@ -124,14 +124,34 @@ func (a *ApplyCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
// If there are no projects to apply, don't respond to the PR and ignore
if len(projectCmds) == 0 && a.SilenceNoProjects {
ctx.Log.Info("determined there was no project to run apply in.")
ctx.Log.Info("determined there was no project to run plan in")
if !a.silenceVCSStatusNoProjects {
// If there were no projects modified, we set successful commit statuses
// with 0/0 projects applied successfully because some users require
// the Atlantis status to be passing for all pull requests.
ctx.Log.Debug("setting VCS status to success with no projects found")
if err := a.commitStatusUpdater.UpdateCombinedCount(baseRepo, pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
if cmd.IsForSpecificProject() {
// With a specific apply, just reset the status so it's not stuck in pending state
pullStatus, err := a.Backend.GetPullStatus(pull)
if err != nil {
ctx.Log.Warn("unable to fetch pull status: %s", err)
return
}
if pullStatus == nil {
// default to 0/0
ctx.Log.Debug("setting VCS status to 0/0 success as no previous state was found")
if err := a.commitStatusUpdater.UpdateCombinedCount(baseRepo, pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}
return
}
ctx.Log.Debug("resetting VCS status")
a.updateCommitStatus(ctx, *pullStatus)
} else {
// With a generic apply, we set successful commit statuses
// with 0/0 projects planned successfully because some users require
// the Atlantis status to be passing for all pull requests.
// Does not apply to skipped runs for specific projects
ctx.Log.Debug("setting VCS status to success with no projects found")
if err := a.commitStatusUpdater.UpdateCombinedCount(baseRepo, pull, models.SuccessCommitStatus, command.Apply, 0, 0); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}
}
}
return

View File

@@ -6,13 +6,16 @@ import (
"github.com/google/go-github/v49/github"
. "github.com/petergtz/pegomock"
"github.com/runatlantis/atlantis/server/core/db"
"github.com/runatlantis/atlantis/server/core/locking"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/models/testdata"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
. "github.com/runatlantis/atlantis/testing"
)
func TestApplyCommandRunner_IsLocked(t *testing.T) {
@@ -73,3 +76,140 @@ func TestApplyCommandRunner_IsLocked(t *testing.T) {
})
}
}
func TestApplyCommandRunner_IsSilenced(t *testing.T) {
RegisterMockTestingT(t)
cases := []struct {
Description string
Matched bool
Targeted bool
VCSStatusSilence bool
PrevApplyStored bool // stores a 1/1 passing apply in the backend
ExpVCSStatusSet bool
ExpVCSStatusTotal int
ExpVCSStatusSucc int
ExpSilenced bool
}{
{
Description: "When applying, don't comment but set the 0/0 VCS status",
ExpVCSStatusSet: true,
ExpSilenced: true,
},
{
Description: "When applying with any previous apply's, don't comment but set the 0/0 VCS status",
PrevApplyStored: true,
ExpVCSStatusSet: true,
ExpSilenced: true,
},
{
Description: "When applying with unmatched target, don't comment but set the 0/0 VCS status",
Targeted: true,
ExpVCSStatusSet: true,
ExpSilenced: true,
},
{
Description: "When applying with unmatched target and any previous apply's, don't comment and maintain VCS status",
Targeted: true,
PrevApplyStored: true,
ExpVCSStatusSet: true,
ExpSilenced: true,
ExpVCSStatusSucc: 1,
ExpVCSStatusTotal: 1,
},
{
Description: "When applying with silenced VCS status, don't do anything",
VCSStatusSilence: true,
ExpVCSStatusSet: false,
ExpSilenced: true,
},
{
Description: "When applying with matching projects, comment as usual",
Matched: true,
ExpVCSStatusSet: true,
ExpSilenced: false,
ExpVCSStatusSucc: 1,
ExpVCSStatusTotal: 1,
},
}
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
// create an empty DB
tmp := t.TempDir()
db, err := db.New(tmp)
Ok(t, err)
vcsClient := setup(t, func(tc *TestConfig) {
tc.SilenceNoProjects = true
tc.silenceVCSStatusNoProjects = c.VCSStatusSilence
tc.backend = db
})
scopeNull, _, _ := metrics.NewLoggingScope(logger, "atlantis")
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}
cmd := &events.CommentCommand{Name: command.Apply}
if c.Targeted {
cmd.RepoRelDir = "mydir"
}
ctx := &command.Context{
User: testdata.User,
Log: logging.NewNoopLogger(t),
Scope: scopeNull,
Pull: modelPull,
HeadRepo: testdata.GithubRepo,
Trigger: command.CommentTrigger,
}
if c.PrevApplyStored {
_, err = db.UpdatePullWithResults(modelPull, []command.ProjectResult{
{
Command: command.Apply,
RepoRelDir: "prevdir",
Workspace: "default",
},
})
Ok(t, err)
}
When(projectCommandBuilder.BuildApplyCommands(ctx, cmd)).Then(func(args []Param) ReturnValues {
if c.Matched {
return ReturnValues{[]command.ProjectContext{{
CommandName: command.Apply,
ProjectPlanStatus: models.PlannedPlanStatus,
}}, nil}
}
return ReturnValues{[]command.ProjectContext{}, nil}
})
applyCommandRunner.Run(ctx, cmd)
timesComment := 1
if c.ExpSilenced {
timesComment = 0
}
vcsClient.VerifyWasCalled(Times(timesComment)).CreateComment(AnyRepo(), AnyInt(), AnyString(), AnyString())
if c.ExpVCSStatusSet {
commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
matchers.EqModelsCommitStatus(models.SuccessCommitStatus),
matchers.EqCommandName(command.Apply),
EqInt(c.ExpVCSStatusSucc),
EqInt(c.ExpVCSStatusTotal),
)
} else {
commitUpdater.VerifyWasCalled(Never()).UpdateCombinedCount(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
matchers.AnyModelsCommitStatus(),
matchers.EqCommandName(command.Apply),
AnyInt(),
AnyInt(),
)
}
})
}
}

View File

@@ -16,12 +16,14 @@ package events_test
import (
"errors"
"fmt"
"reflect"
"regexp"
"strings"
"testing"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/db"
"github.com/runatlantis/atlantis/server/core/locking"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
@@ -70,21 +72,35 @@ var importCommandRunner *events.ImportCommandRunner
var preWorkflowHooksCommandRunner events.PreWorkflowHooksCommandRunner
var postWorkflowHooksCommandRunner events.PostWorkflowHooksCommandRunner
func AnyRepo() models.Repo {
RegisterMatcher(NewAnyMatcher(reflect.TypeOf(models.Repo{})))
return models.Repo{}
}
type TestConfig struct {
parallelPoolSize int
SilenceNoProjects bool
StatusName string
discardApprovalOnPlan bool
parallelPoolSize int
SilenceNoProjects bool
silenceVCSStatusNoPlans bool
silenceVCSStatusNoProjects bool
StatusName string
discardApprovalOnPlan bool
backend locking.Backend
}
func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.MockClient {
RegisterMockTestingT(t)
// create an empty DB
tmp := t.TempDir()
defaultBoltDB, err := db.New(tmp)
Ok(t, err)
testConfig := &TestConfig{
parallelPoolSize: 1,
SilenceNoProjects: false,
StatusName: "atlantis-test",
discardApprovalOnPlan: false,
backend: defaultBoltDB,
}
for _, op := range options {
@@ -103,9 +119,6 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
pendingPlanFinder = mocks.NewMockPendingPlanFinder()
commitUpdater = mocks.NewMockCommitStatusUpdater()
pullReqStatusFetcher = vcsmocks.NewMockPullReqStatusFetcher()
tmp := t.TempDir()
defaultBoltDB, err := db.New(tmp)
Ok(t, err)
drainer = &events.Drainer{}
deleteLockCommand = mocks.NewMockDeleteLockCommand()
@@ -113,7 +126,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
lockingLocker = lockingmocks.NewMockLocker()
dbUpdater = &events.DBUpdater{
Backend: defaultBoltDB,
Backend: testConfig.backend,
}
pullUpdater = &events.PullUpdater{
@@ -133,13 +146,13 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
commitUpdater,
projectCommandRunner,
testConfig.parallelPoolSize,
false,
testConfig.silenceVCSStatusNoProjects,
false,
)
planCommandRunner = events.NewPlanCommandRunner(
false,
false,
testConfig.silenceVCSStatusNoPlans,
testConfig.silenceVCSStatusNoProjects,
vcsClient,
pendingPlanFinder,
workingDir,
@@ -152,7 +165,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
autoMerger,
testConfig.parallelPoolSize,
testConfig.SilenceNoProjects,
defaultBoltDB,
testConfig.backend,
lockingLocker,
testConfig.discardApprovalOnPlan,
pullReqStatusFetcher,
@@ -168,10 +181,10 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
autoMerger,
pullUpdater,
dbUpdater,
defaultBoltDB,
testConfig.backend,
testConfig.parallelPoolSize,
testConfig.SilenceNoProjects,
false,
testConfig.silenceVCSStatusNoProjects,
pullReqStatusFetcher,
)
@@ -182,7 +195,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
pullUpdater,
dbUpdater,
testConfig.SilenceNoProjects,
false,
testConfig.silenceVCSStatusNoProjects,
vcsClient,
)
@@ -205,6 +218,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
pullReqStatusFetcher,
projectCommandBuilder,
projectCommandRunner,
testConfig.SilenceNoProjects,
)
commentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{
@@ -242,7 +256,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
Drainer: drainer,
PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner,
PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,
PullStatusFetcher: defaultBoltDB,
PullStatusFetcher: testConfig.backend,
}
return vcsClient
@@ -383,6 +397,28 @@ func TestRunCommentCommandPlan_NoProjects_SilenceEnabled(t *testing.T) {
)
}
func TestRunCommentCommandPlan_NoProjectsTarget_SilenceEnabled(t *testing.T) {
// TODO
t.Log("if a plan command is run against a project and SilenceNoProjects is enabled, we are silencing all comments if the project is not in the repo config")
vcsClient := setup(t)
planCommandRunner.SilenceNoProjects = true
var pull github.PullRequest
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState}
When(githubGetter.GetPullRequest(testdata.GithubRepo, testdata.Pull.Num)).ThenReturn(&pull, nil)
When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)
ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan, ProjectName: "meow"})
vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString())
commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
matchers.EqModelsCommitStatus(models.SuccessCommitStatus),
matchers.EqCommandName(command.Plan),
EqInt(0),
EqInt(0),
)
}
func TestRunCommentCommandApply_NoProjects_SilenceEnabled(t *testing.T) {
t.Log("if an apply command is run on a pull request and SilenceNoProjects is enabled and we are silencing all comments if the modified files don't have a matching project")
vcsClient := setup(t)
@@ -438,6 +474,19 @@ func TestRunCommentCommandUnlock_NoProjects_SilenceEnabled(t *testing.T) {
vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString())
}
func TestRunCommentCommandImport_NoProjects_SilenceEnabled(t *testing.T) {
t.Log("if an import command is run on a pull request and SilenceNoProjects is enabled, we are silencing all comments if the modified files don't have a matching project")
vcsClient := setup(t)
importCommandRunner.SilenceNoProjects = true
var pull github.PullRequest
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState}
When(githubGetter.GetPullRequest(testdata.GithubRepo, testdata.Pull.Num)).ThenReturn(&pull, nil)
When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil)
ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Import})
vcsClient.VerifyWasCalled(Never()).CreateComment(matchers.AnyModelsRepo(), AnyInt(), AnyString(), AnyString())
}
func TestRunCommentCommand_DisableApplyAllDisabled(t *testing.T) {
t.Log("if \"atlantis apply\" is run and this is disabled atlantis should" +
" comment saying that this is not allowed")

View File

@@ -10,12 +10,14 @@ func NewImportCommandRunner(
pullReqStatusFetcher vcs.PullReqStatusFetcher,
prjCmdBuilder ProjectImportCommandBuilder,
prjCmdRunner ProjectImportCommandRunner,
SilenceNoProjects bool,
) *ImportCommandRunner {
return &ImportCommandRunner{
pullUpdater: pullUpdater,
pullReqStatusFetcher: pullReqStatusFetcher,
prjCmdBuilder: prjCmdBuilder,
prjCmdRunner: prjCmdRunner,
SilenceNoProjects: SilenceNoProjects,
}
}
@@ -24,6 +26,7 @@ type ImportCommandRunner struct {
pullReqStatusFetcher vcs.PullReqStatusFetcher
prjCmdBuilder ProjectImportCommandBuilder
prjCmdRunner ProjectImportCommandRunner
SilenceNoProjects bool
}
func (v *ImportCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
@@ -48,6 +51,10 @@ func (v *ImportCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
ctx.Log.Warn("Error %s", err)
}
if len(projectCmds) == 0 && v.SilenceNoProjects {
ctx.Log.Info("determined there was no project to run import in.")
return
}
var result command.Result
if len(projectCmds) > 1 {
// There is no usecase to kick terraform import into multiple projects.

View File

@@ -18,9 +18,11 @@ func TestImportCommandRunner_Run(t *testing.T) {
tests := []struct {
name string
silenced bool
pullReqStatus models.PullReqStatus
projectCmds []command.ProjectContext
expComment string
expNoComment bool
}{
{
name: "success with zero projects",
@@ -40,10 +42,22 @@ func TestImportCommandRunner_Run(t *testing.T) {
projectCmds: []command.ProjectContext{{}, {}},
expComment: "**Import Failed**: import cannot run on multiple projects. please specify one project.",
},
{
name: "no comment with zero projects and silencing",
pullReqStatus: models.PullReqStatus{
ApprovalStatus: models.ApprovalStatus{IsApproved: true},
Mergeable: true,
},
projectCmds: []command.ProjectContext{},
silenced: true,
expNoComment: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
vcsClient := setup(t)
vcsClient := setup(t, func(tc *TestConfig) {
tc.SilenceNoProjects = tt.silenced
})
scopeNull, _, _ := metrics.NewLoggingScope(logger, "atlantis")
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}
@@ -63,7 +77,11 @@ func TestImportCommandRunner_Run(t *testing.T) {
importCommandRunner.Run(ctx, cmd)
Assert(t, ctx.PullRequestStatus.Mergeable == true, "PullRequestStatus must be set for import_requirements")
vcsClient.VerifyWasCalledOnce().CreateComment(testdata.GithubRepo, modelPull.Num, tt.expComment, "import")
if tt.expNoComment {
vcsClient.VerifyWasCalled(Never()).CreateComment(AnyRepo(), AnyInt(), AnyString(), AnyString())
} else {
vcsClient.VerifyWasCalledOnce().CreateComment(testdata.GithubRepo, modelPull.Num, tt.expComment, "import")
}
})
}
}

View File

@@ -202,12 +202,32 @@ func (p *PlanCommandRunner) run(ctx *command.Context, cmd *CommentCommand) {
if len(projectCmds) == 0 && p.SilenceNoProjects {
ctx.Log.Info("determined there was no project to run plan in")
if !p.silenceVCSStatusNoProjects {
// If there were no projects modified, we set successful commit statuses
// with 0/0 projects planned successfully because some users require
// the Atlantis status to be passing for all pull requests.
ctx.Log.Debug("setting VCS status to success with no projects found")
if err := p.commitStatusUpdater.UpdateCombinedCount(baseRepo, pull, models.SuccessCommitStatus, command.Plan, 0, 0); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
if cmd.IsForSpecificProject() {
// With a specific plan, just reset the status so it's not stuck in pending state
pullStatus, err := p.pullStatusFetcher.GetPullStatus(pull)
if err != nil {
ctx.Log.Warn("unable to fetch pull status: %s", err)
return
}
if pullStatus == nil {
// default to 0/0
ctx.Log.Debug("setting VCS status to 0/0 success as no previous state was found")
if err := p.commitStatusUpdater.UpdateCombinedCount(baseRepo, pull, models.SuccessCommitStatus, command.Plan, 0, 0); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}
return
}
ctx.Log.Debug("resetting VCS status")
p.updateCommitStatus(ctx, *pullStatus)
} else {
// With a generic plan, we set successful commit statuses
// with 0/0 projects planned successfully because some users require
// the Atlantis status to be passing for all pull requests.
// Does not apply to skipped runs for specific projects
ctx.Log.Debug("setting VCS status to success with no projects found")
if err := p.commitStatusUpdater.UpdateCombinedCount(baseRepo, pull, models.SuccessCommitStatus, command.Plan, 0, 0); err != nil {
ctx.Log.Warn("unable to update commit status: %s", err)
}
}
}
return

View File

@@ -0,0 +1,151 @@
package events_test
import (
"testing"
. "github.com/petergtz/pegomock"
"github.com/runatlantis/atlantis/server/core/db"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/models/testdata"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
. "github.com/runatlantis/atlantis/testing"
)
func TestPlanCommandRunner_IsSilenced(t *testing.T) {
RegisterMockTestingT(t)
cases := []struct {
Description string
Matched bool
Targeted bool
VCSStatusSilence bool
PrevPlanStored bool // stores a 1/1 passing plan in the backend
ExpVCSStatusSet bool
ExpVCSStatusTotal int
ExpVCSStatusSucc int
ExpSilenced bool
}{
{
Description: "When planning, don't comment but set the 0/0 VCS status",
ExpVCSStatusSet: true,
ExpSilenced: true,
},
{
Description: "When planning with any previous plans, don't comment but set the 0/0 VCS status",
PrevPlanStored: true,
ExpVCSStatusSet: true,
ExpSilenced: true,
},
{
Description: "When planning with unmatched target, don't comment but set the 0/0 VCS status",
Targeted: true,
ExpVCSStatusSet: true,
ExpSilenced: true,
},
{
Description: "When planning with unmatched target and any previous plans, don't comment and maintain VCS status",
Targeted: true,
PrevPlanStored: true,
ExpVCSStatusSet: true,
ExpSilenced: true,
ExpVCSStatusSucc: 1,
ExpVCSStatusTotal: 1,
},
{
Description: "When planning with silenced VCS status, don't do anything",
VCSStatusSilence: true,
ExpVCSStatusSet: false,
ExpSilenced: true,
},
{
Description: "When planning with matching projects, comment as usual",
Matched: true,
ExpVCSStatusSet: true,
ExpSilenced: false,
ExpVCSStatusSucc: 1,
ExpVCSStatusTotal: 1,
},
}
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
// create an empty DB
tmp := t.TempDir()
db, err := db.New(tmp)
Ok(t, err)
vcsClient := setup(t, func(tc *TestConfig) {
tc.SilenceNoProjects = true
tc.silenceVCSStatusNoProjects = c.VCSStatusSilence
tc.backend = db
})
scopeNull, _, _ := metrics.NewLoggingScope(logger, "atlantis")
modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num}
cmd := &events.CommentCommand{Name: command.Plan}
if c.Targeted {
cmd.RepoRelDir = "mydir"
}
ctx := &command.Context{
User: testdata.User,
Log: logging.NewNoopLogger(t),
Scope: scopeNull,
Pull: modelPull,
HeadRepo: testdata.GithubRepo,
Trigger: command.CommentTrigger,
}
if c.PrevPlanStored {
_, err = db.UpdatePullWithResults(modelPull, []command.ProjectResult{
{
Command: command.Plan,
RepoRelDir: "prevdir",
Workspace: "default",
PlanSuccess: &models.PlanSuccess{},
},
})
Ok(t, err)
}
When(projectCommandBuilder.BuildPlanCommands(ctx, cmd)).Then(func(args []Param) ReturnValues {
if c.Matched {
return ReturnValues{[]command.ProjectContext{{CommandName: command.Plan}}, nil}
}
return ReturnValues{[]command.ProjectContext{}, nil}
})
planCommandRunner.Run(ctx, cmd)
timesComment := 1
if c.ExpSilenced {
timesComment = 0
}
vcsClient.VerifyWasCalled(Times(timesComment)).CreateComment(AnyRepo(), AnyInt(), AnyString(), AnyString())
if c.ExpVCSStatusSet {
commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
matchers.EqModelsCommitStatus(models.SuccessCommitStatus),
matchers.EqCommandName(command.Plan),
EqInt(c.ExpVCSStatusSucc),
EqInt(c.ExpVCSStatusTotal),
)
} else {
commitUpdater.VerifyWasCalled(Never()).UpdateCombinedCount(
matchers.AnyModelsRepo(),
matchers.AnyModelsPullRequest(),
matchers.AnyModelsCommitStatus(),
matchers.EqCommandName(command.Plan),
AnyInt(),
AnyInt(),
)
}
})
}
}

View File

@@ -53,6 +53,7 @@ func NewInstrumentedProjectCommandBuilder(
AutoDetectModuleFiles string,
AutoplanFileList string,
RestrictFileList bool,
SilenceNoProjects bool,
scope tally.Scope,
logger logging.SimpleLogging,
terraformClient terraform.Client,
@@ -79,6 +80,7 @@ func NewInstrumentedProjectCommandBuilder(
AutoDetectModuleFiles,
AutoplanFileList,
RestrictFileList,
SilenceNoProjects,
scope,
logger,
terraformClient,
@@ -103,6 +105,7 @@ func NewProjectCommandBuilder(
AutoDetectModuleFiles string,
AutoplanFileList string,
RestrictFileList bool,
SilenceNoProjects bool,
scope tally.Scope,
logger logging.SimpleLogging,
terraformClient terraform.Client,
@@ -120,6 +123,7 @@ func NewProjectCommandBuilder(
AutoDetectModuleFiles: AutoDetectModuleFiles,
AutoplanFileList: AutoplanFileList,
RestrictFileList: RestrictFileList,
SilenceNoProjects: SilenceNoProjects,
ProjectCommandContextBuilder: NewProjectCommandContextBuilder(
policyChecksSupported,
commentBuilder,
@@ -202,6 +206,7 @@ type DefaultProjectCommandBuilder struct {
AutoplanFileList string
EnableDiffMarkdownFormat bool
RestrictFileList bool
SilenceNoProjects bool
TerraformExecutor terraform.Client
}
@@ -561,7 +566,11 @@ func (p *DefaultProjectCommandBuilder) getCfg(ctx *command.Context, projectName
}
}
if len(projectsCfg) == 0 {
err = fmt.Errorf("no project with name %q is defined in %s", projectName, repoCfgFile)
if p.SilenceNoProjects && len(repoConfig.Projects) > 0 {
ctx.Log.Debug("no project with name %q found but silencing the error", projectName)
} else {
err = fmt.Errorf("no project with name %q is defined in %s", projectName, repoCfgFile)
}
return
}
return
@@ -718,6 +727,12 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Conte
)...)
}
} else {
// Ignore the project if silenced with projects set in the repo config
if p.SilenceNoProjects && repoCfgPtr != nil && len(repoCfgPtr.Projects) > 0 {
ctx.Log.Debug("silencing is in effect, project will be ignored")
return []command.ProjectContext{}, nil
}
projCfg = p.GlobalCfg.DefaultProjCfg(ctx.Log, ctx.Pull.BaseRepo.ID(), repoRelDir, workspace)
projCtxs = append(projCtxs,
p.ProjectCommandContextBuilder.BuildProjectContext(

View File

@@ -662,6 +662,7 @@ projects:
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
statsScope,
logger,
terraformClient,
@@ -871,6 +872,7 @@ projects:
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
statsScope,
logger,
terraformClient,
@@ -1111,6 +1113,7 @@ workflows:
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
statsScope,
logger,
terraformClient,
@@ -1168,6 +1171,126 @@ workflows:
}
}
func TestBuildProjectCmdCtx_WithSilenceNoProjects(t *testing.T) {
globalCfg := `
repos:
- id: /.*/
`
logger := logging.NewNoopLogger(t)
baseRepo := models.Repo{
FullName: "owner/repo",
VCSHost: models.VCSHost{
Hostname: "github.com",
},
}
cases := map[string]struct {
repoCfg string
expLen int
}{
// One project matches the repo cfg, return it
"matching project": {
repoCfg: `
version: 3
automerge: true
projects:
- dir: project1
workspace: myworkspace
`,
expLen: 1,
},
// No project matches the repo cfg, ignore it
"no matching project": {
repoCfg: `
version: 3
automerge: true
projects:
- dir: project2
workspace: myworkspace
`,
expLen: 0,
},
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
tmp := DirStructure(t, map[string]interface{}{
"project1": map[string]interface{}{
"main.tf": nil,
},
"modules": map[string]interface{}{
"module": map[string]interface{}{
"main.tf": nil,
},
},
})
workingDir := NewMockWorkingDir()
When(workingDir.Clone(matchers.AnyLoggingSimpleLogging(), matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest(), AnyString())).ThenReturn(tmp, false, nil)
vcsClient := vcsmocks.NewMockClient()
When(vcsClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"modules/module/main.tf"}, nil)
// Write and parse the global config file.
globalCfgPath := filepath.Join(tmp, "global.yaml")
Ok(t, os.WriteFile(globalCfgPath, []byte(globalCfg), 0600))
parser := &config.ParserValidator{}
globalCfgArgs := valid.GlobalCfgArgs{
AllowRepoCfg: false,
MergeableReq: false,
ApprovedReq: false,
UnDivergedReq: false,
}
globalCfg, err := parser.ParseGlobalCfg(globalCfgPath, valid.NewGlobalCfgFromArgs(globalCfgArgs))
Ok(t, err)
if c.repoCfg != "" {
Ok(t, os.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600))
}
statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis")
terraformClient := mocks.NewMockClient()
builder := NewProjectCommandBuilder(
false,
parser,
&DefaultProjectFinder{},
vcsClient,
workingDir,
NewDefaultWorkingDirLocker(),
globalCfg,
&DefaultPendingPlanFinder{},
&CommentParser{ExecutableName: "atlantis"},
false,
false,
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
true,
statsScope,
logger,
terraformClient,
)
for _, cmd := range []command.Name{command.Plan, command.Apply} {
t.Run(cmd.String(), func(t *testing.T) {
ctxs, err := builder.buildProjectCommandCtx(&command.Context{
Log: logger,
Scope: statsScope,
Pull: models.PullRequest{
BaseRepo: baseRepo,
},
PullRequestStatus: models.PullReqStatus{
Mergeable: true,
},
}, cmd, "", "", []string{}, tmp, "project1", "myworkspace", true)
Equals(t, c.expLen, len(ctxs))
Ok(t, err)
})
}
})
}
}
func mustVersion(v string) *version.Version {
vers, err := version.NewVersion(v)
if err != nil {

View File

@@ -165,6 +165,7 @@ projects:
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
scope,
logger,
terraformClient,
@@ -195,6 +196,7 @@ func TestDefaultProjectCommandBuilder_BuildSinglePlanApplyCommand(t *testing.T)
Description string
AtlantisYAML string
Cmd events.CommentCommand
Silenced bool
ExpCommentArgs []string
ExpWorkspace string
ExpDir string
@@ -203,6 +205,7 @@ func TestDefaultProjectCommandBuilder_BuildSinglePlanApplyCommand(t *testing.T)
ExpApplyReqs []string
ExpParallelApply bool
ExpParallelPlan bool
ExpNoProjects bool
}{
{
Description: "no atlantis.yaml",
@@ -366,6 +369,22 @@ projects:
`,
ExpErr: "no project with name \"notconfigured\" is defined in atlantis.yaml",
},
{
Description: "atlantis.yaml with project flag not matching but silenced",
Cmd: events.CommentCommand{
Name: command.Plan,
RepoRelDir: ".",
Workspace: "default",
ProjectName: "notconfigured",
},
AtlantisYAML: `
version: 3
projects:
- dir: .
`,
Silenced: true,
ExpNoProjects: true,
},
{
Description: "atlantis.yaml with ParallelPlan Set to true",
Cmd: events.CommentCommand{
@@ -438,6 +457,7 @@ projects:
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
c.Silenced,
scope,
logger,
terraformClient,
@@ -459,6 +479,10 @@ projects:
return
}
Ok(t, err)
if c.ExpNoProjects {
Equals(t, 0, len(actCtxs))
return
}
Equals(t, 1, len(actCtxs))
actCtx := actCtxs[0]
Equals(t, c.ExpDir, actCtx.RepoRelDir)
@@ -615,6 +639,7 @@ projects:
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
true,
false,
scope,
logger,
terraformClient,
@@ -800,6 +825,7 @@ projects:
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
scope,
logger,
terraformClient,
@@ -898,6 +924,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
scope,
logger,
terraformClient,
@@ -987,6 +1014,7 @@ projects:
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
scope,
logger,
terraformClient,
@@ -1071,6 +1099,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
scope,
logger,
terraformClient,
@@ -1237,6 +1266,7 @@ projects:
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
scope,
logger,
terraformClient,
@@ -1330,6 +1360,7 @@ parallel_plan: true`,
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
scope,
logger,
terraformClient,
@@ -1393,6 +1424,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
scope,
logger,
terraformClient,
@@ -1479,6 +1511,7 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) {
"",
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
false,
false,
scope,
logger,
terraformClient,

View File

@@ -567,6 +567,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
userConfig.AutoplanModulesFromProjects,
userConfig.AutoplanFileList,
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
statsScope,
logger,
terraformClient,
@@ -730,6 +731,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
pullReqStatusFetcher,
projectCommandBuilder,
instrumentedProjectCmdRunner,
userConfig.SilenceNoProjects,
)
stateCommandRunner := events.NewStateCommandRunner(