mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-04 22:58:44 +00:00
feat: set atlantis/apply check to successful if all plans are No Changes (#3378)
* mod: rename updateCommitStatus func * feat: add PlannedNoChangesPlanStatus * Add skipApplyNoChanges option to PlanCommandRunner * Add skipApplyNoChanges option to ApplyCommandRunner * Add --skip-apply-no-changes flag * Fix typo Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> * Rename --skip-apply-no-changes flag * Refactor updateCommitStatus functions * chore(docs): add detailed use case for the flag * test: add plan_command_runner set apply status * feat: set apply status to successful by default when result is 'No Changes' --------- Co-authored-by: chroju <chroju@users.noreply.github.com> Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com>
This commit is contained in:
@@ -199,7 +199,7 @@ func (a *ApplyCommandRunner) updateCommitStatus(ctx *command.Context, pullStatus
|
||||
var numErrored int
|
||||
status := models.SuccessCommitStatus
|
||||
|
||||
numSuccess = pullStatus.StatusCount(models.AppliedPlanStatus)
|
||||
numSuccess = pullStatus.StatusCount(models.AppliedPlanStatus) + pullStatus.StatusCount(models.PlannedNoChangesPlanStatus)
|
||||
numErrored = pullStatus.StatusCount(models.ErroredApplyStatus)
|
||||
|
||||
if numErrored > 0 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -67,6 +67,22 @@ func TestApplyUpdateCommitStatus(t *testing.T) {
|
||||
expNumSuccess: 1,
|
||||
expNumTotal: 3,
|
||||
},
|
||||
"apply, one planned no changes": {
|
||||
cmd: command.Apply,
|
||||
pullStatus: models.PullStatus{
|
||||
Projects: []models.ProjectStatus{
|
||||
{
|
||||
Status: models.AppliedPlanStatus,
|
||||
},
|
||||
{
|
||||
Status: models.PlannedNoChangesPlanStatus,
|
||||
},
|
||||
},
|
||||
},
|
||||
expStatus: models.SuccessCommitStatus,
|
||||
expNumSuccess: 2,
|
||||
expNumTotal: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for name, c := range cases {
|
||||
@@ -86,7 +102,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
|
||||
@@ -137,7 +153,104 @@ func TestPlanUpdateCommitStatus(t *testing.T) {
|
||||
cr := &PlanCommandRunner{
|
||||
commitStatusUpdater: csu,
|
||||
}
|
||||
cr.updateCommitStatus(&command.Context{}, c.pullStatus)
|
||||
cr.updateCommitStatus(&command.Context{}, c.pullStatus, command.Plan)
|
||||
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,
|
||||
}
|
||||
cr.updateCommitStatus(&command.Context{}, c.pullStatus, command.Apply)
|
||||
Equals(t, models.Repo{}, csu.CalledRepo)
|
||||
Equals(t, models.PullRequest{}, csu.CalledPull)
|
||||
Equals(t, c.expStatus, csu.CalledStatus)
|
||||
|
||||
@@ -562,6 +562,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
|
||||
@@ -586,6 +589,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:
|
||||
|
||||
@@ -148,7 +148,8 @@ func (p *PlanCommandRunner) runAutoplan(ctx *command.Context) {
|
||||
ctx.Log.Err("writing results: %s", err)
|
||||
}
|
||||
|
||||
p.updateCommitStatus(ctx, pullStatus)
|
||||
p.updateCommitStatus(ctx, pullStatus, command.Plan)
|
||||
p.updateCommitStatus(ctx, pullStatus, command.Apply)
|
||||
|
||||
// Check if there are any planned projects and if there are any errors or if plans are being deleted
|
||||
if len(policyCheckCmds) > 0 &&
|
||||
@@ -218,7 +219,7 @@ func (p *PlanCommandRunner) run(ctx *command.Context, cmd *CommentCommand) {
|
||||
return
|
||||
}
|
||||
ctx.Log.Debug("resetting VCS status")
|
||||
p.updateCommitStatus(ctx, *pullStatus)
|
||||
p.updateCommitStatus(ctx, *pullStatus, command.Plan)
|
||||
} else {
|
||||
// With a generic plan, we set successful commit statuses
|
||||
// with 0/0 projects planned successfully because some users require
|
||||
@@ -272,7 +273,8 @@ func (p *PlanCommandRunner) run(ctx *command.Context, cmd *CommentCommand) {
|
||||
return
|
||||
}
|
||||
|
||||
p.updateCommitStatus(ctx, pullStatus)
|
||||
p.updateCommitStatus(ctx, pullStatus, command.Plan)
|
||||
p.updateCommitStatus(ctx, pullStatus, command.Apply)
|
||||
|
||||
// Runs policy checks step after all plans are successful.
|
||||
// This step does not approve any policies that require approval.
|
||||
@@ -291,26 +293,39 @@ func (p *PlanCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlanCommandRunner) updateCommitStatus(ctx *command.Context, pullStatus models.PullStatus) {
|
||||
func (p *PlanCommandRunner) updateCommitStatus(ctx *command.Context, pullStatus models.PullStatus, commandName command.Name) {
|
||||
var numSuccess int
|
||||
var numErrored int
|
||||
status := models.SuccessCommitStatus
|
||||
|
||||
numErrored = pullStatus.StatusCount(models.ErroredPlanStatus)
|
||||
// We consider anything that isn't a plan error as a plan success.
|
||||
// For example, if there is an apply error, that means that at least a
|
||||
// plan was generated successfully.
|
||||
numSuccess = len(pullStatus.Projects) - numErrored
|
||||
if commandName == command.Plan {
|
||||
numErrored = pullStatus.StatusCount(models.ErroredPlanStatus)
|
||||
// We consider anything that isn't a plan error as a plan success.
|
||||
// For example, if there is an apply error, that means that at least a
|
||||
// plan was generated successfully.
|
||||
numSuccess = len(pullStatus.Projects) - numErrored
|
||||
|
||||
if numErrored > 0 {
|
||||
status = models.FailedCommitStatus
|
||||
if numErrored > 0 {
|
||||
status = models.FailedCommitStatus
|
||||
}
|
||||
} else if commandName == command.Apply {
|
||||
numSuccess = pullStatus.StatusCount(models.AppliedPlanStatus) + pullStatus.StatusCount(models.PlannedNoChangesPlanStatus)
|
||||
numErrored = pullStatus.StatusCount(models.ErroredApplyStatus)
|
||||
|
||||
if numErrored > 0 {
|
||||
status = models.FailedCommitStatus
|
||||
} else if numSuccess < len(pullStatus.Projects) {
|
||||
// If there are plans that haven't been applied yet, we'll use a pending
|
||||
// status.
|
||||
status = models.PendingCommitStatus
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.commitStatusUpdater.UpdateCombinedCount(
|
||||
ctx.Pull.BaseRepo,
|
||||
ctx.Pull,
|
||||
status,
|
||||
command.Plan,
|
||||
commandName,
|
||||
numSuccess,
|
||||
len(pullStatus.Projects),
|
||||
); err != nil {
|
||||
|
||||
@@ -508,3 +508,256 @@ func TestPlanCommandRunner_ExecutionOrder(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanCommandRunner_AtlantisApplyStatus(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
cases := []struct {
|
||||
Description string
|
||||
ProjectContexts []command.ProjectContext
|
||||
ProjectResults []command.ProjectResult
|
||||
PrevPlanStored bool // stores a previous "No changes" plan in the backend
|
||||
ExpVCSApplyStatusTotal int
|
||||
ExpVCSApplyStatusSucc int
|
||||
}{
|
||||
{
|
||||
Description: "When planning with changes, set the 0/1 apply status",
|
||||
ProjectContexts: []command.ProjectContext{
|
||||
{
|
||||
CommandName: command.Plan,
|
||||
RepoRelDir: "mydir",
|
||||
},
|
||||
},
|
||||
ProjectResults: []command.ProjectResult{
|
||||
{
|
||||
RepoRelDir: "mydir",
|
||||
Command: command.Plan,
|
||||
PlanSuccess: &models.PlanSuccess{
|
||||
TerraformOutput: "Plan: 0 to add, 0 to change, 1 to destroy.",
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpVCSApplyStatusTotal: 1,
|
||||
ExpVCSApplyStatusSucc: 0,
|
||||
},
|
||||
{
|
||||
Description: "When planning with no changes, set the 1/1 apply status",
|
||||
ProjectContexts: []command.ProjectContext{
|
||||
{
|
||||
CommandName: command.Plan,
|
||||
RepoRelDir: "mydir",
|
||||
},
|
||||
},
|
||||
ProjectResults: []command.ProjectResult{
|
||||
{
|
||||
RepoRelDir: "mydir",
|
||||
Command: command.Plan,
|
||||
PlanSuccess: &models.PlanSuccess{
|
||||
TerraformOutput: "No changes. Infrastructure is up-to-date.",
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpVCSApplyStatusTotal: 1,
|
||||
ExpVCSApplyStatusSucc: 1,
|
||||
},
|
||||
{
|
||||
Description: "When planning with no changes and previous plan with no changes, set the 1/2 apply status",
|
||||
ProjectContexts: []command.ProjectContext{
|
||||
{
|
||||
CommandName: command.Plan,
|
||||
RepoRelDir: "mydir",
|
||||
},
|
||||
},
|
||||
ProjectResults: []command.ProjectResult{
|
||||
{
|
||||
RepoRelDir: "mydir",
|
||||
Command: command.Plan,
|
||||
PlanSuccess: &models.PlanSuccess{
|
||||
TerraformOutput: "Plan: 0 to add, 0 to change, 1 to destroy.",
|
||||
},
|
||||
},
|
||||
},
|
||||
PrevPlanStored: true,
|
||||
ExpVCSApplyStatusTotal: 2,
|
||||
ExpVCSApplyStatusSucc: 1,
|
||||
},
|
||||
{
|
||||
Description: "When planning with no changes and previous 'No changes' plan, set the 2/2 apply status",
|
||||
ProjectContexts: []command.ProjectContext{
|
||||
{
|
||||
CommandName: command.Plan,
|
||||
RepoRelDir: "mydir",
|
||||
},
|
||||
},
|
||||
ProjectResults: []command.ProjectResult{
|
||||
{
|
||||
RepoRelDir: "mydir",
|
||||
Command: command.Plan,
|
||||
PlanSuccess: &models.PlanSuccess{
|
||||
TerraformOutput: "No changes. Infrastructure is up-to-date.",
|
||||
},
|
||||
},
|
||||
},
|
||||
PrevPlanStored: true,
|
||||
ExpVCSApplyStatusTotal: 2,
|
||||
ExpVCSApplyStatusSucc: 2,
|
||||
},
|
||||
{
|
||||
Description: "When planning again with changes following a previous 'No changes' plan, set the 0/1 apply status",
|
||||
ProjectContexts: []command.ProjectContext{
|
||||
{
|
||||
CommandName: command.Plan,
|
||||
RepoRelDir: "prevdir",
|
||||
Workspace: "default",
|
||||
},
|
||||
},
|
||||
ProjectResults: []command.ProjectResult{
|
||||
{
|
||||
RepoRelDir: "prevdir",
|
||||
Workspace: "default",
|
||||
Command: command.Plan,
|
||||
PlanSuccess: &models.PlanSuccess{
|
||||
TerraformOutput: "Plan: 0 to add, 0 to change, 1 to destroy.",
|
||||
},
|
||||
},
|
||||
},
|
||||
PrevPlanStored: true,
|
||||
ExpVCSApplyStatusTotal: 1,
|
||||
ExpVCSApplyStatusSucc: 0,
|
||||
},
|
||||
{
|
||||
Description: "When planning again with changes following a previous 'No changes' plan, while another plan with 'No changes', set the 1/2 apply status.",
|
||||
ProjectContexts: []command.ProjectContext{
|
||||
{
|
||||
CommandName: command.Plan,
|
||||
RepoRelDir: "prevdir",
|
||||
Workspace: "default",
|
||||
},
|
||||
{
|
||||
CommandName: command.Plan,
|
||||
RepoRelDir: "mydir",
|
||||
},
|
||||
},
|
||||
ProjectResults: []command.ProjectResult{
|
||||
{
|
||||
RepoRelDir: "prevdir",
|
||||
Workspace: "default",
|
||||
Command: command.Plan,
|
||||
PlanSuccess: &models.PlanSuccess{
|
||||
TerraformOutput: "Plan: 0 to add, 0 to change, 1 to destroy.",
|
||||
},
|
||||
},
|
||||
{
|
||||
RepoRelDir: "mydir",
|
||||
Command: command.Plan,
|
||||
PlanSuccess: &models.PlanSuccess{
|
||||
TerraformOutput: "No changes. Infrastructure is up-to-date.",
|
||||
},
|
||||
},
|
||||
},
|
||||
PrevPlanStored: true,
|
||||
ExpVCSApplyStatusTotal: 2,
|
||||
ExpVCSApplyStatusSucc: 1,
|
||||
},
|
||||
{
|
||||
Description: "When planning again with no changes following a previous 'No changes' plan, while another plan also with 'No changes', set the 2/2 apply status.",
|
||||
ProjectContexts: []command.ProjectContext{
|
||||
{
|
||||
CommandName: command.Plan,
|
||||
RepoRelDir: "prevdir",
|
||||
Workspace: "default",
|
||||
},
|
||||
{
|
||||
CommandName: command.Plan,
|
||||
RepoRelDir: "mydir",
|
||||
},
|
||||
},
|
||||
ProjectResults: []command.ProjectResult{
|
||||
{
|
||||
RepoRelDir: "prevdir",
|
||||
Workspace: "default",
|
||||
Command: command.Plan,
|
||||
PlanSuccess: &models.PlanSuccess{
|
||||
TerraformOutput: "No changes. Infrastructure is up-to-date.",
|
||||
},
|
||||
},
|
||||
{
|
||||
RepoRelDir: "mydir",
|
||||
Command: command.Plan,
|
||||
PlanSuccess: &models.PlanSuccess{
|
||||
TerraformOutput: "No changes. Infrastructure is up-to-date.",
|
||||
},
|
||||
},
|
||||
},
|
||||
PrevPlanStored: true,
|
||||
ExpVCSApplyStatusTotal: 2,
|
||||
ExpVCSApplyStatusSucc: 2,
|
||||
},
|
||||
}
|
||||
|
||||
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.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}
|
||||
|
||||
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{
|
||||
TerraformOutput: "No changes. Your infrastructure matches the configuration.",
|
||||
},
|
||||
},
|
||||
})
|
||||
Ok(t, err)
|
||||
}
|
||||
|
||||
When(projectCommandBuilder.BuildPlanCommands(ctx, cmd)).ThenReturn(c.ProjectContexts, nil)
|
||||
|
||||
for i := range c.ProjectContexts {
|
||||
When(projectCommandRunner.Plan(c.ProjectContexts[i])).ThenReturn(c.ProjectResults[i])
|
||||
}
|
||||
|
||||
planCommandRunner.Run(ctx, cmd)
|
||||
|
||||
vcsClient.VerifyWasCalledOnce().CreateComment(Any[models.Repo](), AnyInt(), AnyString(), AnyString())
|
||||
|
||||
ExpCommitStatus := models.SuccessCommitStatus
|
||||
if c.ExpVCSApplyStatusSucc != c.ExpVCSApplyStatusTotal {
|
||||
ExpCommitStatus = models.PendingCommitStatus
|
||||
}
|
||||
|
||||
commitUpdater.VerifyWasCalledOnce().UpdateCombinedCount(
|
||||
Any[models.Repo](),
|
||||
Any[models.PullRequest](),
|
||||
Eq[models.CommitStatus](ExpCommitStatus),
|
||||
Eq[command.Name](command.Apply),
|
||||
Eq(c.ExpVCSApplyStatusSucc),
|
||||
Eq(c.ExpVCSApplyStatusTotal),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user