Files
atlantis/server/core/db/boltdb_test.go
Gabriel Martinez 56e38b4151 chore(atlantis): fix linter errors (#3690)
* chore(atlantis): fix linter errors

* fix superfluous-else

* revert gitlab check

* ignore stub funcs on azuredevops_client

* remove fetch-depth

* remove fail_on_error

* fix plan_step_runner_test.go

* more lint fixes

---------

Co-authored-by: Dylan Page <dylan.page@autodesk.com>
2023-12-11 19:02:16 +00:00

919 lines
22 KiB
Go

// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
package db_test
import (
"os"
"testing"
"time"
"github.com/runatlantis/atlantis/server/core/db"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
. "github.com/runatlantis/atlantis/testing"
bolt "go.etcd.io/bbolt"
)
var lockBucket = "bucket"
var configBucket = "configBucket"
var project = models.NewProject("owner/repo", "parent/child")
var workspace = "default"
var pullNum = 1
var lock = models.ProjectLock{
Pull: models.PullRequest{
Num: pullNum,
},
User: models.User{
Username: "lkysow",
},
Workspace: workspace,
Project: project,
Time: time.Now(),
}
func TestLockCommandNotSet(t *testing.T) {
t.Log("retrieving apply lock when there are none should return empty LockCommand")
db, b := newTestDB()
defer cleanupDB(db)
exists, err := b.CheckCommandLock(command.Apply)
Ok(t, err)
Assert(t, exists == nil, "exp nil")
}
func TestLockCommandEnabled(t *testing.T) {
t.Log("setting the apply lock")
db, b := newTestDB()
defer cleanupDB(db)
timeNow := time.Now()
_, err := b.LockCommand(command.Apply, timeNow)
Ok(t, err)
config, err := b.CheckCommandLock(command.Apply)
Ok(t, err)
Equals(t, true, config.IsLocked())
}
func TestLockCommandFail(t *testing.T) {
t.Log("setting the apply lock")
db, b := newTestDB()
defer cleanupDB(db)
timeNow := time.Now()
_, err := b.LockCommand(command.Apply, timeNow)
Ok(t, err)
_, err = b.LockCommand(command.Apply, timeNow)
ErrEquals(t, "db transaction failed: lock already exists", err)
}
func TestUnlockCommandDisabled(t *testing.T) {
t.Log("unsetting the apply lock")
db, b := newTestDB()
defer cleanupDB(db)
timeNow := time.Now()
_, err := b.LockCommand(command.Apply, timeNow)
Ok(t, err)
config, err := b.CheckCommandLock(command.Apply)
Ok(t, err)
Equals(t, true, config.IsLocked())
err = b.UnlockCommand(command.Apply)
Ok(t, err)
config, err = b.CheckCommandLock(command.Apply)
Ok(t, err)
Assert(t, config == nil, "exp nil object")
}
func TestUnlockCommandFail(t *testing.T) {
t.Log("setting the apply lock")
db, b := newTestDB()
defer cleanupDB(db)
err := b.UnlockCommand(command.Apply)
ErrEquals(t, "db transaction failed: no lock exists", err)
}
func TestMixedLocksPresent(t *testing.T) {
db, b := newTestDB()
defer cleanupDB(db)
timeNow := time.Now()
_, err := b.LockCommand(command.Apply, timeNow)
Ok(t, err)
_, _, err = b.TryLock(lock)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
Equals(t, 1, len(ls))
}
func TestListNoLocks(t *testing.T) {
t.Log("listing locks when there are none should return an empty list")
db, b := newTestDB()
defer cleanupDB(db)
ls, err := b.List()
Ok(t, err)
Equals(t, 0, len(ls))
}
func TestListOneLock(t *testing.T) {
t.Log("listing locks when there is one should return it")
db, b := newTestDB()
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
Equals(t, 1, len(ls))
}
func TestListMultipleLocks(t *testing.T) {
t.Log("listing locks when there are multiple should return them")
db, b := newTestDB()
defer cleanupDB(db)
// add multiple locks
repos := []string{
"owner/repo1",
"owner/repo2",
"owner/repo3",
"owner/repo4",
}
for _, r := range repos {
newLock := lock
newLock.Project = models.NewProject(r, "path")
_, _, err := b.TryLock(newLock)
Ok(t, err)
}
ls, err := b.List()
Ok(t, err)
Equals(t, 4, len(ls))
for _, r := range repos {
found := false
for _, l := range ls {
if l.Project.RepoFullName == r {
found = true
}
}
Assert(t, found, "expected %s in %v", r, ls)
}
}
func TestListAddRemove(t *testing.T) {
t.Log("listing after adding and removing should return none")
db, b := newTestDB()
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
_, err = b.Unlock(project, workspace)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
Equals(t, 0, len(ls))
}
func TestLockingNoLocks(t *testing.T) {
t.Log("with no locks yet, lock should succeed")
db, b := newTestDB()
defer cleanupDB(db)
acquired, currLock, err := b.TryLock(lock)
Ok(t, err)
Equals(t, true, acquired)
Equals(t, lock, currLock)
}
func TestLockingExistingLock(t *testing.T) {
t.Log("if there is an existing lock, lock should...")
db, b := newTestDB()
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
t.Log("...succeed if the new project has a different path")
{
newLock := lock
newLock.Project = models.NewProject(project.RepoFullName, "different/path")
acquired, currLock, err := b.TryLock(newLock)
Ok(t, err)
Equals(t, true, acquired)
Equals(t, pullNum, currLock.Pull.Num)
}
t.Log("...succeed if the new project has a different workspace")
{
newLock := lock
newLock.Workspace = "different-workspace"
acquired, currLock, err := b.TryLock(newLock)
Ok(t, err)
Equals(t, true, acquired)
Equals(t, newLock, currLock)
}
t.Log("...succeed if the new project has a different repoName")
{
newLock := lock
newLock.Project = models.NewProject("different/repo", project.Path)
acquired, currLock, err := b.TryLock(newLock)
Ok(t, err)
Equals(t, true, acquired)
Equals(t, newLock, currLock)
}
t.Log("...not succeed if the new project only has a different pullNum")
{
newLock := lock
newLock.Pull.Num = lock.Pull.Num + 1
acquired, currLock, err := b.TryLock(newLock)
Ok(t, err)
Equals(t, false, acquired)
Equals(t, currLock.Pull.Num, pullNum)
}
}
func TestUnlockingNoLocks(t *testing.T) {
t.Log("unlocking with no locks should succeed")
db, b := newTestDB()
defer cleanupDB(db)
_, err := b.Unlock(project, workspace)
Ok(t, err)
}
func TestUnlocking(t *testing.T) {
t.Log("unlocking with an existing lock should succeed")
db, b := newTestDB()
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
_, err = b.Unlock(project, workspace)
Ok(t, err)
// should be no locks listed
ls, err := b.List()
Ok(t, err)
Equals(t, 0, len(ls))
// should be able to re-lock that repo with a new pull num
newLock := lock
newLock.Pull.Num = lock.Pull.Num + 1
acquired, currLock, err := b.TryLock(newLock)
Ok(t, err)
Equals(t, true, acquired)
Equals(t, newLock, currLock)
}
func TestUnlockingMultiple(t *testing.T) {
t.Log("unlocking and locking multiple locks should succeed")
db, b := newTestDB()
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
new1 := lock
new1.Project.RepoFullName = "new/repo"
_, _, err = b.TryLock(new1)
Ok(t, err)
new2 := lock
new2.Project.Path = "new/path"
_, _, err = b.TryLock(new2)
Ok(t, err)
new3 := lock
new3.Workspace = "new-workspace"
_, _, err = b.TryLock(new3)
Ok(t, err)
// now try and unlock them
_, err = b.Unlock(new3.Project, new3.Workspace)
Ok(t, err)
_, err = b.Unlock(new2.Project, workspace)
Ok(t, err)
_, err = b.Unlock(new1.Project, workspace)
Ok(t, err)
_, err = b.Unlock(project, workspace)
Ok(t, err)
// should be none left
ls, err := b.List()
Ok(t, err)
Equals(t, 0, len(ls))
}
func TestUnlockByPullNone(t *testing.T) {
t.Log("UnlockByPull should be successful when there are no locks")
db, b := newTestDB()
defer cleanupDB(db)
_, err := b.UnlockByPull("any/repo", 1)
Ok(t, err)
}
func TestUnlockByPullOne(t *testing.T) {
t.Log("with one lock, UnlockByPull should...")
db, b := newTestDB()
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
t.Log("...delete nothing when its the same repo but a different pull")
{
_, err := b.UnlockByPull(project.RepoFullName, pullNum+1)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
Equals(t, 1, len(ls))
}
t.Log("...delete nothing when its the same pull but a different repo")
{
_, err := b.UnlockByPull("different/repo", pullNum)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
Equals(t, 1, len(ls))
}
t.Log("...delete the lock when its the same repo and pull")
{
_, err := b.UnlockByPull(project.RepoFullName, pullNum)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
Equals(t, 0, len(ls))
}
}
func TestUnlockByPullAfterUnlock(t *testing.T) {
t.Log("after locking and unlocking, UnlockByPull should be successful")
db, b := newTestDB()
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
_, err = b.Unlock(project, workspace)
Ok(t, err)
_, err = b.UnlockByPull(project.RepoFullName, pullNum)
Ok(t, err)
ls, err := b.List()
Ok(t, err)
Equals(t, 0, len(ls))
}
func TestUnlockByPullMatching(t *testing.T) {
t.Log("UnlockByPull should delete all locks in that repo and pull num")
db, b := newTestDB()
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
// add additional locks with the same repo and pull num but different paths/workspaces
new1 := lock
new1.Project.Path = "dif/path"
_, _, err = b.TryLock(new1)
Ok(t, err)
new2 := lock
new2.Workspace = "new-workspace"
_, _, err = b.TryLock(new2)
Ok(t, err)
// there should now be 3
ls, err := b.List()
Ok(t, err)
Equals(t, 3, len(ls))
// should all be unlocked
_, err = b.UnlockByPull(project.RepoFullName, pullNum)
Ok(t, err)
ls, err = b.List()
Ok(t, err)
Equals(t, 0, len(ls))
}
func TestGetLockNotThere(t *testing.T) {
t.Log("getting a lock that doesn't exist should return a nil pointer")
db, b := newTestDB()
defer cleanupDB(db)
l, err := b.GetLock(project, workspace)
Ok(t, err)
Equals(t, (*models.ProjectLock)(nil), l)
}
func TestGetLock(t *testing.T) {
t.Log("getting a lock should return the lock")
db, b := newTestDB()
defer cleanupDB(db)
_, _, err := b.TryLock(lock)
Ok(t, err)
l, err := b.GetLock(project, workspace)
Ok(t, err)
// can't compare against time so doing each field
Equals(t, lock.Project, l.Project)
Equals(t, lock.Workspace, l.Workspace)
Equals(t, lock.Pull, l.Pull)
Equals(t, lock.User, l.User)
}
// Test we can create a status and then getCommandLock it.
func TestPullStatus_UpdateGet(t *testing.T) {
b := newTestDB2(t)
pull := models.PullRequest{
Num: 1,
HeadCommit: "sha",
URL: "url",
HeadBranch: "head",
BaseBranch: "base",
Author: "lkysow",
State: models.OpenPullState,
BaseRepo: models.Repo{
FullName: "runatlantis/atlantis",
Owner: "runatlantis",
Name: "atlantis",
CloneURL: "clone-url",
SanitizedCloneURL: "clone-url",
VCSHost: models.VCSHost{
Hostname: "github.com",
Type: models.Github,
},
},
}
status, err := b.UpdatePullWithResults(
pull,
[]command.ProjectResult{
{
Command: command.Plan,
RepoRelDir: ".",
Workspace: "default",
Failure: "failure",
},
})
Ok(t, err)
maybeStatus, err := b.GetPullStatus(pull)
Ok(t, err)
Equals(t, pull, maybeStatus.Pull) // nolint: staticcheck
Equals(t, []models.ProjectStatus{
{
Workspace: "default",
RepoRelDir: ".",
ProjectName: "",
Status: models.ErroredPlanStatus,
},
}, status.Projects)
}
// Test we can create a status, delete it, and then we shouldn't be able to getCommandLock
// it.
func TestPullStatus_UpdateDeleteGet(t *testing.T) {
b := newTestDB2(t)
pull := models.PullRequest{
Num: 1,
HeadCommit: "sha",
URL: "url",
HeadBranch: "head",
BaseBranch: "base",
Author: "lkysow",
State: models.OpenPullState,
BaseRepo: models.Repo{
FullName: "runatlantis/atlantis",
Owner: "runatlantis",
Name: "atlantis",
CloneURL: "clone-url",
SanitizedCloneURL: "clone-url",
VCSHost: models.VCSHost{
Hostname: "github.com",
Type: models.Github,
},
},
}
_, err := b.UpdatePullWithResults(
pull,
[]command.ProjectResult{
{
RepoRelDir: ".",
Workspace: "default",
Failure: "failure",
},
})
Ok(t, err)
err = b.DeletePullStatus(pull)
Ok(t, err)
maybeStatus, err := b.GetPullStatus(pull)
Ok(t, err)
Assert(t, maybeStatus == nil, "exp nil")
}
// Test we can create a status, update a specific project's status within that
// pull status, and when we getCommandLock all the project statuses, that specific project
// should be updated.
func TestPullStatus_UpdateProject(t *testing.T) {
b := newTestDB2(t)
pull := models.PullRequest{
Num: 1,
HeadCommit: "sha",
URL: "url",
HeadBranch: "head",
BaseBranch: "base",
Author: "lkysow",
State: models.OpenPullState,
BaseRepo: models.Repo{
FullName: "runatlantis/atlantis",
Owner: "runatlantis",
Name: "atlantis",
CloneURL: "clone-url",
SanitizedCloneURL: "clone-url",
VCSHost: models.VCSHost{
Hostname: "github.com",
Type: models.Github,
},
},
}
_, err := b.UpdatePullWithResults(
pull,
[]command.ProjectResult{
{
RepoRelDir: ".",
Workspace: "default",
Failure: "failure",
},
{
RepoRelDir: ".",
Workspace: "staging",
ApplySuccess: "success!",
},
})
Ok(t, err)
err = b.UpdateProjectStatus(pull, "default", ".", models.DiscardedPlanStatus)
Ok(t, err)
status, err := b.GetPullStatus(pull)
Ok(t, err)
Equals(t, pull, status.Pull) // nolint: staticcheck
Equals(t, []models.ProjectStatus{
{
Workspace: "default",
RepoRelDir: ".",
ProjectName: "",
Status: models.DiscardedPlanStatus,
},
{
Workspace: "staging",
RepoRelDir: ".",
ProjectName: "",
Status: models.AppliedPlanStatus,
},
}, status.Projects) // nolint: staticcheck
}
// Test that if we update an existing pull status and our new status is for a
// different HeadSHA, that we just overwrite the old status.
func TestPullStatus_UpdateNewCommit(t *testing.T) {
b := newTestDB2(t)
pull := models.PullRequest{
Num: 1,
HeadCommit: "sha",
URL: "url",
HeadBranch: "head",
BaseBranch: "base",
Author: "lkysow",
State: models.OpenPullState,
BaseRepo: models.Repo{
FullName: "runatlantis/atlantis",
Owner: "runatlantis",
Name: "atlantis",
CloneURL: "clone-url",
SanitizedCloneURL: "clone-url",
VCSHost: models.VCSHost{
Hostname: "github.com",
Type: models.Github,
},
},
}
_, err := b.UpdatePullWithResults(
pull,
[]command.ProjectResult{
{
RepoRelDir: ".",
Workspace: "default",
Failure: "failure",
},
})
Ok(t, err)
pull.HeadCommit = "newsha"
status, err := b.UpdatePullWithResults(pull,
[]command.ProjectResult{
{
RepoRelDir: ".",
Workspace: "staging",
ApplySuccess: "success!",
},
})
Ok(t, err)
Equals(t, 1, len(status.Projects))
maybeStatus, err := b.GetPullStatus(pull)
Ok(t, err)
Equals(t, pull, maybeStatus.Pull)
Equals(t, []models.ProjectStatus{
{
Workspace: "staging",
RepoRelDir: ".",
ProjectName: "",
Status: models.AppliedPlanStatus,
},
}, maybeStatus.Projects)
}
// Test that if we update an existing pull status via Apply and our new status is for a
// the same commit, that we merge the statuses.
func TestPullStatus_UpdateMerge_Apply(t *testing.T) {
b := newTestDB2(t)
pull := models.PullRequest{
Num: 1,
HeadCommit: "sha",
URL: "url",
HeadBranch: "head",
BaseBranch: "base",
Author: "lkysow",
State: models.OpenPullState,
BaseRepo: models.Repo{
FullName: "runatlantis/atlantis",
Owner: "runatlantis",
Name: "atlantis",
CloneURL: "clone-url",
SanitizedCloneURL: "clone-url",
VCSHost: models.VCSHost{
Hostname: "github.com",
Type: models.Github,
},
},
}
_, err := b.UpdatePullWithResults(
pull,
[]command.ProjectResult{
{
Command: command.Plan,
RepoRelDir: "mergeme",
Workspace: "default",
Failure: "failure",
},
{
Command: command.Plan,
RepoRelDir: "projectname",
Workspace: "default",
ProjectName: "projectname",
Failure: "failure",
},
{
Command: command.Plan,
RepoRelDir: "staythesame",
Workspace: "default",
PlanSuccess: &models.PlanSuccess{
TerraformOutput: "tf out",
LockURL: "lock-url",
RePlanCmd: "plan command",
ApplyCmd: "apply command",
},
},
})
Ok(t, err)
updateStatus, err := b.UpdatePullWithResults(pull,
[]command.ProjectResult{
{
Command: command.Apply,
RepoRelDir: "mergeme",
Workspace: "default",
ApplySuccess: "applied!",
},
{
Command: command.Apply,
RepoRelDir: "projectname",
Workspace: "default",
ProjectName: "projectname",
Error: errors.New("apply error"),
},
{
Command: command.Apply,
RepoRelDir: "newresult",
Workspace: "default",
ApplySuccess: "success!",
},
})
Ok(t, err)
getStatus, err := b.GetPullStatus(pull)
Ok(t, err)
// Test both the pull state returned from the update call *and* the getCommandLock
// call.
for _, s := range []models.PullStatus{updateStatus, *getStatus} {
Equals(t, pull, s.Pull)
Equals(t, []models.ProjectStatus{
{
RepoRelDir: "mergeme",
Workspace: "default",
Status: models.AppliedPlanStatus,
},
{
RepoRelDir: "projectname",
Workspace: "default",
ProjectName: "projectname",
Status: models.ErroredApplyStatus,
},
{
RepoRelDir: "staythesame",
Workspace: "default",
Status: models.PlannedPlanStatus,
},
{
RepoRelDir: "newresult",
Workspace: "default",
Status: models.AppliedPlanStatus,
},
}, updateStatus.Projects)
}
}
// Test that if we update one existing policy status via approve_policies and our new status is for a
// the same commit, that we merge the statuses.
func TestPullStatus_UpdateMerge_ApprovePolicies(t *testing.T) {
b := newTestDB2(t)
pull := models.PullRequest{
Num: 1,
HeadCommit: "sha",
URL: "url",
HeadBranch: "head",
BaseBranch: "base",
Author: "lkysow",
State: models.OpenPullState,
BaseRepo: models.Repo{
FullName: "runatlantis/atlantis",
Owner: "runatlantis",
Name: "atlantis",
CloneURL: "clone-url",
SanitizedCloneURL: "clone-url",
VCSHost: models.VCSHost{
Hostname: "github.com",
Type: models.Github,
},
},
}
_, err := b.UpdatePullWithResults(
pull,
[]command.ProjectResult{
{
Command: command.PolicyCheck,
RepoRelDir: "mergeme",
Workspace: "default",
Failure: "policy failure",
PolicyCheckResults: &models.PolicyCheckResults{
PolicySetResults: []models.PolicySetResult{
{
PolicySetName: "policy1",
ReqApprovals: 1,
},
},
},
},
{
Command: command.PolicyCheck,
RepoRelDir: "projectname",
Workspace: "default",
ProjectName: "projectname",
Failure: "policy failure",
PolicyCheckResults: &models.PolicyCheckResults{
PolicySetResults: []models.PolicySetResult{
{
PolicySetName: "policy1",
ReqApprovals: 1,
},
},
},
},
})
Ok(t, err)
updateStatus, err := b.UpdatePullWithResults(pull,
[]command.ProjectResult{
{
Command: command.ApprovePolicies,
RepoRelDir: "mergeme",
Workspace: "default",
PolicyCheckResults: &models.PolicyCheckResults{
PolicySetResults: []models.PolicySetResult{
{
PolicySetName: "policy1",
ReqApprovals: 1,
CurApprovals: 1,
},
},
},
},
})
Ok(t, err)
getStatus, err := b.GetPullStatus(pull)
Ok(t, err)
// Test both the pull state returned from the update call *and* the getCommandLock
// call.
for _, s := range []models.PullStatus{updateStatus, *getStatus} {
Equals(t, pull, s.Pull)
Equals(t, []models.ProjectStatus{
{
RepoRelDir: "mergeme",
Workspace: "default",
Status: models.PassedPolicyCheckStatus,
PolicyStatus: []models.PolicySetStatus{
{
PolicySetName: "policy1",
Approvals: 1,
},
},
},
{
RepoRelDir: "projectname",
Workspace: "default",
ProjectName: "projectname",
Status: models.ErroredPolicyCheckStatus,
PolicyStatus: []models.PolicySetStatus{
{
PolicySetName: "policy1",
Approvals: 0,
},
},
},
}, updateStatus.Projects)
}
}
// newTestDB returns a TestDB using a temporary path.
func newTestDB() (*bolt.DB, *db.BoltDB) {
// Retrieve a temporary path.
f, err := os.CreateTemp("", "")
if err != nil {
panic(errors.Wrap(err, "failed to create temp file"))
}
path := f.Name()
f.Close() // nolint: errcheck
// Open the database.
boltDB, err := bolt.Open(path, 0600, nil)
if err != nil {
panic(errors.Wrap(err, "could not start bolt DB"))
}
if err := boltDB.Update(func(tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(lockBucket)); err != nil {
return errors.Wrap(err, "failed to create bucket")
}
if _, err := tx.CreateBucketIfNotExists([]byte(configBucket)); err != nil {
return errors.Wrap(err, "failed to create bucket")
}
return nil
}); err != nil {
panic(errors.Wrap(err, "could not create bucket"))
}
b, _ := db.NewWithDB(boltDB, lockBucket, configBucket)
return boltDB, b
}
func newTestDB2(t *testing.T) *db.BoltDB {
tmp := t.TempDir()
boltDB, err := db.New(tmp)
Ok(t, err)
return boltDB
}
func cleanupDB(db *bolt.DB) {
os.Remove(db.Path()) // nolint: errcheck
db.Close() // nolint: errcheck
}