mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-07 11:58:32 +00:00
Add Apache 2 license and Hootsuite copyright to all golang files. Add a disclaimer that the files have been modified hereafter by contributors to this repo in order to abide by the Apache 2 license requirements.
224 lines
7.1 KiB
Go
224 lines
7.1 KiB
Go
// Copyright 2017 HootSuite Media Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the License);
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an AS IS BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// Modified hereafter by contributors to runatlantis/atlantis.
|
|
//
|
|
// Package boltdb provides a locking implementation using Bolt.
|
|
// Bolt is a key/value store that writes all data to a file.
|
|
// See https://github.com/boltdb/bolt for more information.
|
|
package boltdb
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/boltdb/bolt"
|
|
"github.com/pkg/errors"
|
|
"github.com/runatlantis/atlantis/server/events/models"
|
|
)
|
|
|
|
// BoltLocker is a locking backend using BoltDB
|
|
type BoltLocker struct {
|
|
db *bolt.DB
|
|
bucket []byte
|
|
}
|
|
|
|
const bucketName = "runLocks"
|
|
|
|
// New returns a valid locker. We need to be able to write to dataDir
|
|
// since bolt stores its data as a file
|
|
func New(dataDir string) (*BoltLocker, error) {
|
|
if err := os.MkdirAll(dataDir, 0700); err != nil {
|
|
return nil, errors.Wrap(err, "creating data dir")
|
|
}
|
|
db, err := bolt.Open(path.Join(dataDir, "atlantis.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
|
|
if err != nil {
|
|
if err.Error() == "timeout" {
|
|
return nil, errors.New("starting BoltDB: timeout (a possible cause is another Atlantis instance already running)")
|
|
}
|
|
return nil, errors.Wrap(err, "starting BoltDB")
|
|
}
|
|
err = db.Update(func(tx *bolt.Tx) error {
|
|
if _, err = tx.CreateBucketIfNotExists([]byte(bucketName)); err != nil {
|
|
return errors.Wrapf(err, "creating %q bucketName", bucketName)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "starting BoltDB")
|
|
}
|
|
// todo: close BoltDB when server is sigtermed
|
|
return &BoltLocker{db, []byte(bucketName)}, nil
|
|
}
|
|
|
|
// NewWithDB is used for testing.
|
|
func NewWithDB(db *bolt.DB, bucket string) (*BoltLocker, error) {
|
|
return &BoltLocker{db, []byte(bucket)}, nil
|
|
}
|
|
|
|
// TryLock attempts to create a new lock. If the lock is
|
|
// acquired, it will return true and the lock returned will be newLock.
|
|
// If the lock is not acquired, it will return false and the current
|
|
// lock that is preventing this lock from being acquired.
|
|
func (b *BoltLocker) TryLock(newLock models.ProjectLock) (bool, models.ProjectLock, error) {
|
|
var lockAcquired bool
|
|
var currLock models.ProjectLock
|
|
key := b.key(newLock.Project, newLock.Workspace)
|
|
newLockSerialized, _ := json.Marshal(newLock)
|
|
transactionErr := b.db.Update(func(tx *bolt.Tx) error {
|
|
bucket := tx.Bucket(b.bucket)
|
|
|
|
// if there is no run at that key then we're free to create the lock
|
|
currLockSerialized := bucket.Get([]byte(key))
|
|
if currLockSerialized == nil {
|
|
// This will only error on readonly buckets, it's okay to ignore.
|
|
bucket.Put([]byte(key), newLockSerialized) // nolint: errcheck
|
|
lockAcquired = true
|
|
currLock = newLock
|
|
return nil
|
|
}
|
|
|
|
// otherwise the lock fails, return to caller the run that's holding the lock
|
|
if err := json.Unmarshal(currLockSerialized, &currLock); err != nil {
|
|
return errors.Wrap(err, "failed to deserialize current lock")
|
|
}
|
|
lockAcquired = false
|
|
return nil
|
|
})
|
|
|
|
if transactionErr != nil {
|
|
return false, currLock, errors.Wrap(transactionErr, "DB transaction failed")
|
|
}
|
|
|
|
return lockAcquired, currLock, nil
|
|
}
|
|
|
|
// Unlock attempts to unlock the project and workspace.
|
|
// If there is no lock, then it will return a nil pointer.
|
|
// If there is a lock, then it will delete it, and then return a pointer
|
|
// to the deleted lock.
|
|
func (b BoltLocker) Unlock(p models.Project, workspace string) (*models.ProjectLock, error) {
|
|
var lock models.ProjectLock
|
|
foundLock := false
|
|
key := b.key(p, workspace)
|
|
err := b.db.Update(func(tx *bolt.Tx) error {
|
|
bucket := tx.Bucket(b.bucket)
|
|
serialized := bucket.Get([]byte(key))
|
|
if serialized != nil {
|
|
if err := json.Unmarshal(serialized, &lock); err != nil {
|
|
return errors.Wrap(err, "failed to deserialize lock")
|
|
}
|
|
foundLock = true
|
|
}
|
|
return bucket.Delete([]byte(key))
|
|
})
|
|
err = errors.Wrap(err, "DB transaction failed")
|
|
if foundLock {
|
|
return &lock, err
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// List lists all current locks.
|
|
func (b BoltLocker) List() ([]models.ProjectLock, error) {
|
|
var locks []models.ProjectLock
|
|
var locksBytes [][]byte
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
bucket := tx.Bucket(b.bucket)
|
|
c := bucket.Cursor()
|
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
|
locksBytes = append(locksBytes, v)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return locks, errors.Wrap(err, "DB transaction failed")
|
|
}
|
|
|
|
// deserialize bytes into the proper objects
|
|
for k, v := range locksBytes {
|
|
var lock models.ProjectLock
|
|
if err := json.Unmarshal(v, &lock); err != nil {
|
|
return locks, errors.Wrap(err, fmt.Sprintf("failed to deserialize lock at key %q", string(k)))
|
|
}
|
|
locks = append(locks, lock)
|
|
}
|
|
|
|
return locks, nil
|
|
}
|
|
|
|
// UnlockByPull deletes all locks associated with that pull request and returns them.
|
|
func (b BoltLocker) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {
|
|
var locks []models.ProjectLock
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
c := tx.Bucket(b.bucket).Cursor()
|
|
|
|
// we can use the repoFullName as a prefix search since that's the first part of the key
|
|
for k, v := c.Seek([]byte(repoFullName)); k != nil && bytes.HasPrefix(k, []byte(repoFullName)); k, v = c.Next() {
|
|
var lock models.ProjectLock
|
|
if err := json.Unmarshal(v, &lock); err != nil {
|
|
return errors.Wrapf(err, "deserializing lock at key %q", string(k))
|
|
}
|
|
if lock.Pull.Num == pullNum {
|
|
locks = append(locks, lock)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return locks, err
|
|
}
|
|
|
|
// delete the locks
|
|
for _, lock := range locks {
|
|
if _, err = b.Unlock(lock.Project, lock.Workspace); err != nil {
|
|
return locks, errors.Wrapf(err, "unlocking repo %s, path %s, workspace %s", lock.Project.RepoFullName, lock.Project.Path, lock.Workspace)
|
|
}
|
|
}
|
|
return locks, nil
|
|
}
|
|
|
|
// GetLock returns a pointer to the lock for that project and workspace.
|
|
// If there is no lock, it returns a nil pointer.
|
|
func (b BoltLocker) GetLock(p models.Project, workspace string) (*models.ProjectLock, error) {
|
|
key := b.key(p, workspace)
|
|
var lockBytes []byte
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket(b.bucket)
|
|
lockBytes = b.Get([]byte(key))
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting lock data")
|
|
}
|
|
// lockBytes will be nil if there was no data at that key
|
|
if lockBytes == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var lock models.ProjectLock
|
|
if err := json.Unmarshal(lockBytes, &lock); err != nil {
|
|
return nil, errors.Wrapf(err, "deserializing lock at key %q", key)
|
|
}
|
|
|
|
// need to set it to Local after deserialization due to https://github.com/golang/go/issues/19486
|
|
lock.Time = lock.Time.Local()
|
|
return &lock, nil
|
|
}
|
|
|
|
func (b BoltLocker) key(p models.Project, workspace string) string {
|
|
return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, workspace)
|
|
}
|