Skip to content

Commit ad76385

Browse files
authored
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
1 parent ca976d8 commit ad76385

25 files changed

+1460
-49
lines changed

server/events/apply_command_runner.go

+19-4
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ package events
22

33
import (
44
"github.com/runatlantis/atlantis/server/events/db"
5+
"github.com/runatlantis/atlantis/server/events/locking"
56
"github.com/runatlantis/atlantis/server/events/models"
67
"github.com/runatlantis/atlantis/server/events/vcs"
78
)
89

910
func NewApplyCommandRunner(
1011
vcsClient vcs.Client,
1112
disableApplyAll bool,
12-
disableApply bool,
13+
applyCommandLocker locking.ApplyLockChecker,
1314
commitStatusUpdater CommitStatusUpdater,
1415
prjCommandBuilder ProjectApplyCommandBuilder,
1516
prjCmdRunner ProjectApplyCommandRunner,
@@ -22,7 +23,7 @@ func NewApplyCommandRunner(
2223
return &ApplyCommandRunner{
2324
vcsClient: vcsClient,
2425
DisableApplyAll: disableApplyAll,
25-
DisableApply: disableApply,
26+
locker: applyCommandLocker,
2627
commitStatusUpdater: commitStatusUpdater,
2728
prjCmdBuilder: prjCommandBuilder,
2829
prjCmdRunner: prjCmdRunner,
@@ -36,8 +37,8 @@ func NewApplyCommandRunner(
3637

3738
type ApplyCommandRunner struct {
3839
DisableApplyAll bool
39-
DisableApply bool
4040
DB *db.BoltDB
41+
locker locking.ApplyLockChecker
4142
vcsClient vcs.Client
4243
commitStatusUpdater CommitStatusUpdater
4344
prjCmdBuilder ProjectApplyCommandBuilder
@@ -53,7 +54,15 @@ func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {
5354
baseRepo := ctx.Pull.BaseRepo
5455
pull := ctx.Pull
5556

56-
if a.DisableApply {
57+
locked, err := a.IsLocked()
58+
// CheckApplyLock falls back to DisableApply flag if fetching the lock
59+
// raises an erro r
60+
// We will log failure as warning
61+
if err != nil {
62+
ctx.Log.Warn("checking global apply lock: %s", err)
63+
}
64+
65+
if locked {
5766
ctx.Log.Info("ignoring apply command since apply disabled globally")
5867
if err := a.vcsClient.CreateComment(baseRepo, pull.Num, applyDisabledComment, models.ApplyCommand.String()); err != nil {
5968
ctx.Log.Err("unable to comment on pull request: %s", err)
@@ -135,6 +144,12 @@ func (a *ApplyCommandRunner) Run(ctx *CommandContext, cmd *CommentCommand) {
135144
}
136145
}
137146

147+
func (a *ApplyCommandRunner) IsLocked() (bool, error) {
148+
lock, err := a.locker.CheckApplyLock()
149+
150+
return lock.Locked, err
151+
}
152+
138153
func (a *ApplyCommandRunner) isParallelEnabled(projectCmds []models.ProjectCommandContext) bool {
139154
return len(projectCmds) > 0 && projectCmds[0].ParallelApplyEnabled
140155
}
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package events_test
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/google/go-github/v31/github"
8+
. "github.com/petergtz/pegomock"
9+
"github.com/runatlantis/atlantis/server/events"
10+
"github.com/runatlantis/atlantis/server/events/locking"
11+
"github.com/runatlantis/atlantis/server/events/models"
12+
"github.com/runatlantis/atlantis/server/events/models/fixtures"
13+
)
14+
15+
func TestApplyCommandRunner_IsLocked(t *testing.T) {
16+
RegisterMockTestingT(t)
17+
18+
cases := []struct {
19+
Description string
20+
ApplyLocked bool
21+
ApplyLockError error
22+
ExpComment string
23+
}{
24+
{
25+
Description: "When global apply lock is present IsDisabled returns true",
26+
ApplyLocked: true,
27+
ApplyLockError: nil,
28+
ExpComment: "**Error:** Running `atlantis apply` is disabled.",
29+
},
30+
{
31+
Description: "When no global apply lock is present and DisableApply flag is false IsDisabled returns false",
32+
ApplyLocked: false,
33+
ApplyLockError: nil,
34+
ExpComment: "Ran Apply for 0 projects:\n\n\n\n",
35+
},
36+
{
37+
Description: "If ApplyLockChecker returns an error IsDisabled return value of DisableApply flag",
38+
ApplyLockError: errors.New("error"),
39+
ApplyLocked: false,
40+
ExpComment: "Ran Apply for 0 projects:\n\n\n\n",
41+
},
42+
}
43+
44+
for _, c := range cases {
45+
t.Run(c.Description, func(t *testing.T) {
46+
vcsClient := setup(t)
47+
48+
pull := &github.PullRequest{
49+
State: github.String("open"),
50+
}
51+
modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState, Num: fixtures.Pull.Num}
52+
When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil)
53+
When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil)
54+
55+
ctx := &events.CommandContext{
56+
User: fixtures.User,
57+
Log: noopLogger,
58+
Pull: modelPull,
59+
HeadRepo: fixtures.GithubRepo,
60+
Trigger: events.Comment,
61+
}
62+
63+
When(applyLockChecker.CheckApplyLock()).ThenReturn(locking.ApplyCommandLock{Locked: c.ApplyLocked}, c.ApplyLockError)
64+
applyCommandRunner.Run(ctx, &events.CommentCommand{Name: models.ApplyCommand})
65+
66+
vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, c.ExpComment, "apply")
67+
})
68+
}
69+
}

server/events/command_runner_test.go

+4-18
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/google/go-github/v31/github"
2727
. "github.com/petergtz/pegomock"
2828
"github.com/runatlantis/atlantis/server/events"
29+
lockingmocks "github.com/runatlantis/atlantis/server/events/locking/mocks"
2930
"github.com/runatlantis/atlantis/server/events/mocks"
3031
eventmocks "github.com/runatlantis/atlantis/server/events/mocks"
3132
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
@@ -59,6 +60,7 @@ var autoMerger *events.AutoMerger
5960
var policyCheckCommandRunner *events.PolicyCheckCommandRunner
6061
var approvePoliciesCommandRunner *events.ApprovePoliciesCommandRunner
6162
var planCommandRunner *events.PlanCommandRunner
63+
var applyLockChecker *lockingmocks.MockApplyLockChecker
6264
var applyCommandRunner *events.ApplyCommandRunner
6365
var unlockCommandRunner *events.UnlockCommandRunner
6466
var preWorkflowHooksCommandRunner events.PreWorkflowHooksCommandRunner
@@ -85,6 +87,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
8587

8688
drainer = &events.Drainer{}
8789
deleteLockCommand = eventmocks.NewMockDeleteLockCommand()
90+
applyLockChecker = lockingmocks.NewMockApplyLockChecker()
8891
When(logger.GetLevel()).ThenReturn(logging.Info)
8992
When(logger.NewLogger("runatlantis/atlantis#1", true, logging.Info)).
9093
ThenReturn(pullLogger)
@@ -131,7 +134,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
131134
applyCommandRunner = events.NewApplyCommandRunner(
132135
vcsClient,
133136
false,
134-
false,
137+
applyLockChecker,
135138
commitUpdater,
136139
projectCommandBuilder,
137140
projectCommandRunner,
@@ -292,23 +295,6 @@ func TestRunCommentCommand_DisableApplyAllDisabled(t *testing.T) {
292295
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")
293296
}
294297

295-
func TestRunCommentCommand_ApplyDisabled(t *testing.T) {
296-
t.Log("if \"atlantis apply\" is run and this is disabled globally atlantis should" +
297-
" comment saying that this is not allowed")
298-
vcsClient := setup(t)
299-
applyCommandRunner.DisableApply = true
300-
defer func() { applyCommandRunner.DisableApply = false }()
301-
pull := &github.PullRequest{
302-
State: github.String("open"),
303-
}
304-
modelPull := models.PullRequest{BaseRepo: fixtures.GithubRepo, State: models.OpenPullState, Num: fixtures.Pull.Num}
305-
When(githubGetter.GetPullRequest(fixtures.GithubRepo, fixtures.Pull.Num)).ThenReturn(pull, nil)
306-
When(eventParsing.ParseGithubPull(pull)).ThenReturn(modelPull, modelPull.BaseRepo, fixtures.GithubRepo, nil)
307-
308-
ch.RunCommentCommand(fixtures.GithubRepo, nil, nil, fixtures.User, modelPull.Num, &events.CommentCommand{Name: models.ApplyCommand})
309-
vcsClient.VerifyWasCalledOnce().CreateComment(fixtures.GithubRepo, modelPull.Num, "**Error:** Running `atlantis apply` is disabled.", "apply")
310-
}
311-
312298
func TestRunCommentCommand_DisableDisableAutoplan(t *testing.T) {
313299
t.Log("if \"DisableAutoplan is true\" are disabled and we are silencing return and do not comment with error")
314300
setup(t)

server/events/db/boltdb.go

+109-9
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ import (
1717

1818
// BoltDB is a database using BoltDB
1919
type BoltDB struct {
20-
db *bolt.DB
21-
locksBucketName []byte
22-
pullsBucketName []byte
20+
db *bolt.DB
21+
locksBucketName []byte
22+
pullsBucketName []byte
23+
globalLocksBucketName []byte
2324
}
2425

2526
const (
26-
locksBucketName = "runLocks"
27-
pullsBucketName = "pulls"
28-
pullKeySeparator = "::"
27+
locksBucketName = "runLocks"
28+
pullsBucketName = "pulls"
29+
globalLocksBucketName = "globalLocks"
30+
pullKeySeparator = "::"
2931
)
3032

3133
// New returns a valid locker. We need to be able to write to dataDir
@@ -50,18 +52,31 @@ func New(dataDir string) (*BoltDB, error) {
5052
if _, err = tx.CreateBucketIfNotExists([]byte(pullsBucketName)); err != nil {
5153
return errors.Wrapf(err, "creating bucket %q", pullsBucketName)
5254
}
55+
if _, err = tx.CreateBucketIfNotExists([]byte(globalLocksBucketName)); err != nil {
56+
return errors.Wrapf(err, "creating bucket %q", globalLocksBucketName)
57+
}
5358
return nil
5459
})
5560
if err != nil {
5661
return nil, errors.Wrap(err, "starting BoltDB")
5762
}
5863
// todo: close BoltDB when server is sigtermed
59-
return &BoltDB{db: db, locksBucketName: []byte(locksBucketName), pullsBucketName: []byte(pullsBucketName)}, nil
64+
return &BoltDB{
65+
db: db,
66+
locksBucketName: []byte(locksBucketName),
67+
pullsBucketName: []byte(pullsBucketName),
68+
globalLocksBucketName: []byte(globalLocksBucketName),
69+
}, nil
6070
}
6171

6272
// NewWithDB is used for testing.
63-
func NewWithDB(db *bolt.DB, bucket string) (*BoltDB, error) {
64-
return &BoltDB{db: db, locksBucketName: []byte(bucket), pullsBucketName: []byte(pullsBucketName)}, nil
73+
func NewWithDB(db *bolt.DB, bucket string, globalBucket string) (*BoltDB, error) {
74+
return &BoltDB{
75+
db: db,
76+
locksBucketName: []byte(bucket),
77+
pullsBucketName: []byte(pullsBucketName),
78+
globalLocksBucketName: []byte(globalBucket),
79+
}, nil
6580
}
6681

6782
// TryLock attempts to create a new lock. If the lock is
@@ -155,6 +170,87 @@ func (b *BoltDB) List() ([]models.ProjectLock, error) {
155170
return locks, nil
156171
}
157172

173+
// LockCommand attempts to create a new lock for a CommandName.
174+
// If the lock doesn't exists, it will create a lock and return a pointer to it.
175+
// If the lock already exists, it will return an "lock already exists" error
176+
func (b *BoltDB) LockCommand(cmdName models.CommandName, lockTime time.Time) (*models.CommandLock, error) {
177+
lock := models.CommandLock{
178+
CommandName: cmdName,
179+
LockMetadata: models.LockMetadata{
180+
UnixTime: lockTime.Unix(),
181+
},
182+
}
183+
184+
newLockSerialized, _ := json.Marshal(lock)
185+
transactionErr := b.db.Update(func(tx *bolt.Tx) error {
186+
bucket := tx.Bucket(b.globalLocksBucketName)
187+
188+
currLockSerialized := bucket.Get([]byte(b.commandLockKey(cmdName)))
189+
if currLockSerialized != nil {
190+
return errors.New("lock already exists")
191+
}
192+
193+
// This will only error on readonly buckets, it's okay to ignore.
194+
bucket.Put([]byte(b.commandLockKey(cmdName)), newLockSerialized) // nolint: errcheck
195+
return nil
196+
})
197+
198+
if transactionErr != nil {
199+
return nil, errors.Wrap(transactionErr, "db transaction failed")
200+
}
201+
202+
return &lock, nil
203+
}
204+
205+
// UnlockCommand removes CommandName lock if present.
206+
// If there are no lock it returns an error.
207+
func (b *BoltDB) UnlockCommand(cmdName models.CommandName) error {
208+
transactionErr := b.db.Update(func(tx *bolt.Tx) error {
209+
bucket := tx.Bucket(b.globalLocksBucketName)
210+
211+
if l := bucket.Get([]byte(b.commandLockKey(cmdName))); l == nil {
212+
return errors.New("no lock exists")
213+
}
214+
215+
return bucket.Delete([]byte(b.commandLockKey(cmdName)))
216+
})
217+
218+
if transactionErr != nil {
219+
return errors.Wrap(transactionErr, "db transaction failed")
220+
}
221+
222+
return nil
223+
}
224+
225+
// CheckCommandLock checks if CommandName lock was set.
226+
// If the lock exists return the pointer to the lock object, otherwise return nil
227+
func (b *BoltDB) CheckCommandLock(cmdName models.CommandName) (*models.CommandLock, error) {
228+
cmdLock := models.CommandLock{}
229+
230+
found := false
231+
232+
err := b.db.View(func(tx *bolt.Tx) error {
233+
bucket := tx.Bucket(b.globalLocksBucketName)
234+
235+
serializedLock := bucket.Get([]byte(b.commandLockKey(cmdName)))
236+
237+
if serializedLock != nil {
238+
if err := json.Unmarshal(serializedLock, &cmdLock); err != nil {
239+
return errors.Wrap(err, "failed to deserialize UserConfig")
240+
}
241+
found = true
242+
}
243+
244+
return nil
245+
})
246+
247+
if found {
248+
return &cmdLock, err
249+
}
250+
251+
return nil, err
252+
}
253+
158254
// UnlockByPull deletes all locks associated with that pull request and returns them.
159255
func (b *BoltDB) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {
160256
var locks []models.ProjectLock
@@ -355,6 +451,10 @@ func (b *BoltDB) pullKey(pull models.PullRequest) ([]byte, error) {
355451
nil
356452
}
357453

454+
func (b *BoltDB) commandLockKey(cmdName models.CommandName) string {
455+
return fmt.Sprintf("%s/lock", cmdName)
456+
}
457+
358458
func (b *BoltDB) lockKey(p models.Project, workspace string) string {
359459
return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, workspace)
360460
}

0 commit comments

Comments
 (0)