mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-28 22:28:25 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
69
server/events/apply_command_runner_test.go
Normal file
69
server/events/apply_command_runner_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
113
server/events/locking/apply_locking.go
Normal file
113
server/events/locking/apply_locking.go
Normal 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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
20
server/events/locking/mocks/matchers/models_commandlock.go
Normal file
20
server/events/locking/mocks/matchers/models_commandlock.go
Normal 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
|
||||
}
|
||||
20
server/events/locking/mocks/matchers/models_commandname.go
Normal file
20
server/events/locking/mocks/matchers/models_commandname.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
20
server/events/locking/mocks/matchers/time_time.go
Normal file
20
server/events/locking/mocks/matchers/time_time.go
Normal 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
|
||||
}
|
||||
99
server/events/locking/mocks/mock_apply_lock_checker.go
Normal file
99
server/events/locking/mocks/mock_apply_lock_checker.go
Normal 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() {
|
||||
}
|
||||
167
server/events/locking/mocks/mock_apply_locker.go
Normal file
167
server/events/locking/mocks/mock_apply_locker.go
Normal 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() {
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
105
server/events/mocks/mock_apply_command_locker.go
Normal file
105
server/events/mocks/mock_apply_command_locker.go
Normal 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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
**Error:** Running `atlantis apply` is disabled.
|
||||
@@ -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">×</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">×</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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user