-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Orca 679 global atlantis lock new release branch #49
Merged
msarvar
merged 12 commits into
release-v0.17.0-beta-lyft.1
from
ORCA-679-global-atlantis-lock-new-release-branch
Mar 12, 2021
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
892b52a
Adding CommandLocker to boltDB and exposing it through locker interface
msarvar 3e2dff3
Apply lock ui and apply command lock controller
msarvar 20b53c4
Minor comments
msarvar f4b6268
Adding more tests and refactorinng
msarvar 8206184
Linting fixes
msarvar 888a2b7
creating applyLockingClient variable to fix interface error
msarvar 02f375c
nullsink for stats
msarvar e225a63
Addressing PR comments
msarvar 6ab3f62
fixing e2e tests
msarvar 9e6473d
linting fix fml
msarvar edb49da
Update outdated function descriptions
msarvar fac637e
revert stats sink changes
msarvar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package events_test | ||
|
||
import ( | ||
"errors" | ||
"testing" | ||
|
||
"github.com/google/go-github/v31/github" | ||
stats "github.com/lyft/gostats" | ||
. "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) | ||
|
||
scopeNull := stats.NewStore(stats.NewNullSink(), 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) | ||
|
||
ctx := &events.CommandContext{ | ||
User: fixtures.User, | ||
Log: noopLogger, | ||
Pull: modelPull, | ||
HeadRepo: fixtures.GithubRepo, | ||
Trigger: events.Comment, | ||
Scope: scopeNull, | ||
} | ||
|
||
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") | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to make this idempotent? Personally, would rather error out if the lock exists. Think about TFE returning a 409 in a similar situation. |
||
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) | ||
} | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The applyDisabledComment is very generic, would be good to think about how to make that more useful depending on how the apply command lock is configured. Not in scope for this PR though.