Global atlantis lock new release branch (#1473)

* Orca 679 global atlantis lock new release branch (#49)

* Adding CommandLocker to boltDB and exposing it through locker interface

* Apply lock ui and apply command lock controller

* Minor comments

* Adding more tests and refactorinng

* Linting fixes

* creating applyLockingClient variable to fix interface error

* nullsink for stats

* Addressing PR comments

* fixing e2e tests

* linting fix fml

* Update outdated function descriptions

Address PR comments

* revert stats sink changes

* remove unnecessary dependencies
This commit is contained in:
Sarvar Muminov
2021-04-01 15:40:29 -07:00
committed by GitHub
parent ca976d8bd3
commit ad76385c76
25 changed files with 1460 additions and 49 deletions

View File

@@ -2,6 +2,7 @@ package events
import (
"github.com/runatlantis/atlantis/server/events/db"
"github.com/runatlantis/atlantis/server/events/locking"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs"
)
@@ -9,7 +10,7 @@ import (
func NewApplyCommandRunner(
vcsClient vcs.Client,
disableApplyAll bool,
disableApply bool,
applyCommandLocker locking.ApplyLockChecker,
commitStatusUpdater CommitStatusUpdater,
prjCommandBuilder ProjectApplyCommandBuilder,
prjCmdRunner ProjectApplyCommandRunner,
@@ -22,7 +23,7 @@ func NewApplyCommandRunner(
return &ApplyCommandRunner{
vcsClient: vcsClient,
DisableApplyAll: disableApplyAll,
DisableApply: disableApply,
locker: applyCommandLocker,
commitStatusUpdater: commitStatusUpdater,
prjCmdBuilder: prjCommandBuilder,
prjCmdRunner: prjCmdRunner,
@@ -36,8 +37,8 @@ func NewApplyCommandRunner(
type ApplyCommandRunner struct {
DisableApplyAll bool
DisableApply bool
DB *db.BoltDB
locker locking.ApplyLockChecker
vcsClient vcs.Client
commitStatusUpdater CommitStatusUpdater
prjCmdBuilder ProjectApplyCommandBuilder
@@ -53,7 +54,15 @@ func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {
baseRepo := ctx.Pull.BaseRepo
pull := ctx.Pull
if a.DisableApply {
locked, err := a.IsLocked()
// CheckApplyLock falls back to DisableApply flag if fetching the lock
// raises an erro r
// We will log failure as warning
if err != nil {
ctx.Log.Warn("checking global apply lock: %s", err)
}
if locked {
ctx.Log.Info("ignoring apply command since apply disabled globally")
if err := a.vcsClient.CreateComment(baseRepo, pull.Num, applyDisabledComment, models.ApplyCommand.String()); err != nil {
ctx.Log.Err("unable to comment on pull request: %s", err)
@@ -135,6 +144,12 @@ func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {
}
}
func (a *ApplyCommandRunner) IsLocked() (bool, error) {
lock, err := a.locker.CheckApplyLock()
return lock.Locked, err
}
func (a *ApplyCommandRunner) isParallelEnabled(projectCmds []models.ProjectCommandContext) bool {
return len(projectCmds) > 0 && projectCmds[0].ParallelApplyEnabled
}

View File

@@ -0,0 +1,69 @@
package events_test
import (
"errors"
"testing"
"github.com/google/go-github/v31/github"
. "github.com/petergtz/pegomock"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/locking"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/models/fixtures"
)
func TestApplyCommandRunner_IsLocked(t *testing.T) {
RegisterMockTestingT(t)
cases := []struct {
Description string
ApplyLocked bool
ApplyLockError error
ExpComment string
}{
{
Description: "When global apply lock is present IsDisabled returns true",
ApplyLocked: true,
ApplyLockError: nil,
ExpComment: "**Error:** Running `atlantis apply` is disabled.",
},
{
Description: "When no global apply lock is present and DisableApply flag is false IsDisabled returns false",
ApplyLocked: false,
ApplyLockError: nil,
ExpComment: "Ran Apply for 0 projects:\n\n\n\n",
},
{
Description: "If ApplyLockChecker returns an error IsDisabled return value of DisableApply flag",
ApplyLockError: errors.New("error"),
ApplyLocked: false,
ExpComment: "Ran Apply for 0 projects:\n\n\n\n",
},
}
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
vcsClient := setup(t)
pull := &github.PullRequest{
State: github.String("open"),
}
modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState, Num: fixtures.Pull.Num}
When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil)
When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil)
ctx := &events.CommandContext{
User: fixtures.User,
Log: noopLogger,
Pull: modelPull,
HeadRepo: fixtures.GithubRepo,
Trigger: events.Comment,
}
When(applyLockChecker.CheckApplyLock()).ThenReturn(locking.ApplyCommandLock{Locked: c.ApplyLocked}, c.ApplyLockError)
applyCommandRunner.Run(ctx, &events.CommentCommand{Name: models.ApplyCommand})
vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, c.ExpComment, "apply")
})
}
}

View File

@@ -26,6 +26,7 @@ import (
"github.com/google/go-github/v31/github"
. "github.com/petergtz/pegomock"
"github.com/runatlantis/atlantis/server/events"
lockingmocks "github.com/runatlantis/atlantis/server/events/locking/mocks"
"github.com/runatlantis/atlantis/server/events/mocks"
eventmocks "github.com/runatlantis/atlantis/server/events/mocks"
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
@@ -59,6 +60,7 @@ var autoMerger *events.AutoMerger
var policyCheckCommandRunner *events.PolicyCheckCommandRunner
var approvePoliciesCommandRunner *events.ApprovePoliciesCommandRunner
var planCommandRunner *events.PlanCommandRunner
var applyLockChecker *lockingmocks.MockApplyLockChecker
var applyCommandRunner *events.ApplyCommandRunner
var unlockCommandRunner *events.UnlockCommandRunner
var preWorkflowHooksCommandRunner events.PreWorkflowHooksCommandRunner
@@ -85,6 +87,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
drainer = &events.Drainer{}
deleteLockCommand = eventmocks.NewMockDeleteLockCommand()
applyLockChecker = lockingmocks.NewMockApplyLockChecker()
When(logger.GetLevel()).ThenReturn(logging.Info)
When(logger.NewLogger("runatlantis/atlantis#1", true, logging.Info)).
ThenReturn(pullLogger)
@@ -131,7 +134,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
applyCommandRunner = events.NewApplyCommandRunner(
vcsClient,
false,
false,
applyLockChecker,
commitUpdater,
projectCommandBuilder,
projectCommandRunner,
@@ -292,23 +295,6 @@ func TestRunCommentCommand_DisableApplyAllDisabled(t *testing.T) {
vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "**Error:** Running `atlantis apply` without flags is disabled. You must specify which project to apply via the `-d <dir>`, `-w <workspace>` or `-p <project name>` flags.", "apply")
}
func TestRunCommentCommand_ApplyDisabled(t *testing.T) {
t.Log("if \"atlantis apply\" is run and this is disabled globally atlantis should" +
" comment saying that this is not allowed")
vcsClient := setup(t)
applyCommandRunner.DisableApply = true
defer func() { applyCommandRunner.DisableApply = false }()
pull := &github.PullRequest{
State: github.String("open"),
}
modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState, Num: fixtures.Pull.Num}
When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil)
When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil)
ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, modelPull.Num, &events.CommentCommand{Name: models.ApplyCommand})
vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "**Error:** Running `atlantis apply` is disabled.", "apply")
}
func TestRunCommentCommand_DisableDisableAutoplan(t *testing.T) {
t.Log("if \"DisableAutoplan is true\" are disabled and we are silencing return and do not comment with error")
setup(t)

View File

@@ -17,15 +17,17 @@ import (
// BoltDB is a database using BoltDB
type BoltDB struct {
db *bolt.DB
locksBucketName []byte
pullsBucketName []byte
db *bolt.DB
locksBucketName []byte
pullsBucketName []byte
globalLocksBucketName []byte
}
const (
locksBucketName = "runLocks"
pullsBucketName = "pulls"
pullKeySeparator = "::"
locksBucketName = "runLocks"
pullsBucketName = "pulls"
globalLocksBucketName = "globalLocks"
pullKeySeparator = "::"
)
// New returns a valid locker. We need to be able to write to dataDir
@@ -50,18 +52,31 @@ func New(dataDir string) (*BoltDB, error) {
if _, err = tx.CreateBucketIfNotExists([]byte(pullsBucketName)); err != nil {
return errors.Wrapf(err, "creating bucket %q", pullsBucketName)
}
if _, err = tx.CreateBucketIfNotExists([]byte(globalLocksBucketName)); err != nil {
return errors.Wrapf(err, "creating bucket %q", globalLocksBucketName)
}
return nil
})
if err != nil {
return nil, errors.Wrap(err, "starting BoltDB")
}
// todo: close BoltDB when server is sigtermed
return &BoltDB{db: db, locksBucketName: []byte(locksBucketName), pullsBucketName: []byte(pullsBucketName)}, nil
return &BoltDB{
db: db,
locksBucketName: []byte(locksBucketName),
pullsBucketName: []byte(pullsBucketName),
globalLocksBucketName: []byte(globalLocksBucketName),
}, nil
}
// NewWithDB is used for testing.
func NewWithDB(db *bolt.DB, bucket string) (*BoltDB, error) {
return &BoltDB{db: db, locksBucketName: []byte(bucket), pullsBucketName: []byte(pullsBucketName)}, nil
func NewWithDB(db *bolt.DB, bucket string, globalBucket string) (*BoltDB, error) {
return &BoltDB{
db: db,
locksBucketName: []byte(bucket),
pullsBucketName: []byte(pullsBucketName),
globalLocksBucketName: []byte(globalBucket),
}, nil
}
// TryLock attempts to create a new lock. If the lock is
@@ -155,6 +170,87 @@ func (b *BoltDB) List() ([]models.ProjectLock, error) {
return locks, nil
}
// LockCommand attempts to create a new lock for a CommandName.
// If the lock doesn't exists, it will create a lock and return a pointer to it.
// If the lock already exists, it will return an "lock already exists" error
func (b *BoltDB) LockCommand(cmdName models.CommandName, lockTime time.Time) (*models.CommandLock, error) {
lock := models.CommandLock{
CommandName: cmdName,
LockMetadata: models.LockMetadata{
UnixTime: lockTime.Unix(),
},
}
newLockSerialized, _ := json.Marshal(lock)
transactionErr := b.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(b.globalLocksBucketName)
currLockSerialized := bucket.Get([]byte(b.commandLockKey(cmdName)))
if currLockSerialized != nil {
return errors.New("lock already exists")
}
// This will only error on readonly buckets, it's okay to ignore.
bucket.Put([]byte(b.commandLockKey(cmdName)), newLockSerialized) // nolint: errcheck
return nil
})
if transactionErr != nil {
return nil, errors.Wrap(transactionErr, "db transaction failed")
}
return &lock, nil
}
// UnlockCommand removes CommandName lock if present.
// If there are no lock it returns an error.
func (b *BoltDB) UnlockCommand(cmdName models.CommandName) error {
transactionErr := b.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket(b.globalLocksBucketName)
if l := bucket.Get([]byte(b.commandLockKey(cmdName))); l == nil {
return errors.New("no lock exists")
}
return bucket.Delete([]byte(b.commandLockKey(cmdName)))
})
if transactionErr != nil {
return errors.Wrap(transactionErr, "db transaction failed")
}
return nil
}
// CheckCommandLock checks if CommandName lock was set.
// If the lock exists return the pointer to the lock object, otherwise return nil
func (b *BoltDB) CheckCommandLock(cmdName models.CommandName) (*models.CommandLock, error) {
cmdLock := models.CommandLock{}
found := false
err := b.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket(b.globalLocksBucketName)
serializedLock := bucket.Get([]byte(b.commandLockKey(cmdName)))
if serializedLock != nil {
if err := json.Unmarshal(serializedLock, &cmdLock); err != nil {
return errors.Wrap(err, "failed to deserialize UserConfig")
}
found = true
}
return nil
})
if found {
return &cmdLock, err
}
return nil, err
}
// UnlockByPull deletes all locks associated with that pull request and returns them.
func (b *BoltDB) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {
var locks []models.ProjectLock
@@ -355,6 +451,10 @@ func (b *BoltDB) pullKey(pull models.PullRequest) ([]byte, error) {
nil
}
func (b *BoltDB) commandLockKey(cmdName models.CommandName) string {
return fmt.Sprintf("%s/lock", cmdName)
}
func (b *BoltDB) lockKey(p models.Project, workspace string) string {
return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, workspace)
}

