Files
atlantis/locking/locking.go
2017-07-09 18:18:36 -07:00

123 lines
3.7 KiB
Go

// Package locking handles locking projects when they have in-progress runs.
package locking
import (
"errors"
"fmt"
"regexp"
"time"
"github.com/hootsuite/atlantis/models"
)
// Backend is an implementation of the locking API we require.
type Backend interface {
TryLock(lock models.ProjectLock) (bool, models.ProjectLock, error)
Unlock(project models.Project, env string) (*models.ProjectLock, error)
List() ([]models.ProjectLock, error)
GetLock(project models.Project, env string) (*models.ProjectLock, error)
UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error)
}
// TryLockResponse results from an attempted lock.
type TryLockResponse struct {
// LockAcquired is true if the lock was acquired from this call.
LockAcquired bool
// CurrLock is what project is currently holding the lock.
CurrLock models.ProjectLock
// LockKey is an identified by which to lookup and delete this lock.
LockKey string
}
// Client is used to perform locking actions.
type Client struct {
backend Backend
}
// NewClient returns a new locking client.
func NewClient(backend Backend) *Client {
return &Client{
backend: backend,
}
}
// keyRegex matches and captures {repoFullName}/{path}/{env} where path can have multiple /'s in it.
var keyRegex = regexp.MustCompile(`^(.*?\/.*?)\/(.*)\/(.*)$`)
// TryLock attempts to acquire a lock to a project and environment.
func (c *Client) TryLock(p models.Project, env string, pull models.PullRequest, user models.User) (TryLockResponse, error) {
lock := models.ProjectLock{
Env: env,
Time: time.Now().Local(),
Project: p,
User: user,
Pull: pull,
}
lockAcquired, currLock, err := c.backend.TryLock(lock)
if err != nil {
return TryLockResponse{}, err
}
return TryLockResponse{lockAcquired, currLock, c.key(p, env)}, nil
}
// Unlock attempts to unlock a project and environment. If successful,
// a pointer to the now deleted lock will be returned. Else, that
// pointer will be nil. An error will only be returned if there was
// an error deleting the lock (i.e. not if there was no lock).
func (c *Client) Unlock(key string) (*models.ProjectLock, error) {
project, env, err := c.lockKeyToProjectEnv(key)
if err != nil {
return nil, err
}
return c.backend.Unlock(project, env)
}
// List lists all current locks.
func (c *Client) List() (map[string]models.ProjectLock, error) {
m := make(map[string]models.ProjectLock)
locks, err := c.backend.List()
if err != nil {
return m, err
}
for _, lock := range locks {
m[c.key(lock.Project, lock.Env)] = lock
}
return m, nil
}
// UnlockByPull deletes all locks associated with that pull request.
func (c *Client) UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error) {
return c.backend.UnlockByPull(repoFullName, pullNum)
}
// GetLock attempts to get the lock stored at key. If successful,
// a pointer to the lock will be returned. Else, the pointer will be nil.
// An error will only be returned if there was an error getting the lock
// (i.e. not if there was no lock).
func (c *Client) GetLock(key string) (*models.ProjectLock, error) {
project, env, err := c.lockKeyToProjectEnv(key)
if err != nil {
return nil, err
}
projectLock, err := c.backend.GetLock(project, env)
if err != nil {
return nil, err
}
return projectLock, nil
}
func (c *Client) key(p models.Project, env string) string {
return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, env)
}
func (c *Client) lockKeyToProjectEnv(key string) (models.Project, string, error) {
matches := keyRegex.FindStringSubmatch(key)
if len(matches) != 4 {
return models.Project{}, "", errors.New("invalid key format")
}
return models.Project{RepoFullName: matches[1], Path: matches[2]}, matches[3], nil
}