mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-06 05:58:28 +00:00
* set version in one place * rename ExecutionContext to CommandContext * delete unneeded PullRequestContext since it just duplicated what is in CommandContext * split fields inside CommandContext into Repo, PullRequest, and User models * clean up unused fields * remove base_executor stuff which was trying to do inheritance in go and didn't get us anywhere * clean up unused code and assets * created a locking.Client that creates the Keys that are used on the front-end. This allows the backends to store the locks any way they like as long as they support the interface
178 lines
5.2 KiB
Go
178 lines
5.2 KiB
Go
package dynamodb
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/client"
|
|
"github.com/aws/aws-sdk-go/service/dynamodb"
|
|
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
|
|
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
|
|
"github.com/hootsuite/atlantis/models"
|
|
"github.com/pkg/errors"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
type Backend struct {
|
|
DB dynamodbiface.DynamoDBAPI
|
|
LockTable string
|
|
}
|
|
|
|
// dynamoLock duplicates the fields of models.ProjectLock and adds LocksKey
|
|
// so everything is a top-level field for serialization and then querying
|
|
// in DynamodB and also so any changes to models.ProjectLock won't affect
|
|
// how we're storing our data or will at least cause a compile error
|
|
type dynamoLock struct {
|
|
LockKey string
|
|
RepoFullName string
|
|
Path string
|
|
PullNum int
|
|
Env string
|
|
Time time.Time
|
|
}
|
|
|
|
func New(lockTable string, p client.ConfigProvider) Backend {
|
|
return Backend{
|
|
DB: dynamodb.New(p),
|
|
LockTable: lockTable,
|
|
}
|
|
}
|
|
|
|
func (b Backend) key(project models.Project, env string) string {
|
|
return fmt.Sprintf("%s/%s/%s", project.RepoFullName, project.Path, env)
|
|
}
|
|
|
|
func (b Backend) TryLock(project models.Project, env string, pullNum int) (bool, int, error) {
|
|
key := b.key(project, env)
|
|
newDynamoLock := dynamoLock{
|
|
LockKey: key,
|
|
RepoFullName: project.RepoFullName,
|
|
Path: project.Path,
|
|
PullNum: pullNum,
|
|
Env: env,
|
|
Time: time.Now(),
|
|
}
|
|
newLockSerialized, err := dynamodbattribute.MarshalMap(newDynamoLock)
|
|
if err != nil {
|
|
return false, 0, errors.Wrap(err, "serializing")
|
|
}
|
|
|
|
// check if there is an existing lock
|
|
getItemParams := &dynamodb.GetItemInput{
|
|
Key: map[string]*dynamodb.AttributeValue{
|
|
"LockKey": {
|
|
S: aws.String(key),
|
|
},
|
|
},
|
|
TableName: aws.String(b.LockTable),
|
|
ConsistentRead: aws.Bool(true),
|
|
}
|
|
item, err := b.DB.GetItem(getItemParams)
|
|
if err != nil {
|
|
return false, 0, errors.Wrap(err, "checking if lock exists")
|
|
}
|
|
|
|
// if there is already a lock then we can't acquire a lock. Return the existing lock
|
|
var currLock dynamoLock
|
|
if len(item.Item) != 0 {
|
|
if err := dynamodbattribute.UnmarshalMap(item.Item, &currLock); err != nil {
|
|
return false, 0, errors.Wrap(err, "found an existing lock at that key but it could not be deserialized. We suggest manually deleting this key from DynamoDB")
|
|
}
|
|
return false, currLock.PullNum, nil
|
|
}
|
|
|
|
// else we should be able to lock
|
|
putItem := &dynamodb.PutItemInput{
|
|
Item: newLockSerialized,
|
|
TableName: aws.String(b.LockTable),
|
|
// this will ensure that we don't insert the new item in a race situation
|
|
// where someone has written this key just after our read
|
|
ConditionExpression: aws.String("attribute_not_exists(LockKey)"),
|
|
}
|
|
if _, err := b.DB.PutItem(putItem); err != nil {
|
|
return false, 0, errors.Wrap(err, "writing lock")
|
|
}
|
|
return true, pullNum, nil
|
|
}
|
|
|
|
func (b Backend) Unlock(project models.Project, env string) error {
|
|
key := b.key(project, env)
|
|
params := &dynamodb.DeleteItemInput{
|
|
Key: map[string]*dynamodb.AttributeValue{
|
|
"LockKey": {S: aws.String(key)},
|
|
},
|
|
TableName: aws.String(b.LockTable),
|
|
}
|
|
_, err := b.DB.DeleteItem(params)
|
|
return errors.Wrap(err, "deleting lock")
|
|
}
|
|
|
|
func (b Backend) List() ([]models.ProjectLock, error) {
|
|
var locks []models.ProjectLock
|
|
var err, internalErr error
|
|
params := &dynamodb.ScanInput{
|
|
TableName: aws.String(b.LockTable),
|
|
}
|
|
err = b.DB.ScanPages(params, func(out *dynamodb.ScanOutput, lastPage bool) bool {
|
|
var dynamoLocks []dynamoLock
|
|
if err := dynamodbattribute.UnmarshalListOfMaps(out.Items, &dynamoLocks); err != nil {
|
|
internalErr = errors.Wrap(err, "deserializing locks")
|
|
return false
|
|
}
|
|
for _, lock := range dynamoLocks {
|
|
locks = append(locks, models.ProjectLock{
|
|
PullNum: lock.PullNum,
|
|
Project: models.NewProject(lock.RepoFullName, lock.Path),
|
|
Env: lock.Env,
|
|
Time: lock.Time,
|
|
})
|
|
}
|
|
return lastPage
|
|
})
|
|
|
|
if err == nil && internalErr != nil {
|
|
err = internalErr
|
|
}
|
|
return locks, errors.Wrap(err, "scanning dynamodb")
|
|
}
|
|
|
|
func (b Backend) UnlockByPull(repoFullName string, pullNum int) error {
|
|
params := &dynamodb.ScanInput{
|
|
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
|
|
":pullNum": {
|
|
N: aws.String(strconv.Itoa(pullNum)),
|
|
},
|
|
":repoFullName": {
|
|
S: aws.String(repoFullName),
|
|
},
|
|
},
|
|
FilterExpression: aws.String("RepoFullName = :repoFullName and PullNum = :pullNum"),
|
|
TableName: aws.String(b.LockTable),
|
|
}
|
|
|
|
// scan DynamoDB for locks that match the pull request
|
|
var locks []dynamoLock
|
|
var err, internalErr error
|
|
err = b.DB.ScanPages(params, func(out *dynamodb.ScanOutput, lastPage bool) bool {
|
|
if err := dynamodbattribute.UnmarshalListOfMaps(out.Items, &locks); err != nil {
|
|
internalErr = errors.Wrap(err, "deserializing locks")
|
|
return false
|
|
}
|
|
return lastPage
|
|
})
|
|
if err == nil {
|
|
err = internalErr
|
|
}
|
|
if err != nil {
|
|
return errors.Wrap(err, "scanning dynamodb")
|
|
}
|
|
|
|
// now we can unlock all of them
|
|
for _, lock := range locks {
|
|
if err := b.Unlock(models.NewProject(lock.RepoFullName, lock.Path), lock.Env); err != nil {
|
|
return errors.Wrapf(err, "unlocking repo %s, path %s, env %s", lock.RepoFullName, lock.Path, lock.Env)
|
|
}
|
|
}
|
|
return nil
|
|
}
|