View File

@@ -28,6 +28,7 @@ import (
)
var lockBucket = "bucket"
var configBucket = "configBucket"
var project = models.NewProject("owner/repo", "parent/child")
var workspace = "default"
var pullNum = 1
@@ -43,6 +44,82 @@ var lock = models.ProjectLock{
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(models.ApplyCommand)
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(models.ApplyCommand, timeNow)
Ok(t, err)
config, err := b.CheckCommandLock(models.ApplyCommand)
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(models.ApplyCommand, timeNow)
Ok(t, err)
_, err = b.LockCommand(models.ApplyCommand, 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(models.ApplyCommand, timeNow)
Ok(t, err)
config, err := b.CheckCommandLock(models.ApplyCommand)
Ok(t, err)
Equals(t, true, config.IsLocked())
err = b.UnlockCommand(models.ApplyCommand)
Ok(t, err)
config, err = b.CheckCommandLock(models.ApplyCommand)
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(models.ApplyCommand)
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(models.ApplyCommand, 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()
@@ -353,7 +430,7 @@ func TestGetLock(t *testing.T) {
Equals(t, lock.User, l.User)
}
// Test we can create a status and then get it.
// Test we can create a status and then getCommandLock it.
func TestPullStatus_UpdateGet(t *testing.T) {
b, cleanup := newTestDB2(t)
defer cleanup()
@@ -404,7 +481,7 @@ func TestPullStatus_UpdateGet(t *testing.T) {
}, status.Projects)
}
// Test we can create a status, delete it, and then we shouldn't be able to get
// 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, cleanup := newTestDB2(t)
@@ -450,7 +527,7 @@ func TestPullStatus_UpdateDeleteGet(t *testing.T) {
}
// Test we can create a status, update a specific project's status within that
// pull status, and when we get all the project statuses, that specific project
// pull status, and when we getCommandLock all the project statuses, that specific project
// should be updated.
func TestPullStatus_UpdateProject(t *testing.T) {
b, cleanup := newTestDB2(t)
@@ -661,7 +738,7 @@ func TestPullStatus_UpdateMerge(t *testing.T) {
getStatus, err := b.GetPullStatus(pull)
Ok(t, err)
// Test both the pull state returned from the update call *and* the get
// 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)
@@ -710,11 +787,14 @@ func newTestDB() (*bolt.DB, *db.BoltDB) {
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)
b, _ := db.NewWithDB(boltDB, lockBucket, configBucket)
return boltDB, b
}

View File

@@ -0,0 +1,113 @@
package locking
import (
"errors"
"time"
"github.com/runatlantis/atlantis/server/events/models"
)
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_apply_lock_checker.go ApplyLockChecker
// ApplyLockChecker is an implementation of the global apply lock retrieval.
// It returns an object that contains information about apply locks status.
type ApplyLockChecker interface {
CheckApplyLock() (ApplyCommandLock, error)
}
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_apply_locker.go ApplyLocker
// ApplyLocker interface that manages locks for apply command runner
type ApplyLocker interface {
// LockApply creates a lock for ApplyCommand if lock already exists it will
// return existing lock without any changes
LockApply() (ApplyCommandLock, error)
// UnlockApply deletes apply lock created by LockApply if present, otherwise
// it is a no-op
UnlockApply() error
ApplyLockChecker
}
// ApplyCommandLock contains information about apply command lock status.
type ApplyCommandLock struct {
// Locked is true is when apply commands are locked
// Either by using DisableApply flag or creating a global ApplyCommandLock
// DisableApply lock take precedence when set
Locked bool
Time time.Time
Failure string
}
type ApplyClient struct {
backend Backend
disableApplyFlag bool
}
func NewApplyClient(backend Backend, disableApplyFlag bool) ApplyLocker {
return &ApplyClient{
backend: backend,
disableApplyFlag: disableApplyFlag,
}
}
// LockApply acquires global apply lock.
// DisableApplyFlag takes presedence to any existing locks, if it is set to true
// this function returns an error
func (c *ApplyClient) LockApply() (ApplyCommandLock, error) {
response := ApplyCommandLock{}
if c.disableApplyFlag {
return response, errors.New("DisableApplyFlag is set; Apply commands are locked globally until flag is unset")
}
applyCmdLock, err := c.backend.LockCommand(models.ApplyCommand, time.Now())
if err != nil {
return response, err
}
if applyCmdLock != nil {
response.Locked = true
response.Time = applyCmdLock.LockTime()
}
return response, nil
}
// UnlockApply releases a global apply lock.
// DisableApplyFlag takes presedence to any existing locks, if it is set to true
// this function returns an error
func (c *ApplyClient) UnlockApply() error {
if c.disableApplyFlag {
return errors.New("apply commands are disabled until DisableApply flag is unset")
}
err := c.backend.UnlockCommand(models.ApplyCommand)
if err != nil {
return err
}
return nil
}
// CheckApplyLock retrieves an apply command lock if present.
// If DisableApplyFlag is set it will always return a lock.
func (c *ApplyClient) CheckApplyLock() (ApplyCommandLock, error) {
response := ApplyCommandLock{}
if c.disableApplyFlag {
return ApplyCommandLock{
Locked: true,
}, nil
}
applyCmdLock, err := c.backend.CheckCommandLock(models.ApplyCommand)
if err != nil {
return response, err
}
if applyCmdLock != nil {
response.Locked = true
response.Time = applyCmdLock.LockTime()
}
return response, nil
}

View File

@@ -32,6 +32,10 @@ type Backend interface {
List() ([]models.ProjectLock, error)
GetLock(project models.Project, workspace string) (*models.ProjectLock, error)
UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error)
LockCommand(cmdName models.CommandName, lockTime time.Time) (*models.CommandLock, error)
UnlockCommand(cmdName models.CommandName) error
CheckCommandLock(cmdName models.CommandName) (*models.CommandLock, error)
}
// TryLockResponse results from an attempted lock.

View File

@@ -181,3 +181,107 @@ func TestGetLock_NoOpLocker(t *testing.T) {
var expected *models.ProjectLock = nil
Equals(t, expected, lock)
}
func TestApplyLocker(t *testing.T) {
RegisterMockTestingT(t)
applyLock := &models.CommandLock{
CommandName: models.ApplyCommand,
LockMetadata: models.LockMetadata{
UnixTime: time.Now().Unix(),
},
}
t.Run("LockApply", func(t *testing.T) {
t.Run("backend errors", func(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.LockCommand(matchers.AnyModelsCommandName(), matchers.AnyTimeTime())).ThenReturn(nil, errExpected)
l := locking.NewApplyClient(backend, false)
lock, err := l.LockApply()
Equals(t, errExpected, err)
Assert(t, !lock.Locked, "exp false")
})
t.Run("can't lock if userConfig.DisableApply is set", func(t *testing.T) {
backend := mocks.NewMockBackend()
l := locking.NewApplyClient(backend, true)
_, err := l.LockApply()
ErrEquals(t, "DisableApplyFlag is set; Apply commands are locked globally until flag is unset", err)
backend.VerifyWasCalled(Never()).LockCommand(matchers.AnyModelsCommandName(), matchers.AnyTimeTime())
})
t.Run("succeeds", func(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.LockCommand(matchers.AnyModelsCommandName(), matchers.AnyTimeTime())).ThenReturn(applyLock, nil)
l := locking.NewApplyClient(backend, false)
lock, _ := l.LockApply()
Assert(t, lock.Locked, "exp lock present")
})
})
t.Run("UnlockApply", func(t *testing.T) {
t.Run("backend fails", func(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.UnlockCommand(matchers.AnyModelsCommandName())).ThenReturn(errExpected)
l := locking.NewApplyClient(backend, false)
err := l.UnlockApply()
Equals(t, errExpected, err)
})
t.Run("can't unlock if userConfig.DisableApply is set", func(t *testing.T) {
backend := mocks.NewMockBackend()
l := locking.NewApplyClient(backend, true)
err := l.UnlockApply()
ErrEquals(t, "apply commands are disabled until DisableApply flag is unset", err)
backend.VerifyWasCalled(Never()).UnlockCommand(matchers.AnyModelsCommandName())
})
t.Run("succeeds", func(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.UnlockCommand(matchers.AnyModelsCommandName())).ThenReturn(nil)
l := locking.NewApplyClient(backend, false)
err := l.UnlockApply()
Equals(t, nil, err)
})
})
t.Run("CheckApplyLock", func(t *testing.T) {
t.Run("fails", func(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.CheckCommandLock(matchers.AnyModelsCommandName())).ThenReturn(nil, errExpected)
l := locking.NewApplyClient(backend, false)
lock, err := l.CheckApplyLock()
Equals(t, errExpected, err)
Equals(t, lock.Locked, false)
})
t.Run("when DisableApply flag is set always return a lock", func(t *testing.T) {
backend := mocks.NewMockBackend()
l := locking.NewApplyClient(backend, true)
lock, err := l.CheckApplyLock()
Ok(t, err)
Equals(t, lock.Locked, true)
backend.VerifyWasCalled(Never()).CheckCommandLock(matchers.AnyModelsCommandName())
})
t.Run("UnlockCommand succeeds", func(t *testing.T) {
backend := mocks.NewMockBackend()
When(backend.CheckCommandLock(matchers.AnyModelsCommandName())).ThenReturn(applyLock, nil)
l := locking.NewApplyClient(backend, false)
lock, err := l.CheckApplyLock()
Equals(t, nil, err)
Assert(t, lock.Locked, "exp lock present")
})
})
}

