Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: set atlantis/apply check to successful if all plans are No Changes #3378

Merged
merged 14 commits into from
Aug 3, 2023
Merged
5 changes: 5 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const (
SilenceAllowlistErrorsFlag = "silence-allowlist-errors"
// SilenceWhitelistErrorsFlag is deprecated for SilenceAllowlistErrorsFlag.
SilenceWhitelistErrorsFlag = "silence-whitelist-errors"
SkipApplyNoChanges = "skip-apply-no-changes"
SkipCloneNoChanges = "skip-clone-no-changes"
SlackTokenFlag = "slack-token"
SSLCertFileFlag = "ssl-cert-file"
Expand Down Expand Up @@ -514,6 +515,10 @@ var boolFlags = map[string]boolFlag{
" This writes secrets to disk and should only be enabled in a secure environment.",
defaultValue: false,
},
SkipApplyNoChanges: {
description: "Skips the apply command if the plan command resutls in 'No Changes'.",
chroju marked this conversation as resolved.
Show resolved Hide resolved
defaultValue: false,
},
SkipCloneNoChanges: {
description: "Skips cloning the PR repo if there are no projects were changed in the PR.",
defaultValue: false,
Expand Down
8 changes: 8 additions & 0 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,14 @@ This is useful when you have many projects and want to keep the pull request cle
```
`--silence-vcs-status-no-plans` will tell Atlantis to ignore setting VCS status if none of the modified files are part of a project defined in the `atlantis.yaml` file.

### `--skip-apply-no-changes`
```bash
atlantis server --skip-apply-no-changes
# or
ATLANTIS_SKIP_APPLY_NO_CHANGES=true
```
`--skip-apply-no-changes` will allow skipping the apply command if the plan command results in "No Changes". This option enables skipping of the apply command by setting the commit status in the VCS to "successful" without actually running the apply command. Defaults to `false`.

### `--skip-clone-no-changes`
```bash
atlantis server --skip-clone-no-changes
Expand Down
2 changes: 2 additions & 0 deletions server/controllers/events/events_controller_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
lockingClient,
discardApprovalOnPlan,
e2ePullReqStatusFetcher,
false,
)

applyCommandRunner := events.NewApplyCommandRunner(
Expand All @@ -1317,6 +1318,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
silenceNoProjects,
false,
e2ePullReqStatusFetcher,
false,
)

approvePoliciesCommandRunner := events.NewApprovePoliciesCommandRunner(
Expand Down
6 changes: 6 additions & 0 deletions server/events/apply_command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func NewApplyCommandRunner(
SilenceNoProjects bool,
silenceVCSStatusNoProjects bool,
pullReqStatusFetcher vcs.PullReqStatusFetcher,
skipApplyNoChanges bool,
) *ApplyCommandRunner {
return &ApplyCommandRunner{
vcsClient: vcsClient,
Expand All @@ -38,6 +39,7 @@ func NewApplyCommandRunner(
SilenceNoProjects: SilenceNoProjects,
silenceVCSStatusNoProjects: silenceVCSStatusNoProjects,
pullReqStatusFetcher: pullReqStatusFetcher,
skipApplyNoChanges: skipApplyNoChanges,
}
}

Expand All @@ -60,6 +62,7 @@ type ApplyCommandRunner struct {
// SilenceVCSStatusNoPlans is whether any plan should set commit status if no projects
// are found
silenceVCSStatusNoProjects bool
skipApplyNoChanges bool
}

func (a *ApplyCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
Expand Down Expand Up @@ -200,6 +203,9 @@ func (a *ApplyCommandRunner) updateCommitStatus(ctx *command.Context, pullStatus
status := models.SuccessCommitStatus

numSuccess = pullStatus.StatusCount(models.AppliedPlanStatus)
if a.skipApplyNoChanges {
numSuccess += pullStatus.StatusCount(models.PlannedNoChangesPlanStatus)
}
nitrocode marked this conversation as resolved.
Show resolved Hide resolved
numErrored = pullStatus.StatusCount(models.ErroredApplyStatus)

if numErrored > 0 {
Expand Down
2 changes: 2 additions & 0 deletions server/events/command/project_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ func (p ProjectResult) PlanStatus() models.ProjectPlanStatus {
return models.ErroredPlanStatus
} else if p.Failure != "" {
return models.ErroredPlanStatus
} else if p.PlanSuccess.NoChanges() {
return models.PlannedNoChangesPlanStatus
}
return models.PlannedPlanStatus
case PolicyCheck, ApprovePolicies:
Expand Down
9 changes: 9 additions & 0 deletions server/events/command/project_result_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ func TestProjectResult_PlanStatus(t *testing.T) {
},
expStatus: models.PlannedPlanStatus,
},
{
p: command.ProjectResult{
Command: command.Plan,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "No changes. Infrastructure is up-to-date.",
},
},
expStatus: models.PlannedNoChangesPlanStatus,
},
{
p: command.ProjectResult{
Command: command.Apply,
Expand Down
157 changes: 147 additions & 10 deletions server/events/command_runner_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import (

func TestApplyUpdateCommitStatus(t *testing.T) {
cases := map[string]struct {
cmd command.Name
pullStatus models.PullStatus
expStatus models.CommitStatus
expNumSuccess int
expNumTotal int
cmd command.Name
skipApplyNoChanges bool
pullStatus models.PullStatus
expStatus models.CommitStatus
expNumSuccess int
expNumTotal int
}{
"apply, one pending": {
cmd: command.Apply,
cmd: command.Apply,
skipApplyNoChanges: false,
pullStatus: models.PullStatus{
Projects: []models.ProjectStatus{
{
Expand All @@ -33,7 +35,8 @@ func TestApplyUpdateCommitStatus(t *testing.T) {
expNumTotal: 2,
},
"apply, all successful": {
cmd: command.Apply,
cmd: command.Apply,
skipApplyNoChanges: false,
pullStatus: models.PullStatus{
Projects: []models.ProjectStatus{
{
Expand All @@ -49,7 +52,8 @@ func TestApplyUpdateCommitStatus(t *testing.T) {
expNumTotal: 2,
},
"apply, one errored, one pending": {
cmd: command.Apply,
cmd: command.Apply,
skipApplyNoChanges: false,
pullStatus: models.PullStatus{
Projects: []models.ProjectStatus{
{
Expand All @@ -67,13 +71,48 @@ func TestApplyUpdateCommitStatus(t *testing.T) {
expNumSuccess: 1,
expNumTotal: 3,
},
"apply, one planned no changes": {
cmd: command.Apply,
skipApplyNoChanges: false,
pullStatus: models.PullStatus{
Projects: []models.ProjectStatus{
{
Status: models.AppliedPlanStatus,
},
{
Status: models.PlannedNoChangesPlanStatus,
},
},
},
expStatus: models.PendingCommitStatus,
expNumSuccess: 1,
expNumTotal: 2,
},
"apply, one planned no changes, skip apply when no changes": {
cmd: command.Apply,
skipApplyNoChanges: true,
pullStatus: models.PullStatus{
Projects: []models.ProjectStatus{
{
Status: models.AppliedPlanStatus,
},
{
Status: models.PlannedNoChangesPlanStatus,
},
},
},
expStatus: models.SuccessCommitStatus,
expNumSuccess: 2,
expNumTotal: 2,
},
}

for name, c := range cases {
t.Run(name, func(t *testing.T) {
csu := &MockCSU{}
cr := &ApplyCommandRunner{
commitStatusUpdater: csu,
skipApplyNoChanges: c.skipApplyNoChanges,
}
cr.updateCommitStatus(&command.Context{}, c.pullStatus)
Equals(t, models.Repo{}, csu.CalledRepo)
Expand All @@ -86,7 +125,7 @@ func TestApplyUpdateCommitStatus(t *testing.T) {
}
}

func TestPlanUpdateCommitStatus(t *testing.T) {
func TestPlanUpdatePlanCommitStatus(t *testing.T) {
cases := map[string]struct {
cmd command.Name
pullStatus models.PullStatus
Expand Down Expand Up @@ -137,7 +176,105 @@ func TestPlanUpdateCommitStatus(t *testing.T) {
cr := &PlanCommandRunner{
commitStatusUpdater: csu,
}
cr.updateCommitStatus(&command.Context{}, c.pullStatus)
cr.updatePlanCommitStatus(&command.Context{}, c.pullStatus)
Equals(t, models.Repo{}, csu.CalledRepo)
Equals(t, models.PullRequest{}, csu.CalledPull)
Equals(t, c.expStatus, csu.CalledStatus)
Equals(t, c.cmd, csu.CalledCommand)
Equals(t, c.expNumSuccess, csu.CalledNumSuccess)
Equals(t, c.expNumTotal, csu.CalledNumTotal)
})
}
}

func TestPlanUpdateApplyCommitStatus(t *testing.T) {
cases := map[string]struct {
cmd command.Name
pullStatus models.PullStatus
expStatus models.CommitStatus
expNumSuccess int
expNumTotal int
}{
"all plans success with no changes": {
cmd: command.Apply,
pullStatus: models.PullStatus{
Projects: []models.ProjectStatus{
{
Status: models.PlannedNoChangesPlanStatus,
},
{
Status: models.PlannedNoChangesPlanStatus,
},
},
},
expStatus: models.SuccessCommitStatus,
expNumSuccess: 2,
expNumTotal: 2,
},
"one plan, one plan success with no changes": {
cmd: command.Apply,
pullStatus: models.PullStatus{
Projects: []models.ProjectStatus{
{
Status: models.PlannedNoChangesPlanStatus,
},
{
Status: models.PlannedPlanStatus,
},
},
},
expStatus: models.PendingCommitStatus,
expNumSuccess: 1,
expNumTotal: 2,
},
"one plan, one apply, one plan success with no changes": {
cmd: command.Apply,
pullStatus: models.PullStatus{
Projects: []models.ProjectStatus{
{
Status: models.PlannedNoChangesPlanStatus,
},
{
Status: models.AppliedPlanStatus,
},
{
Status: models.PlannedPlanStatus,
},
},
},
expStatus: models.PendingCommitStatus,
expNumSuccess: 2,
expNumTotal: 3,
},
"one apply error, one apply, one plan success with no changes": {
cmd: command.Apply,
pullStatus: models.PullStatus{
Projects: []models.ProjectStatus{
{
Status: models.PlannedNoChangesPlanStatus,
},
{
Status: models.AppliedPlanStatus,
},
{
Status: models.ErroredApplyStatus,
},
},
},
expStatus: models.FailedCommitStatus,
expNumSuccess: 2,
expNumTotal: 3,
},
}

for name, c := range cases {
t.Run(name, func(t *testing.T) {
csu := &MockCSU{}
cr := &PlanCommandRunner{
commitStatusUpdater: csu,
skipApplyNoChanges: true,
}
cr.updateApplyCommitStatus(&command.Context{}, c.pullStatus)
Equals(t, models.Repo{}, csu.CalledRepo)
Equals(t, models.PullRequest{}, csu.CalledPull)
Equals(t, c.expStatus, csu.CalledStatus)
Expand Down
2 changes: 2 additions & 0 deletions server/events/command_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
lockingLocker,
testConfig.discardApprovalOnPlan,
pullReqStatusFetcher,
false,
)

applyCommandRunner = events.NewApplyCommandRunner(
Expand All @@ -186,6 +187,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
testConfig.SilenceNoProjects,
testConfig.silenceVCSStatusNoProjects,
pullReqStatusFetcher,
false,
)

approvePoliciesCommandRunner = events.NewApprovePoliciesCommandRunner(
Expand Down
5 changes: 5 additions & 0 deletions server/events/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,9 @@ const (
// PlannedPlanStatus means that a plan has been successfully generated but
// not yet applied.
PlannedPlanStatus
// PlannedNoChangesPlanStatus means that a plan has been successfully
// generated with "No changes" and not yet applied.
PlannedNoChangesPlanStatus
// ErroredApplyStatus means that a plan has been generated but there was an
// error while applying it.
ErroredApplyStatus
Expand All @@ -578,6 +581,8 @@ func (p ProjectPlanStatus) String() string {
return "plan_errored"
case PlannedPlanStatus:
return "planned"
case PlannedNoChangesPlanStatus:
return "planned_no_changes"
case ErroredApplyStatus:
return "apply_errored"
case AppliedPlanStatus:
Expand Down
Loading