-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Fixes #1006: Keep track of project status even if plans have been deleted #1005
Fixes #1006: Keep track of project status even if plans have been deleted #1005
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1005 +/- ##
==========================================
+ Coverage 71.49% 71.93% +0.43%
==========================================
Files 67 67
Lines 5564 5565 +1
==========================================
+ Hits 3978 4003 +25
+ Misses 1272 1238 -34
- Partials 314 324 +10
Continue to review full report at Codecov.
|
Extra guards have been added to preven automatically merging a branch upstream when automerge is enabled but errors exist or if no commands where executed at all. An example of this bug in action is when you delete a plan and atlantis lock via the UI and then execute atlantis apply No projects are applied but the branch is automerged
462e45e
to
c85155e
Compare
As part of the fix the model is slightly extended to allow for an extra PlanStatus indicating the absence of a plan for a project. This allows us to unlock a project and delete the plan, without having to completely remove the project status. That means that atlantis knows there is a project that should normally have a plan and be accounted for, but the plan has been deleted/unlocked. The reason this is important is so that we can't "trick" atlantis into thinking that the PR can be merged just because we've lost track of a projectstatus as a result of a manual unlock.
Hi 👋 |
@lkysow I believe this deserves a look. This is an important bug IMO. Would we be able to go forward? @parmouraly are there any updates? I don't want code to be merged without being applied. I know my devs. 😂 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, needs tests please.
server/events/models/models.go
Outdated
// error while applying it. | ||
ErroredApplyStatus | ||
// AppliedPlanStatus means that a plan has been generated and applied | ||
// successfully. | ||
AppliedPlanStatus | ||
// NotPlannedPlanStatus means that either no plan has been generated yet or | ||
// a plan has been deleted - eg after using the UI to unlock the project | ||
NotPlannedPlanStatus |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only set when the project is unlocked, not when no plan has been generated yet, so maybe we can call this DiscardedPlanStatus
and update the description to say:
means that there was an unapplied plan that was discarded due to a project being unlocked
@@ -108,8 +108,8 @@ func (l *LocksController) DeleteLock(w http.ResponseWriter, r *http.Request) { | |||
l.Logger.Err("unable to delete workspace: %s", err) | |||
} | |||
} | |||
if err := l.DB.DeleteProjectStatus(lock.Pull, lock.Workspace, lock.Project.Path); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should delete the DeleteProjectStatus
method since it's not used anymore
With BoltDB interface we are able to mock/stub and improve test coverage. Some other refactoring was required to make code aware and compatible with the new interface
server/events/command_runner_test.go
Outdated
//When(boltDB.UpdatePullWithResults(modelPull, nil)).ThenReturn(pullStatus, nil) | ||
When(boltDB.UpdatePullWithResults(dbmatchers.EqModelsPullRequest(modelPull), dbmatchers.EqSliceOfModelsProjectResult(nil))).ThenReturn(pullStatus, nil) | ||
// TODO: stubbing here doesn't seem to work? pullStatus defined here is not actually returned and so I cannot uncomment the next two lines which would verify our scenario here | ||
//vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "not automerging because project at dir %q, workspace %q has status %q") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lkysow this tests the meat of this PR but I've been having some trouble with stubbing. Line 287 (or 286) doesn't seem to have any effect. What am I missing?
This is ready to be merged with the exception of one new testcase I've added in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition to my suggestions is it possible to use a real bolt db instead of the mock? That will reduce a lot of the code changes.
server/events/db/boltdb.go
Outdated
// workspace and repoRelDir. | ||
func (b *BoltDB) DeleteProjectStatus(pull models.PullRequest, workspace string, repoRelDir string) error { | ||
func (b *DefaultBoltDB) UpdateProjectStatus(pull models.PullRequest, workspace string, repoRelDir string, targetStatus models.ProjectPlanStatus) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested rewrite.
// UpdateProjectStatus updates project status.
func (b *DefaultBoltDB) UpdateProjectStatus(pull models.PullRequest, workspace string, repoRelDir string, newStatus models.ProjectPlanStatus) error {
key, err := b.pullKey(pull)
if err != nil {
return err
}
err = b.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(b.pullsBucketName)
currStatusPtr, err := b.getPullFromBucket(bucket, key)
if err != nil {
return err
}
if currStatusPtr == nil {
return nil
}
currStatus := *currStatusPtr
// Update the status.
for i := range currStatus.Projects {
// NOTE: We're using a reference here because we are
// in-place updating its Status field.
proj := &currStatus.Projects[i]
if proj.Workspace == workspace && proj.RepoRelDir == repoRelDir {
proj.Status = newStatus
break
}
}
return b.writePullToBucket(bucket, key, currStatus)
})
return errors.Wrap(err, "DB transaction failed")
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
server/locks_controller_test.go
Outdated
RepoFullName: repoName, | ||
}, | ||
}, nil) | ||
db := dbmocks.NewMockBoltDB() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use a real boltdb instead?
tmp, cleanup := TempDir(t)
defer os.Remove(tmp)
db, err := db.New(tmp)
Ok(t, err)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I don't follow.
The value of this test is to verify that db.UpdateProjectStatus is actually called when someone uses the deleteLock endpoint.
Using mock allows me to verify this (see line 234) in a simple way.
If I use a real DB like you suggest, how can I do this?
Am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented above.
Hey Luke, thanks for your comments. I have replied to all of them, please if you can send me some clarifications so I can push the last commit and we can wrap this up. |
server/events/command_runner_test.go
Outdated
@@ -234,3 +238,20 @@ func TestRunAutoplanCommand_DeletePlans(t *testing.T) { | |||
ch.RunAutoplanCommand(fixtures.GithubRepo, fixtures.GithubRepo, fixtures.Pull, fixtures.User) | |||
pendingPlanFinder.VerifyWasCalledOnce().DeletePlans(tmp) | |||
} | |||
|
|||
func TestApplyWithAutoMerge_VSCMerge(t *testing.T) { | |||
t.Log("if \"atlantis apply\" is run with automerge and at least one project" + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment here is not describing the test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good spot, fixed
@@ -192,6 +194,46 @@ func TestDeleteLock_OldFormat(t *testing.T) { | |||
cp.VerifyWasCalled(Never()).CreateComment(AnyRepo(), AnyInt(), AnyString()) | |||
} | |||
|
|||
func TestDeleteLock_UpdateProjectStatus(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you update to the following the you don't need the db mock:
func TestDeleteLock_UpdateProjectStatus(t *testing.T) {
t.Log("When deleting a lock, pull status has to be updated to reflect discarded plan")
RegisterMockTestingT(t)
repoName := "owner/repo"
projectPath := "path"
workspaceName := "workspace"
cp := vcsmocks.NewMockClient()
l := mocks.NewMockLocker()
workingDir := mocks2.NewMockWorkingDir()
workingDirLocker := events.NewDefaultWorkingDirLocker()
pull := models.PullRequest{
BaseRepo: models.Repo{FullName: repoName},
}
When(l.Unlock("id")).ThenReturn(&models.ProjectLock{
Pull: pull,
Workspace: workspaceName,
Project: models.Project{
Path: projectPath,
RepoFullName: repoName,
},
}, nil)
tmp, cleanup := TempDir(t)
defer cleanup()
db, err := db.New(tmp)
Ok(t, err)
// Seed the DB with a successful plan for that project (that is later discarded).
_, err = db.UpdatePullWithResults(pull, []models.ProjectResult{
{
Command: models.PlanCommand,
RepoRelDir: projectPath,
Workspace: workspaceName,
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "tf-output",
LockURL: "lock-url",
},
},
})
Ok(t, err)
lc := server.LocksController{
Locker: l,
Logger: logging.NewNoopLogger(),
VCSClient: cp,
WorkingDirLocker: workingDirLocker,
WorkingDir: workingDir,
DB: db,
}
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
req = mux.SetURLVars(req, map[string]string{"id": "id"})
w := httptest.NewRecorder()
lc.DeleteLock(w, req)
responseContains(t, w, http.StatusOK, "Deleted lock id \"id\"")
status, err := db.GetPullStatus(pull)
Ok(t, err)
Assert(t, status != nil, "status was nil")
Equals(t, []models.ProjectStatus{
{
Workspace: workspaceName,
RepoRelDir: projectPath,
Status: models.DiscardedPlanStatus,
},
}, status.Projects)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, replaced 👍
@lkysow I think I've address all comments here, is there anything else you want me to look at or can we merge? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! I need to pull the code and test it which I'll do just before including it in the next release.
As part of the fix the model is slightly extended to allow for an extra PlanStatus indicating the absence of a plan for a project.
This allows us to unlock a project and delete the plan, without having to completely remove the project status.
That means that atlantis knows there is a project that should normally have a plan and be accounted for, but the plan has been deleted/unlocked.
The reason this is important is so that we can't "trick" atlantis into thinking that the PR can be merged just because we've lost track of a projectstatus as a result of a manual unlock.
An example of things going wrong is when you delete a plan and atlantis lock via the UI and then execute atlantis apply
No projects are applied as a result but the branch is automatically merged which means you can end up with changed infrastructure code on your master without applying any changes
Issue link: #1006