View File

@@ -0,0 +1,20 @@
// Code generated by pegomock. DO NOT EDIT.
package matchers
import (
"reflect"
"github.com/petergtz/pegomock"
locking "github.com/runatlantis/atlantis/server/events/locking"
)
func AnyLockingApplyCommandLockResponse() locking.ApplyCommandLock {
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(locking.ApplyCommandLock))(nil)).Elem()))
var nullValue locking.ApplyCommandLock
return nullValue
}
func EqLockingApplyCommandLockResponse(value locking.ApplyCommandLock) locking.ApplyCommandLock {
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
var nullValue locking.ApplyCommandLock
return nullValue
}

View File

@@ -0,0 +1,20 @@
// Code generated by pegomock. DO NOT EDIT.
package matchers
import (
"reflect"
"github.com/petergtz/pegomock"
models "github.com/runatlantis/atlantis/server/events/models"
)
func AnyModelsCommandLock() models.CommandLock {
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.CommandLock))(nil)).Elem()))
var nullValue models.CommandLock
return nullValue
}
func EqModelsCommandLock(value models.CommandLock) models.CommandLock {
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
var nullValue models.CommandLock
return nullValue
}

View File

@@ -0,0 +1,20 @@
// Code generated by pegomock. DO NOT EDIT.
package matchers
import (
"reflect"
"github.com/petergtz/pegomock"
models "github.com/runatlantis/atlantis/server/events/models"
)
func AnyModelsCommandName() models.CommandName {
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(models.CommandName))(nil)).Elem()))
var nullValue models.CommandName
return nullValue
}
func EqModelsCommandName(value models.CommandName) models.CommandName {
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
var nullValue models.CommandName
return nullValue
}

