forked from runatlantis/atlantis
-
Notifications
You must be signed in to change notification settings - Fork 0
/
locking.go
135 lines (115 loc) · 4.46 KB
/
locking.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Package locking handles locking projects when they have in-progress runs.
package locking
import (
"errors"
"fmt"
"regexp"
"time"
"github.com/hootsuite/atlantis/server/events/models"
)
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_backend.go Backend
// 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, workspace string) (*models.ProjectLock, error)
List() ([]models.ProjectLock, error)
GetLock(project models.Project, workspace 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
}
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_locker.go Locker
type Locker interface {
TryLock(p models.Project, workspace string, pull models.PullRequest, user models.User) (TryLockResponse, error)
Unlock(key string) (*models.ProjectLock, error)
List() (map[string]models.ProjectLock, error)
UnlockByPull(repoFullName string, pullNum int) ([]models.ProjectLock, error)
GetLock(key string) (*models.ProjectLock, error)
}
// NewClient returns a new locking client.
func NewClient(backend Backend) *Client {
return &Client{
backend: backend,
}
}
// keyRegex matches and captures {repoFullName}/{path}/{workspace} where path can have multiple /'s in it.
var keyRegex = regexp.MustCompile(`^(.*?\/.*?)\/(.*)\/(.*)$`)
// TryLock attempts to acquire a lock to a project and workspace.
func (c *Client) TryLock(p models.Project, workspace string, pull models.PullRequest, user models.User) (TryLockResponse, error) {
lock := models.ProjectLock{
Workspace: workspace,
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, workspace)}, nil
}
// Unlock attempts to unlock a project and workspace. 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, workspace, err := c.lockKeyToProjectWorkspace(key)
if err != nil {
return nil, err
}
return c.backend.Unlock(project, workspace)
}
// List returns a map of all locks with their lock key as the map key.
// The lock key can be used in GetLock() and Unlock().
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.Workspace)] = 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, workspace, err := c.lockKeyToProjectWorkspace(key)
if err != nil {
return nil, err
}
projectLock, err := c.backend.GetLock(project, workspace)
if err != nil {
return nil, err
}
return projectLock, nil
}
func (c *Client) key(p models.Project, workspace string) string {
return fmt.Sprintf("%s/%s/%s", p.RepoFullName, p.Path, workspace)
}
func (c *Client) lockKeyToProjectWorkspace(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
}