Skip to content

Commit aa7defb

Browse files
committed
fix(pkg/sshd): wrap git receive functionality in a locking utility fu… (#339)
* fix(pkg/sshd): wrap git receive functionality in a locking utility function The function always tries once to unlock Fixes deis/builder#309 Doesn’t attempt to fix deis/builder#220
1 parent af0bc8c commit aa7defb

3 files changed

Lines changed: 59 additions & 25 deletions

File tree

pkg/sshd/lock.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package sshd
22

33
import (
4+
"errors"
45
"fmt"
56
"sync"
67
"time"
78
)
89

10+
var (
11+
errAlreadyLocked = errors.New("already locked")
12+
)
13+
914
// RepositoryLock interface that allows the creation of a lock associated
1015
// with a repository name to avoid simultaneous git operations.
1116
type RepositoryLock interface {
@@ -20,6 +25,14 @@ type RepositoryLock interface {
2025
Unlock(repoName string, timeout time.Duration) error
2126
}
2227

28+
func wrapInLock(lck RepositoryLock, repoName string, timeout time.Duration, fn func() error) error {
29+
if err := lck.Lock(repoName, timeout); err != nil {
30+
return errAlreadyLocked
31+
}
32+
defer lck.Unlock(repoName, timeout)
33+
return fn()
34+
}
35+
2336
// NewInMemoryRepositoryLock returns a new instance of a RepositoryLock.
2437
func NewInMemoryRepositoryLock() RepositoryLock {
2538
return &inMemoryRepoLock{

pkg/sshd/lock_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ func TestDoubleLockUnlock(t *testing.T) {
8686
}
8787
}
8888

89+
func TestWrapInLock(t *testing.T) {
90+
lck := NewInMemoryRepositoryLock()
91+
assert.NoErr(t, wrapInLock(lck, "repo", 0*time.Second, func() error {
92+
return nil
93+
}))
94+
lck.Lock("repo", 0*time.Second)
95+
assert.Err(t, errAlreadyLocked, wrapInLock(lck, "repo", 0*time.Second, func() error {
96+
return nil
97+
}))
98+
}
99+
89100
func lockAndCallback(rl RepositoryLock, id string, callbackCh chan<- interface{}) {
90101
if err := rl.Lock(id, time.Duration(0)); err == nil {
91102
callbackCh <- true

pkg/sshd/server.go

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -254,41 +254,20 @@ func (s *server) answer(channel ssh.Channel, requests <-chan *ssh.Request, conda
254254
channel.Stderr().Write([]byte("No repo given"))
255255
return err
256256
}
257-
if err = s.pushLock.Lock(repoName, time.Duration(0)); err != nil {
257+
wrapErr := wrapInLock(s.pushLock, repoName, time.Duration(0), s.runReceive(req, sshconn, channel, repoName, parts, condata))
258+
if wrapErr == errAlreadyLocked {
258259
log.Info(multiplePush)
259260
// The error must be in git format
260-
if err := gitPktLine(channel, fmt.Sprintf("ERR %v\n", multiplePush)); err != nil {
261+
if pktErr := gitPktLine(channel, fmt.Sprintf("ERR %v\n", multiplePush)); pktErr != nil {
261262
log.Err("Failed to write to channel: %s", err)
262263
}
263264
sendExitStatus(1, channel)
264265
req.Reply(false, nil)
265266
return nil
266267
}
267268

268-
req.Reply(true, nil) // We processed. Yay.
269-
if !strings.Contains(sshconn.Permissions.Extensions["apps"], repoName) {
270-
if err := s.pushLock.Unlock(repoName, time.Duration(0)); err != nil {
271-
log.Err("unable to unlock repository lock for %s (%s)", repoName, err)
272-
// TODO: this is an important error case that needs to be covered
273-
// Probably the best solution is to change the lock into a lease so that even on unlock
274-
// failures, RepositoryLock will eventually yield
275-
}
276-
return errBuildAppPerm
277-
}
278-
repo := repoName + ".git"
279-
err = git.Receive(repo, parts[0], s.gitHome, channel, sshconn.Permissions.Extensions["fingerprint"], sshconn.Permissions.Extensions["user"], condata, s.receivetype)
280-
if err != nil {
281-
if err := s.pushLock.Unlock(repoName, time.Duration(0)); err != nil {
282-
log.Err("unable to unlock repository lock for %s (%s)", repoName, err)
283-
// TODO: this is an important error case that needs to be covered
284-
// Probably the best solution is to change the lock into a lease so that even on unlock
285-
// failures, RepositoryLock will eventually yield
286-
}
287-
return err
288-
}
289-
290269
var xs uint32
291-
if err != nil {
270+
if wrapErr != nil {
292271
log.Err("Failed git receive: %v", err)
293272
xs = 1
294273
}
@@ -316,6 +295,37 @@ func (s *server) answer(channel ssh.Channel, requests <-chan *ssh.Request, conda
316295
return nil
317296
}
318297

298+
func (s *server) runReceive(
299+
req *ssh.Request,
300+
sshConn *ssh.ServerConn,
301+
channel ssh.Channel,
302+
repoName string,
303+
parts []string,
304+
connData string,
305+
) func() error {
306+
return func() error {
307+
req.Reply(true, nil) // We processed. Yay.
308+
if !strings.Contains(sshConn.Permissions.Extensions["apps"], repoName) {
309+
return errBuildAppPerm
310+
}
311+
repo := repoName + ".git"
312+
recvErr := git.Receive(
313+
repo,
314+
parts[0],
315+
s.gitHome,
316+
channel,
317+
sshConn.Permissions.Extensions["fingerprint"],
318+
sshConn.Permissions.Extensions["user"],
319+
connData,
320+
s.receivetype,
321+
)
322+
if recvErr != nil {
323+
return recvErr
324+
}
325+
return nil
326+
}
327+
}
328+
319329
// ExecCmd is an SSH exec request.
320330
type ExecCmd struct {
321331
Value string

0 commit comments

Comments
 (0)