View File

@@ -0,0 +1,20 @@
// Code generated by pegomock. DO NOT EDIT.
package matchers
import (
"reflect"
"github.com/petergtz/pegomock"
models "github.com/runatlantis/atlantis/server/events/models"
)
func AnyPtrToModelsCommandLock() *models.CommandLock {
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(*models.CommandLock))(nil)).Elem()))
var nullValue *models.CommandLock
return nullValue
}
func EqPtrToModelsCommandLock(value *models.CommandLock) *models.CommandLock {
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
var nullValue *models.CommandLock
return nullValue
}

View File

@@ -0,0 +1,20 @@
// Code generated by pegomock. DO NOT EDIT.
package matchers
import (
"reflect"
"github.com/petergtz/pegomock"
time "time"
)
func AnyTimeTime() time.Time {
pegomock.RegisterMatcher(pegomock.NewAnyMatcher(reflect.TypeOf((*(time.Time))(nil)).Elem()))
var nullValue time.Time
return nullValue
}
func EqTimeTime(value time.Time) time.Time {
pegomock.RegisterMatcher(&pegomock.EqMatcher{Value: value})
var nullValue time.Time
return nullValue
}

View File

@@ -0,0 +1,99 @@
// Code generated by pegomock. DO NOT EDIT.
// Source: github.com/runatlantis/atlantis/server/events/locking (interfaces: ApplyLockChecker)
package mocks
import (
pegomock "github.com/petergtz/pegomock"
locking "github.com/runatlantis/atlantis/server/events/locking"
"reflect"
"time"
)
type MockApplyLockChecker struct {
fail func(message string, callerSkip ...int)
}
func NewMockApplyLockChecker(options ...pegomock.Option) *MockApplyLockChecker {
mock := &MockApplyLockChecker{}
for _, option := range options {
option.Apply(mock)
}
return mock
}
func (mock *MockApplyLockChecker) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockApplyLockChecker) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockApplyLockChecker) CheckApplyLock() (locking.ApplyCommandLock, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockApplyLockChecker().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("CheckApplyLock", params, []reflect.Type{reflect.TypeOf((*locking.ApplyCommandLock)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 locking.ApplyCommandLock
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(locking.ApplyCommandLock)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockApplyLockChecker) VerifyWasCalledOnce() *VerifierMockApplyLockChecker {
return &VerifierMockApplyLockChecker{
mock: mock,
invocationCountMatcher: pegomock.Times(1),
}
}
func (mock *MockApplyLockChecker) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockApplyLockChecker {
return &VerifierMockApplyLockChecker{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
}
}
func (mock *MockApplyLockChecker) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockApplyLockChecker {
return &VerifierMockApplyLockChecker{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
inOrderContext: inOrderContext,
}
}
func (mock *MockApplyLockChecker) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockApplyLockChecker {
return &VerifierMockApplyLockChecker{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
timeout: timeout,
}
}
type VerifierMockApplyLockChecker struct {
mock *MockApplyLockChecker
invocationCountMatcher pegomock.Matcher
inOrderContext *pegomock.InOrderContext
timeout time.Duration
}
func (verifier *VerifierMockApplyLockChecker) CheckApplyLock() *MockApplyLockChecker_CheckApplyLock_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckApplyLock", params, verifier.timeout)
return &MockApplyLockChecker_CheckApplyLock_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockApplyLockChecker_CheckApplyLock_OngoingVerification struct {
mock *MockApplyLockChecker
methodInvocations []pegomock.MethodInvocation
}
func (c *MockApplyLockChecker_CheckApplyLock_OngoingVerification) GetCapturedArguments() {
}
func (c *MockApplyLockChecker_CheckApplyLock_OngoingVerification) GetAllCapturedArguments() {
}

View File

@@ -0,0 +1,167 @@
// Code generated by pegomock. DO NOT EDIT.
// Source: github.com/runatlantis/atlantis/server/events/locking (interfaces: ApplyLocker)
package mocks
import (
pegomock "github.com/petergtz/pegomock"
locking "github.com/runatlantis/atlantis/server/events/locking"
"reflect"
"time"
)
type MockApplyLocker struct {
fail func(message string, callerSkip ...int)
}
func NewMockApplyLocker(options ...pegomock.Option) *MockApplyLocker {
mock := &MockApplyLocker{}
for _, option := range options {
option.Apply(mock)
}
return mock
}
func (mock *MockApplyLocker) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockApplyLocker) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockApplyLocker) LockApply() (locking.ApplyCommandLock, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockApplyLocker().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("LockApply", params, []reflect.Type{reflect.TypeOf((*locking.ApplyCommandLock)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 locking.ApplyCommandLock
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(locking.ApplyCommandLock)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockApplyLocker) UnlockApply() error {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockApplyLocker().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("UnlockApply", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
var ret0 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(error)
}
}
return ret0
}
func (mock *MockApplyLocker) CheckApplyLock() (locking.ApplyCommandLock, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockApplyLocker().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("CheckApplyLock", params, []reflect.Type{reflect.TypeOf((*locking.ApplyCommandLock)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 locking.ApplyCommandLock
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(locking.ApplyCommandLock)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockApplyLocker) VerifyWasCalledOnce() *VerifierMockApplyLocker {
return &VerifierMockApplyLocker{
mock: mock,
invocationCountMatcher: pegomock.Times(1),
}
}
func (mock *MockApplyLocker) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockApplyLocker {
return &VerifierMockApplyLocker{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
}
}
func (mock *MockApplyLocker) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockApplyLocker {
return &VerifierMockApplyLocker{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
inOrderContext: inOrderContext,
}
}
func (mock *MockApplyLocker) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockApplyLocker {
return &VerifierMockApplyLocker{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
timeout: timeout,
}
}
type VerifierMockApplyLocker struct {
mock *MockApplyLocker
invocationCountMatcher pegomock.Matcher
inOrderContext *pegomock.InOrderContext
timeout time.Duration
}
func (verifier *VerifierMockApplyLocker) LockApply() *MockApplyLocker_LockApply_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "LockApply", params, verifier.timeout)
return &MockApplyLocker_LockApply_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockApplyLocker_LockApply_OngoingVerification struct {
mock *MockApplyLocker
methodInvocations []pegomock.MethodInvocation
}
func (c *MockApplyLocker_LockApply_OngoingVerification) GetCapturedArguments() {
}
func (c *MockApplyLocker_LockApply_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockApplyLocker) UnlockApply() *MockApplyLocker_UnlockApply_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UnlockApply", params, verifier.timeout)
return &MockApplyLocker_UnlockApply_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockApplyLocker_UnlockApply_OngoingVerification struct {
mock *MockApplyLocker
methodInvocations []pegomock.MethodInvocation
}
func (c *MockApplyLocker_UnlockApply_OngoingVerification) GetCapturedArguments() {
}
func (c *MockApplyLocker_UnlockApply_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockApplyLocker) CheckApplyLock() *MockApplyLocker_CheckApplyLock_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckApplyLock", params, verifier.timeout)
return &MockApplyLocker_CheckApplyLock_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockApplyLocker_CheckApplyLock_OngoingVerification struct {
mock *MockApplyLocker
methodInvocations []pegomock.MethodInvocation
}
func (c *MockApplyLocker_CheckApplyLock_OngoingVerification) GetCapturedArguments() {
}
func (c *MockApplyLocker_CheckApplyLock_OngoingVerification) GetAllCapturedArguments() {
}

View File

@@ -124,6 +124,59 @@ func (mock *MockBackend) UnlockByPull(repoFullName string, pullNum int) ([]model
return ret0, ret1
}
func (mock *MockBackend) LockCommand(cmdName models.CommandName, lockTime time.Time) (*models.CommandLock, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockBackend().")
}
params := []pegomock.Param{cmdName, lockTime}
result := pegomock.GetGenericMockFrom(mock).Invoke("LockCommand", params, []reflect.Type{reflect.TypeOf((**models.CommandLock)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 *models.CommandLock
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(*models.CommandLock)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockBackend) UnlockCommand(cmdName models.CommandName) error {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockBackend().")
}
params := []pegomock.Param{cmdName}
result := pegomock.GetGenericMockFrom(mock).Invoke("UnlockCommand", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
var ret0 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(error)
}
}
return ret0
}
func (mock *MockBackend) CheckCommandLock(cmdName models.CommandName) (*models.CommandLock, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockBackend().")
}
params := []pegomock.Param{cmdName}
result := pegomock.GetGenericMockFrom(mock).Invoke("CheckCommandLock", params, []reflect.Type{reflect.TypeOf((**models.CommandLock)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 *models.CommandLock
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(*models.CommandLock)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockBackend) VerifyWasCalledOnce() *VerifierMockBackend {
return &VerifierMockBackend{
mock: mock,
@@ -297,3 +350,88 @@ func (c *MockBackend_UnlockByPull_OngoingVerification) GetAllCapturedArguments()
}
return
}
func (verifier *VerifierMockBackend) LockCommand(cmdName models.CommandName, lockTime time.Time) *MockBackend_LockCommand_OngoingVerification {
params := []pegomock.Param{cmdName, lockTime}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "LockCommand", params, verifier.timeout)
return &MockBackend_LockCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockBackend_LockCommand_OngoingVerification struct {
mock *MockBackend
methodInvocations []pegomock.MethodInvocation
}
func (c *MockBackend_LockCommand_OngoingVerification) GetCapturedArguments() (models.CommandName, time.Time) {
cmdName, lockTime := c.GetAllCapturedArguments()
return cmdName[len(cmdName)-1], lockTime[len(lockTime)-1]
}
func (c *MockBackend_LockCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []models.CommandName, _param1 []time.Time) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]models.CommandName, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(models.CommandName)
}
_param1 = make([]time.Time, len(c.methodInvocations))
for u, param := range params[1] {
_param1[u] = param.(time.Time)
}
}
return
}
func (verifier *VerifierMockBackend) UnlockCommand(cmdName models.CommandName) *MockBackend_UnlockCommand_OngoingVerification {
params := []pegomock.Param{cmdName}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UnlockCommand", params, verifier.timeout)
return &MockBackend_UnlockCommand_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockBackend_UnlockCommand_OngoingVerification struct {
mock *MockBackend
methodInvocations []pegomock.MethodInvocation
}
func (c *MockBackend_UnlockCommand_OngoingVerification) GetCapturedArguments() models.CommandName {
cmdName := c.GetAllCapturedArguments()
return cmdName[len(cmdName)-1]
}
func (c *MockBackend_UnlockCommand_OngoingVerification) GetAllCapturedArguments() (_param0 []models.CommandName) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]models.CommandName, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(models.CommandName)
}
}
return
}
func (verifier *VerifierMockBackend) CheckCommandLock(cmdName models.CommandName) *MockBackend_CheckCommandLock_OngoingVerification {
params := []pegomock.Param{cmdName}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckCommandLock", params, verifier.timeout)
return &MockBackend_CheckCommandLock_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockBackend_CheckCommandLock_OngoingVerification struct {
mock *MockBackend
methodInvocations []pegomock.MethodInvocation
}
func (c *MockBackend_CheckCommandLock_OngoingVerification) GetCapturedArguments() models.CommandName {
cmdName := c.GetAllCapturedArguments()
return cmdName[len(cmdName)-1]
}
func (c *MockBackend_CheckCommandLock_OngoingVerification) GetAllCapturedArguments() (_param0 []models.CommandName) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]models.CommandName, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(models.CommandName)
}
}
return
}

