Skip to content

Commit 1d7eaef

Browse files
committed
Add terraform state lock
1 parent 2f8577e commit 1d7eaef

File tree

6 files changed

+74
-0
lines changed

6 files changed

+74
-0
lines changed

modules/globallock/globallock.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ func TryLock(ctx context.Context, key string) (bool, ReleaseFunc, error) {
4545
return DefaultLocker().TryLock(ctx, key)
4646
}
4747

48+
func Unlock(ctx context.Context, key string) error {
49+
return DefaultLocker().Unlock(ctx, key)
50+
}
51+
4852
// LockAndDo tries to acquire a lock for the given key and then calls the given function.
4953
// It uses the default locker, and it will return an error if failed to acquire the lock.
5054
func LockAndDo(ctx context.Context, key string, f func(context.Context) error) error {

modules/globallock/locker.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ type Locker interface {
3232
// And if it fails to acquire the lock because it's already locked, not other reasons like redis is down,
3333
// it will return false without any error.
3434
TryLock(ctx context.Context, key string) (bool, ReleaseFunc, error)
35+
36+
Unlock(ctx context.Context, key string) error
3537
}
3638

3739
// ReleaseFunc is a function that releases a lock.

modules/globallock/memory_locker.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ func (l *memoryLocker) TryLock(_ context.Context, key string) (bool, ReleaseFunc
6161
return false, func() {}, nil
6262
}
6363

64+
func (l *memoryLocker) Unlock(_ context.Context, key string) error {
65+
l.locks.Delete(key)
66+
return nil
67+
}
68+
6469
func (l *memoryLocker) tryLock(key string) bool {
6570
_, loaded := l.locks.LoadOrStore(key, struct{}{})
6671
return !loaded

modules/globallock/redis_locker.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ func (l *redisLocker) TryLock(ctx context.Context, key string) (bool, ReleaseFun
6464
return err == nil, f, err
6565
}
6666

67+
func (l *redisLocker) Unlock(ctx context.Context, key string) error {
68+
mutex, ok := l.mutexM.Load(key)
69+
if ok {
70+
l.mutexM.Delete(key)
71+
72+
// It's safe to ignore the error here,
73+
// if it failed to unlock, it will be released automatically after the lock expires.
74+
// Do not call mutex.UnlockContext(ctx) here, or it will fail to release when ctx has timed out.
75+
_, _ = mutex.(*redsync.Mutex).Unlock()
76+
}
77+
return nil
78+
}
79+
6780
// Close closes the locker.
6881
// It will stop extending the locks and refuse to acquire new locks.
6982
// In actual use, it is not necessary to call this function.

routers/api/packages/api.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,10 @@ func CommonRoutes() *web.Router {
672672
r.Put("", terraform.UploadPackage)
673673
r.Delete("", terraform.DeletePackageFile)
674674
}, reqPackageAccess(perm.AccessModeWrite))
675+
r.Group("/lock", func() {
676+
r.Post("", terraform.LockPackage)
677+
r.Delete("", terraform.UnlockPackage)
678+
}, reqPackageAccess(perm.AccessModeWrite))
675679
})
676680
})
677681
}, reqPackageAccess(perm.AccessModeRead))

routers/api/packages/terraform/terraform.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,49 @@ func DeletePackageFile(ctx *context.Context) {
208208

209209
ctx.Status(http.StatusNoContent)
210210
}
211+
212+
// LockPackage locks the specific terraform state.
213+
func LockPackage(ctx *context.Context) {
214+
packageName := ctx.PathParam("packagename")
215+
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraform, packageName, ctx.PathParam("filename"))
216+
if err != nil {
217+
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) {
218+
apiError(ctx, http.StatusNotFound, err)
219+
return
220+
}
221+
apiError(ctx, http.StatusInternalServerError, err)
222+
return
223+
}
224+
225+
//log.Error("pv: %+v", pv)
226+
ok, _, err := globallock.TryLock(ctx, fmt.Sprintf("%s/%s", packageName, pv.LowerVersion))
227+
if err != nil {
228+
apiError(ctx, http.StatusInternalServerError, err)
229+
return
230+
}
231+
if !ok {
232+
apiError(ctx, http.StatusLocked, err)
233+
return
234+
}
235+
236+
ctx.Status(http.StatusOK)
237+
}
238+
239+
// UnlockPackage unlock the specific terraform state.
240+
func UnlockPackage(ctx *context.Context) {
241+
packageName := ctx.PathParam("packagename")
242+
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraform, packageName, ctx.PathParam("filename"))
243+
if err != nil {
244+
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) {
245+
apiError(ctx, http.StatusNotFound, err)
246+
return
247+
}
248+
apiError(ctx, http.StatusInternalServerError, err)
249+
return
250+
}
251+
252+
//log.Error("pv: %+v", pv)
253+
_ = globallock.Unlock(ctx, fmt.Sprintf("%s/%s", packageName, pv.LowerVersion))
254+
255+
ctx.Status(http.StatusOK)
256+
}

0 commit comments

Comments
 (0)