View File

@@ -0,0 +1,105 @@
// Code generated by pegomock. DO NOT EDIT.
// Source: github.com/runatlantis/atlantis/server/events (interfaces: ApplyCommandLocker)
package mocks
import (
pegomock "github.com/petergtz/pegomock"
events "github.com/runatlantis/atlantis/server/events"
"reflect"
"time"
)
type MockApplyCommandLocker struct {
fail func(message string, callerSkip ...int)
}
func NewMockApplyCommandLocker(options ...pegomock.Option) *MockApplyCommandLocker {
mock := &MockApplyCommandLocker{}
for _, option := range options {
option.Apply(mock)
}
return mock
}
func (mock *MockApplyCommandLocker) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockApplyCommandLocker) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockApplyCommandLocker) IsDisabled(ctx *events.CommandContext) bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockApplyCommandLocker().")
}
params := []pegomock.Param{ctx}
result := pegomock.GetGenericMockFrom(mock).Invoke("IsDisabled", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})
var ret0 bool
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(bool)
}
}
return ret0
}
func (mock *MockApplyCommandLocker) VerifyWasCalledOnce() *VerifierMockApplyCommandLocker {
return &VerifierMockApplyCommandLocker{
mock: mock,
invocationCountMatcher: pegomock.Times(1),
}
}
func (mock *MockApplyCommandLocker) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockApplyCommandLocker {
return &VerifierMockApplyCommandLocker{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
}
}
func (mock *MockApplyCommandLocker) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockApplyCommandLocker {
return &VerifierMockApplyCommandLocker{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
inOrderContext: inOrderContext,
}
}
func (mock *MockApplyCommandLocker) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockApplyCommandLocker {
return &VerifierMockApplyCommandLocker{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
timeout: timeout,
}
}
type VerifierMockApplyCommandLocker struct {
mock *MockApplyCommandLocker
invocationCountMatcher pegomock.Matcher
inOrderContext *pegomock.InOrderContext
timeout time.Duration
}
func (verifier *VerifierMockApplyCommandLocker) IsDisabled(ctx *events.CommandContext) *MockApplyCommandLocker_IsDisabled_OngoingVerification {
params := []pegomock.Param{ctx}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsDisabled", params, verifier.timeout)
return &MockApplyCommandLocker_IsDisabled_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockApplyCommandLocker_IsDisabled_OngoingVerification struct {
mock *MockApplyCommandLocker
methodInvocations []pegomock.MethodInvocation
}
func (c *MockApplyCommandLocker_IsDisabled_OngoingVerification) GetCapturedArguments() *events.CommandContext {
ctx := c.GetAllCapturedArguments()
return ctx[len(ctx)-1]
}
func (c *MockApplyCommandLocker_IsDisabled_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]*events.CommandContext, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(*events.CommandContext)
}
}
return
}

View File

@@ -206,6 +206,27 @@ type User struct {
Username string
}
// LockMetadata contains additional data provided to the lock
type LockMetadata struct {
UnixTime int64
}
// CommandLock represents a global lock for an atlantis command (plan, apply, policy_check).
// It is used to prevent commands from being executed
type CommandLock struct {
// Time is the time at which the lock was first created.
LockMetadata LockMetadata
CommandName CommandName
}
func (l *CommandLock) LockTime() time.Time {
return time.Unix(l.LockMetadata.UnixTime, 0)
}
func (l *CommandLock) IsLocked() bool {
return !l.LockTime().IsZero()
}
// ProjectLock represents a lock on a project.
type ProjectLock struct {
// Project is the project that is being locked.

View File

@@ -37,6 +37,9 @@ import (
. "github.com/runatlantis/atlantis/testing"
)
var applyLocker locking.ApplyLocker
var userConfig server.UserConfig
type NoopTFDownloader struct{}
var mockPreWorkflowHookRunner *runtimemocks.MockPreWorkflowHookRunner
@@ -73,6 +76,10 @@ func TestGitHubWorkflow(t *testing.T) {
ModifiedFiles []string
// Comments are what our mock user writes to the pull request.
Comments []string
// DisableApply flag used by userConfig object when initializing atlantis server.
DisableApply bool
// ApplyLock creates an apply lock that temporarily disables apply command
ApplyLock bool
// ExpAutomerge is true if we expect Atlantis to automerge.
ExpAutomerge bool
// ExpAutoplan is true if we expect Atlantis to autoplan.
@@ -336,12 +343,48 @@ func TestGitHubWorkflow(t *testing.T) {
{"exp-output-merge.txt"},
},
},
{
Description: "global apply lock disables apply commands",
RepoDir: "simple-yaml",
ModifiedFiles: []string{"main.tf"},
DisableApply: false,
ApplyLock: true,
ExpAutoplan: true,
Comments: []string{
"atlantis apply",
},
ExpReplies: [][]string{
{"exp-output-autoplan.txt"},
{"exp-output-apply-locked.txt"},
{"exp-output-merge.txt"},
},
},
{
Description: "disable apply flag always takes presedence",
RepoDir: "simple-yaml",
ModifiedFiles: []string{"main.tf"},
DisableApply: true,
ApplyLock: false,
ExpAutoplan: true,
Comments: []string{
"atlantis apply",
},
ExpReplies: [][]string{
{"exp-output-autoplan.txt"},
{"exp-output-apply-locked.txt"},
{"exp-output-merge.txt"},
},
},
}
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
RegisterMockTestingT(t)
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, false)
// reset userConfig
userConfig = server.UserConfig{}
userConfig.DisableApply = c.DisableApply
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir)
// Set the repo to be cloned through the testing backdoor.
repoDir, headSHA, cleanup := initializeRepo(t, c.RepoDir)
defer cleanup()
@@ -357,6 +400,11 @@ func TestGitHubWorkflow(t *testing.T) {
ctrl.Post(w, pullOpenedReq)
responseContains(t, w, 200, "Processing...")
// Create global apply lock if required
if c.ApplyLock {
_, _ = applyLocker.LockApply()
}
// Now send any other comments.
for _, comment := range c.Comments {
commentReq := GitHubCommentEvent(t, comment)
@@ -489,7 +537,11 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) {
t.Run(c.Description, func(t *testing.T) {
RegisterMockTestingT(t)
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, true)
// reset userConfig
userConfig = server.UserConfig{}
userConfig.EnablePolicyChecksFlag = true
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir)
// Set the repo to be cloned through the testing backdoor.
repoDir, headSHA, cleanup := initializeRepo(t, c.RepoDir)
defer cleanup()
@@ -559,14 +611,14 @@ func TestGitHubWorkflowWithPolicyCheck(t *testing.T) {
}
}
func setupE2E(t *testing.T, repoDir string, policyChecksEnabled bool) (server.EventsController, *vcsmocks.MockClient, *mocks.MockGithubPullGetter, *events.FileWorkspace) {
func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks.MockClient, *mocks.MockGithubPullGetter, *events.FileWorkspace) {
allowForkPRs := false
dataDir, binDir, cacheDir, cleanup := mkSubDirs(t)
defer cleanup()
//env vars
if policyChecksEnabled {
if userConfig.EnablePolicyChecksFlag {
// need this to be set or we'll fail the policy check step
os.Setenv(policy.DefaultConftestVersionEnvKey, "0.21.0")
}
@@ -594,6 +646,7 @@ func setupE2E(t *testing.T, repoDir string, policyChecksEnabled bool) (server.Ev
boltdb, err := db.New(dataDir)
Ok(t, err)
lockingClient := locking.NewClient(boltdb)
applyLocker = locking.NewApplyClient(boltdb, userConfig.DisableApply)
projectLocker := &events.DefaultProjectLocker{
Locker: lockingClient,
VCSClient: e2eVCSClient,
@@ -630,7 +683,7 @@ func setupE2E(t *testing.T, repoDir string, policyChecksEnabled bool) (server.Ev
PreWorkflowHookRunner: mockPreWorkflowHookRunner,
}
projectCommandBuilder := events.NewProjectCommandBuilder(
policyChecksEnabled,
userConfig.EnablePolicyChecksFlag,
parser,
&events.DefaultProjectFinder{},
e2eVCSClient,
@@ -729,7 +782,7 @@ func setupE2E(t *testing.T, repoDir string, policyChecksEnabled bool) (server.Ev
applyCommandRunner := events.NewApplyCommandRunner(
e2eVCSClient,
false,
false,
applyLocker,
e2eStatusUpdater,
projectCommandBuilder,
projectCommandRunner,

View File

@@ -20,6 +20,7 @@ type LocksController struct {
AtlantisVersion string
AtlantisURL *url.URL
Locker locking.Locker
ApplyLocker locking.ApplyLocker
Logger *logging.SimpleLogger
VCSClient vcs.Client
LockDetailTemplate TemplateWriter
@@ -29,6 +30,30 @@ type LocksController struct {
DeleteLockCommand events.DeleteLockCommand
}
// LockApply handles creating a global apply lock.
// If Lock already exists it will be a no-op
func (l *LocksController) LockApply(w http.ResponseWriter, r *http.Request) {
lock, err := l.ApplyLocker.LockApply()
if err != nil {
l.respond(w, logging.Error, http.StatusInternalServerError, "creating apply lock failed with: %s", err)
return
}
l.respond(w, logging.Info, http.StatusOK, "Apply Lock is acquired on %s", lock.Time.Format("2006-01-02 15:04:05"))
}
// UnlockApply handles releasing a global apply lock.
// If Lock doesn't exists it will be a no-op
func (l *LocksController) UnlockApply(w http.ResponseWriter, r *http.Request) {
err := l.ApplyLocker.UnlockApply()
if err != nil {
l.respond(w, logging.Error, http.StatusInternalServerError, "deleting apply lock failed with: %s", err)
return
}
l.respond(w, logging.Info, http.StatusOK, "Deleted apply lock")
}
// GetLock is the GET /locks/{id} route. It renders the lock detail view.
func (l *LocksController) GetLock(w http.ResponseWriter, r *http.Request) {
id, ok := mux.Vars(r)["id"]

View File

@@ -3,13 +3,16 @@ package server_test
import (
"bytes"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"time"
"github.com/runatlantis/atlantis/server/events/db"
"github.com/runatlantis/atlantis/server/events/locking"
"github.com/gorilla/mux"
. "github.com/petergtz/pegomock"
@@ -30,6 +33,84 @@ func AnyRepo() models.Repo {
return models.Repo{}
}
func TestCreateApplyLock(t *testing.T) {
t.Run("Creates apply lock", func(t *testing.T) {
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
w := httptest.NewRecorder()
layout := "2006-01-02T15:04:05.000Z"
strLockTime := "2020-09-01T00:45:26.371Z"
expLockTime := "2020-09-01 00:45:26"
lockTime, _ := time.Parse(layout, strLockTime)
l := mocks.NewMockApplyLocker()
When(l.LockApply()).ThenReturn(locking.ApplyCommandLock{
Locked: true,
Time: lockTime,
}, nil)
lc := server.LocksController{
Logger: logging.NewNoopLogger(),
ApplyLocker: l,
}
lc.LockApply(w, req)
responseContains(t, w, http.StatusOK, fmt.Sprintf("Apply Lock is acquired on %s", expLockTime))
})
t.Run("Apply lock creation fails", func(t *testing.T) {
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
w := httptest.NewRecorder()
l := mocks.NewMockApplyLocker()
When(l.LockApply()).ThenReturn(locking.ApplyCommandLock{
Locked: false,
}, errors.New("failed to acquire lock"))
lc := server.LocksController{
Logger: logging.NewNoopLogger(),
ApplyLocker: l,
}
lc.LockApply(w, req)
responseContains(t, w, http.StatusInternalServerError, fmt.Sprintf("creating apply lock failed with: failed to acquire lock"))
})
}
func TestUnlockApply(t *testing.T) {
t.Run("Apply lock deleted successfully", func(t *testing.T) {
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
w := httptest.NewRecorder()
l := mocks.NewMockApplyLocker()
When(l.UnlockApply()).ThenReturn(nil)
lc := server.LocksController{
Logger: logging.NewNoopLogger(),
ApplyLocker: l,
}
lc.UnlockApply(w, req)
responseContains(t, w, http.StatusOK, fmt.Sprintf("Deleted apply lock"))
})
t.Run("Apply lock deletion failed", func(t *testing.T) {
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
w := httptest.NewRecorder()
l := mocks.NewMockApplyLocker()
When(l.UnlockApply()).ThenReturn(errors.New("failed to delete lock"))
lc := server.LocksController{
Logger: logging.NewNoopLogger(),
ApplyLocker: l,
}
lc.UnlockApply(w, req)
responseContains(t, w, http.StatusInternalServerError, fmt.Sprintf("deleting apply lock failed with: failed to delete lock"))
})
}
func TestGetLockRoute_NoLockID(t *testing.T) {
t.Log("If there is no lock ID in the request then we should get a 400")
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))

View File

@@ -84,6 +84,7 @@ type Server struct {
CommandRunner *events.DefaultCommandRunner
Logger *logging.SimpleLogger
Locker locking.Locker
ApplyLocker locking.ApplyLocker
EventsController *EventsController
GithubAppController *GithubAppController
LocksController *LocksController
@@ -294,10 +295,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
return nil, err
}
var lockingClient locking.Locker
var applyLockingClient locking.ApplyLocker
if userConfig.DisableRepoLocking {
lockingClient = locking.NewNoOpLocker()
} else {
lockingClient = locking.NewClient(boltdb)
applyLockingClient = locking.NewApplyClient(boltdb, userConfig.DisableApply)
}
workingDirLocker := events.NewDefaultWorkingDirLocker()
@@ -501,7 +504,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
applyCommandRunner := events.NewApplyCommandRunner(
vcsClient,
userConfig.DisableApplyAll,
userConfig.DisableApply,
applyLockingClient,
commitStatusUpdater,
projectCommandBuilder,
projectCommandRunner,
@@ -556,6 +559,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
AtlantisVersion: config.AtlantisVersion,
AtlantisURL: parsedURL,
Locker: lockingClient,
ApplyLocker: applyLockingClient,
Logger: logger,
VCSClient: vcsClient,
LockDetailTemplate: lockTemplate,
@@ -601,6 +605,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
CommandRunner: commandRunner,
Logger: logger,
Locker: lockingClient,
ApplyLocker: applyLockingClient,
EventsController: eventsController,
GithubAppController: githubAppController,
LocksController: locksController,
@@ -624,6 +629,8 @@ func (s *Server) Start() error {
s.Router.HandleFunc("/events", s.EventsController.Post).Methods("POST")
s.Router.HandleFunc("/github-app/exchange-code", s.GithubAppController.ExchangeCode).Methods("GET")
s.Router.HandleFunc("/github-app/setup", s.GithubAppController.New).Methods("GET")
s.Router.HandleFunc("/apply/lock", s.LocksController.LockApply).Methods("POST").Queries()
s.Router.HandleFunc("/apply/unlock", s.LocksController.UnlockApply).Methods("DELETE").Queries()
s.Router.HandleFunc("/locks", s.LocksController.DeleteLock).Methods("DELETE").Queries("id", "{id:.*}")
s.Router.HandleFunc("/lock", s.LocksController.GetLock).Methods("GET").
Queries(LockViewRouteIDQueryParam, fmt.Sprintf("{%s}", LockViewRouteIDQueryParam)).Name(LockViewRouteName)
@@ -710,11 +717,25 @@ func (s *Server) Index(w http.ResponseWriter, _ *http.Request) {
})
}
applyCmdLock, err := s.ApplyLocker.CheckApplyLock()
s.Logger.Info("Apply Lock: %v", applyCmdLock)
if err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprintf(w, "Could not retrieve global apply lock: %s", err)
return
}
applyLockData := ApplyLockData{
Time: applyCmdLock.Time,
Locked: applyCmdLock.Locked,
TimeFormatted: applyCmdLock.Time.Format("02-01-2006 15:04:05"),
}
//Sort by date - newest to oldest.
sort.SliceStable(lockResults, func(i, j int) bool { return lockResults[i].Time.After(lockResults[j].Time) })
err = s.IndexTemplate.Execute(w, IndexData{
Locks: lockResults,
ApplyLock: applyLockData,
AtlantisVersion: s.AtlantisVersion,
CleanedBasePath: s.AtlantisURL.Path,
})

View File

@@ -76,6 +76,7 @@ func TestIndex_Success(t *testing.T) {
t.Log("Index should render the index template successfully.")
RegisterMockTestingT(t)
l := mocks.NewMockLocker()
al := mocks.NewMockApplyLocker()
// These are the locks that we expect to be rendered.
now := time.Now()
locks := map[string]models.ProjectLock{
@@ -100,6 +101,7 @@ func TestIndex_Success(t *testing.T) {
Ok(t, err)
s := server.Server{
Locker: l,
ApplyLocker: al,
IndexTemplate: it,
Router: r,
AtlantisVersion: atlantisVersion,
@@ -109,6 +111,11 @@ func TestIndex_Success(t *testing.T) {
w := httptest.NewRecorder()
s.Index(w, req)
it.VerifyWasCalledOnce().Execute(w, server.IndexData{
ApplyLock: server.ApplyLockData{
Locked: false,
Time: time.Time{},
TimeFormatted: "01-01-0001 00:00:00",
},
Locks: []server.LockIndexData{
{
LockPath: "/lock?id=lkysow%252Fatlantis-example%252F.%252Fdefault",

View File

@@ -0,0 +1 @@
**Error:** Running `atlantis apply` is disabled.

View File

@@ -40,9 +40,17 @@ type LockIndexData struct {
TimeFormatted string
}
// ApplyLockData holds the fields to display in the index view
type ApplyLockData struct {
Locked bool
Time time.Time
TimeFormatted string
}
// IndexData holds the data for rendering the index page
type IndexData struct {
Locks []LockIndexData
ApplyLock ApplyLockData
AtlantisVersion string
// CleanedBasePath is the path Atlantis is accessible at externally. If
// not using a path-based proxy, this will be an empty string. Never ends
@@ -80,11 +88,23 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
<p class="title-heading">atlantis</p>
<p class="js-discard-success"><strong>Plan discarded and unlocked!</strong></p>
</section>
<nav class="navbar">
<div class="container">
<section>
{{ if .ApplyLock.Locked }}
<div class="twelve center columns">
<h6><strong>Apply commands are disabled globally</strong></h6>
<h6><code>Lock Status</code>: <strong>Active</strong></h6>
<h6><code>Active Since</code>: <strong>{{ .ApplyLock.TimeFormatted }}</strong></h6>
<a class="button button-primary" id="applyUnlockPrompt">Enable Apply Commands</a>
</div>
</nav>
<div class="navbar-spacer"></div>
{{ else }}
<div class="twelve columns">
<h6><strong>Apply commands are enabled</strong></h6>
<a class="button button-primary" id="applyLockPrompt">Disable Apply Commands</a>
</div>
{{ end }}
</section>
<br>
<br>
<br>
<section>
<p class="title-heading small"><strong>Locks</strong></p>
@@ -103,10 +123,112 @@ var indexTemplate = template.Must(template.New("index.html.tmpl").Parse(`
<p class="placeholder">No locks found.</p>
{{ end }}
</section>
<div id="applyLockMessageModal" class="modal">
<!-- Modal content -->
<div class="modal-content">
<div class="modal-header">
<span class="close">&times;</span>
</div>
<div class="modal-body">
<p><strong>Are you sure you want to create a global apply lock? It will disable applies globally</strong></p>
<input class="button-primary" id="applyLockYes" type="submit" value="Yes">
<input type="button" class="cancel" value="Cancel">
</div>
</div>
</div>
<div id="applyUnlockMessageModal" class="modal">
<!-- Modal content -->
<div class="modal-content">
<div class="modal-header">
<span class="close">&times;</span>
</div>
<div class="modal-body">
<p><strong>Are you sure you want to release global apply lock?</strong></p>
<input class="button-primary" id="applyUnlockYes" type="submit" value="Yes">
<input type="button" class="cancel" value="Cancel">
</div>
</div>
</div>
</div>
<footer>
v{{ .AtlantisVersion }}
</footer>
<script>
function applyLockModalSetup(lockOrUnlock) {
// Get the modal
switch( lockOrUnlock ) {
case "lock":
var modal = $("#applyLockMessageModal");
var btn = $("#applyLockPrompt");
$("#applyLockYes").click(function() {
$.ajax({
url: '{{ .CleanedBasePath }}/apply/lock',
type: 'POST',
success: function(result) {
window.location.replace("{{ .CleanedBasePath }}/?discard=true");
}
});
});
break;
case "unlock":
var modal = $("#applyUnlockMessageModal");
var btn = $("#applyUnlockPrompt");
var btnApplyUnlock =
$("#applyUnlockYes").click(function() {
$.ajax({
url: '{{ .CleanedBasePath }}/apply/unlock',
type: 'DELETE',
success: function(result) {
window.location.replace("{{ .CleanedBasePath }}/?discard=true");
}
});
});
break;
default:
throw("unsupported command " + lockOrUnlock)
}
return [modal, btn];
}
{{ if .ApplyLock.Locked }}
var [modal, btn] = applyLockModalSetup("unlock");
{{ else }}
var [modal, btn] = applyLockModalSetup("lock");
{{ end }}
// Get the <span> element that closes the modal
// using document.getElementsByClassName since jquery $("close") doesn't seem to work for btn click events
var span = document.getElementsByClassName("close")[0];
var cancelBtn = document.getElementsByClassName("cancel")[0];
// When the user clicks the button, open the modal
btn.click(function() {
modal.css("display", "block");
});
// When the user clicks on <span> (x), close the modal
span.onclick = function() {
modal.css("display", "none");
}
cancelBtn.onclick = function() {
modal.css("display", "none");
}
// When the user clicks anywhere outside of the modal, close it
window.onclick = function(event) {
if (event.target == modal) {
modal.css("display", "none");
}
}
</script>
</body>
</html>
`))
@@ -185,7 +307,7 @@ v{{ .AtlantisVersion }}
<script>
// Get the modal
var modal = $("#discardMessageModal");
// Get the button that opens the modal
var btn = $("#discardPlanUnlock");
var btnDiscard = $("#discardYes");
@@ -196,7 +318,7 @@ v{{ .AtlantisVersion }}
var span = document.getElementsByClassName("close")[0];
var cancelBtn = document.getElementsByClassName("cancel")[0];
// When the user clicks the button, open the modal
// When the user clicks the button, open the modal
btn.click(function() {
modal.css("display", "block");
});