Skip to content
This repository has been archived by the owner on Apr 26, 2021. It is now read-only.

Commit

Permalink
Support namespaces for repository
Browse files Browse the repository at this point in the history
  • Loading branch information
scorphus committed Aug 25, 2014
1 parent bd88ce1 commit 9c38566
Show file tree
Hide file tree
Showing 14 changed files with 1,297 additions and 456 deletions.
272 changes: 238 additions & 34 deletions api/handler.go

Large diffs are not rendered by default.

495 changes: 422 additions & 73 deletions api/handler_test.go

Large diffs are not rendered by default.

44 changes: 15 additions & 29 deletions bin/gandalf.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func action() string {
// and gets the repository from the database based on the info
// obtained by the SSH_ORIGINAL_COMMAND parse.
func requestedRepository() (repository.Repository, error) {
repoName, err := requestedRepositoryName()
_, repoNamespace, repoName, err := parseGitCommand()
if err != nil {
return repository.Repository{}, err
}
Expand All @@ -73,36 +73,25 @@ func requestedRepository() (repository.Repository, error) {
return repository.Repository{}, err
}
defer conn.Close()
if err := conn.Repository().Find(bson.M{"_id": repoName}).One(&repo); err != nil {
if err := conn.Repository().Find(repository.BsonIndex(repoNamespace, repoName)).One(&repo); err != nil {
return repository.Repository{}, errors.New("Repository not found")
}
return repo, nil
}

func requestedRepositoryName() (string, error) {
r, err := regexp.Compile(`[\w-]+ '/?([\w-]+)\.git'`)
if err != nil {
panic(err)
}
m := r.FindStringSubmatch(os.Getenv("SSH_ORIGINAL_COMMAND"))
if len(m) < 2 {
return "", errors.New("Cannot deduce repository name from command. You are probably trying to do something nasty")
}
return m[1], nil
}

// Checks whether a command is a valid git command
// The following format is allowed:
// git-([\w-]+) '([\w-]+)\.git'
func validateCmd() error {
r, err := regexp.Compile(`git-([\w-]+) '/?([\w-]+)\.git'`)
// (git-[a-z-]+) '/?(([\w-+.@]+)/)?([\w-]+)\.git'
func parseGitCommand() (command, namespace, name string, err error) {
r, err := regexp.Compile(`(git-[a-z-]+) '/?(([\w-+.@]+)/)?([\w-]+)\.git'`)
if err != nil {
panic(err)
}
if m := r.FindStringSubmatch(os.Getenv("SSH_ORIGINAL_COMMAND")); len(m) < 3 {
return errors.New("You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.")
m := r.FindStringSubmatch(os.Getenv("SSH_ORIGINAL_COMMAND"))
if len(m) != 5 {
return "", "", "", errors.New("You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.")
}
return nil
return m[1], m[3], m[4], nil
}

// Executes the SSH_ORIGINAL_COMMAND based on the condition
Expand Down Expand Up @@ -161,21 +150,18 @@ func formatCommand() ([]string, error) {
log.Err(err.Error())
return []string{}, err
}
repoName, err := requestedRepositoryName()
_, repoNamespace, repoName, err := parseGitCommand()
if err != nil {
log.Err(err.Error())
return []string{}, err
}
repoName += ".git"
cmdList := strings.Split(os.Getenv("SSH_ORIGINAL_COMMAND"), " ")
for i, c := range cmdList {
c = strings.Trim(c, "'")
c = strings.Trim(c, "/")
if c == repoName {
cmdList[i] = path.Join(p, repoName)
break
}
if len(cmdList) != 2 {
log.Err("Malformed git command")
return []string{}, fmt.Errorf("Malformed git command")
}
cmdList[1] = path.Join(p, repoNamespace, repoName)
return cmdList, nil
}

Expand All @@ -192,7 +178,7 @@ func main() {
fmt.Fprintln(os.Stderr, err.Error())
return
}
err = validateCmd()
_, _, _, err = parseGitCommand()
if err != nil {
log.Err(err.Error())
fmt.Fprintln(os.Stderr, err.Error())
Expand Down
70 changes: 49 additions & 21 deletions bin/gandalf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/tsuru/gandalf/db"
"github.com/tsuru/gandalf/repository"
"github.com/tsuru/gandalf/user"
"gopkg.in/mgo.v2/bson"
"launchpad.net/gocheck"
"log/syslog"
"os"
Expand Down Expand Up @@ -64,7 +63,7 @@ func (s *S) TestHasWritePermissionShouldReturnFalseWhenUserCannotWriteinRepo(c *
c.Assert(err, gocheck.IsNil)
defer conn.Close()
conn.Repository().Insert(&r)
defer conn.Repository().Remove(bson.M{"_id": r.Name})
defer conn.Repository().Remove(repository.BsonIndex(r.Namespace, r.Name))
allowed := hasWritePermission(s.user, r)
c.Assert(allowed, gocheck.Equals, false)
}
Expand All @@ -75,7 +74,7 @@ func (s *S) TestHasReadPermissionShouldReturnTrueWhenRepositoryIsPublic(c *goche
c.Assert(err, gocheck.IsNil)
defer conn.Close()
conn.Repository().Insert(&r)
defer conn.Repository().Remove(bson.M{"_id": r.Name})
defer conn.Repository().Remove(repository.BsonIndex(r.Namespace, r.Name))
allowed := hasReadPermission(s.user, r)
c.Assert(allowed, gocheck.Equals, true)
}
Expand All @@ -91,7 +90,7 @@ func (s *S) TestHasReadPermissionShouldReturnFalseWhenUserDoesNotHavePermissionT
c.Assert(err, gocheck.IsNil)
defer conn.Close()
conn.Repository().Insert(&r)
defer conn.Repository().Remove(bson.M{"_id": r.Name})
defer conn.Repository().Remove(repository.BsonIndex(r.Namespace, r.Name))
allowed := hasReadPermission(s.user, r)
c.Assert(allowed, gocheck.Equals, false)
}
Expand All @@ -115,7 +114,7 @@ func (s *S) TestRequestedRepositoryShouldGetArgumentInSSH_ORIGINAL_COMMANDAndRet
defer conn.Close()
err = conn.Repository().Insert(&r)
c.Assert(err, gocheck.IsNil)
defer conn.Repository().Remove(bson.M{"_id": r.Name})
defer conn.Repository().Remove(repository.BsonIndex(r.Namespace, r.Name))
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack 'foo.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
repo, err := requestedRepository()
Expand All @@ -130,7 +129,7 @@ func (s *S) TestRequestedRepositoryShouldDeduceCorrectlyRepositoryNameWithDash(c
defer conn.Close()
err = conn.Repository().Insert(&r)
c.Assert(err, gocheck.IsNil)
defer conn.Repository().Remove(bson.M{"_id": r.Name})
defer conn.Repository().Remove(repository.BsonIndex(r.Namespace, r.Name))
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack 'foo-bar.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
repo, err := requestedRepository()
Expand All @@ -142,12 +141,12 @@ func (s *S) TestRequestedRepositoryShouldReturnErrorWhenCommandDoesNotPassesWhat
os.Setenv("SSH_ORIGINAL_COMMAND", "rm -rf /")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
_, err := requestedRepository()
c.Assert(err, gocheck.ErrorMatches, "^Cannot deduce repository name from command. You are probably trying to do something nasty$")
c.Assert(err, gocheck.ErrorMatches, "^You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.$")
}

func (s *S) TestRequestedRepositoryShouldReturnErrorWhenThereIsNoCommandPassedToSSH_ORIGINAL_COMMAND(c *gocheck.C) {
_, err := requestedRepository()
c.Assert(err, gocheck.ErrorMatches, "^Cannot deduce repository name from command. You are probably trying to do something nasty$")
c.Assert(err, gocheck.ErrorMatches, "^You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.$")
}

func (s *S) TestRequestedRepositoryShouldReturnFormatedErrorWhenRepositoryDoesNotExists(c *gocheck.C) {
Expand All @@ -163,48 +162,66 @@ func (s *S) TestRequestedRepositoryShouldReturnEmptyRepositoryStructOnError(c *g
c.Assert(repo.Name, gocheck.Equals, "")
}

func (s *S) TestRequestedRepositoryName(c *gocheck.C) {
func (s *S) TestParseGitCommand(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack 'foobar.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
name, err := requestedRepositoryName()
_, _, name, err := parseGitCommand()
c.Assert(err, gocheck.IsNil)
c.Assert(name, gocheck.Equals, "foobar")
}

func (s *S) TestRequestedRepositoryNameWithSlash(c *gocheck.C) {
func (s *S) TestParseGitCommandWithSlash(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack '/foobar.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
name, err := requestedRepositoryName()
_, _, name, err := parseGitCommand()
c.Assert(err, gocheck.IsNil)
c.Assert(name, gocheck.Equals, "foobar")
}

func (s *S) TestParseGitCommandWithNamespace(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack 'spamegg/foobar.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
_, namespace, name, err := parseGitCommand()
c.Assert(err, gocheck.IsNil)
c.Assert(namespace, gocheck.Equals, "spamegg")
c.Assert(name, gocheck.Equals, "foobar")
}

func (s *S) TestParseGitCommandWithNamespaceAndSlash(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack '/spamegg/foobar.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
_, namespace, name, err := parseGitCommand()
c.Assert(err, gocheck.IsNil)
c.Assert(namespace, gocheck.Equals, "spamegg")
c.Assert(name, gocheck.Equals, "foobar")
}

func (s *S) TestrequestedRepositoryNameShouldReturnErrorWhenTheresNoMatch(c *gocheck.C) {
func (s *S) TestParseGitCommandShouldReturnErrorWhenTheresNoMatch(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack foobar")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
name, err := requestedRepositoryName()
c.Assert(err, gocheck.ErrorMatches, "Cannot deduce repository name from command. You are probably trying to do something nasty")
_, _, name, err := parseGitCommand()
c.Assert(err, gocheck.ErrorMatches, "You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.")
c.Assert(name, gocheck.Equals, "")
}

func (s *S) TestValidateCmdReturnsErrorWhenSSH_ORIGINAL_COMMANDIsNotAGitCommand(c *gocheck.C) {
func (s *S) TestParseGitCommandReturnsErrorWhenSSH_ORIGINAL_COMMANDIsNotAGitCommand(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "rm -rf /")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
err := validateCmd()
_, _, _, err := parseGitCommand()
c.Assert(err, gocheck.ErrorMatches, "^You've tried to execute some weird command, I'm deliberately denying you to do that, get over it.$")
}

func (s *S) TestValidateCmdDoNotReturnsErrorWhenSSH_ORIGINAL_COMMANDIsAValidGitCommand(c *gocheck.C) {
func (s *S) TestParseGitCommandDoNotReturnsErrorWhenSSH_ORIGINAL_COMMANDIsAValidGitCommand(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack 'my-repo.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
err := validateCmd()
_, _, _, err := parseGitCommand()
c.Assert(err, gocheck.IsNil)
}

func (s *S) TestValidateCmdDoNotReturnsErrorWhenSSH_ORIGINAL_COMMANDIsAValidGitCommandWithDashInName(c *gocheck.C) {
func (s *S) TestParseGitCommandDoNotReturnsErrorWhenSSH_ORIGINAL_COMMANDIsAValidGitCommandWithDashInName(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack '/my-repo.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
err := validateCmd()
_, _, _, err := parseGitCommand()
c.Assert(err, gocheck.IsNil)
}

Expand Down Expand Up @@ -270,6 +287,17 @@ func (s *S) TestFormatCommandShouldReceiveAGitCommandAndCanonizalizeTheRepositor
c.Assert(cmd, gocheck.DeepEquals, []string{"git-receive-pack", expected})
}

func (s *S) TestFormatCommandShouldReceiveAGitCommandAndCanonizalizeTheRepositoryPathWithNamespace(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack 'me/myproject.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
cmd, err := formatCommand()
c.Assert(err, gocheck.IsNil)
p, err := config.GetString("git:bare:location")
c.Assert(err, gocheck.IsNil)
expected := path.Join(p, "me/myproject.git")
c.Assert(cmd, gocheck.DeepEquals, []string{"git-receive-pack", expected})
}

func (s *S) TestFormatCommandShouldReceiveAGitCommandProjectWithDash(c *gocheck.C) {
os.Setenv("SSH_ORIGINAL_COMMAND", "git-receive-pack '/myproject.git'")
defer os.Setenv("SSH_ORIGINAL_COMMAND", "")
Expand Down
5 changes: 4 additions & 1 deletion db/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ func Conn() (*Storage, error) {

// Repository returns a reference to the "repository" collection in MongoDB.
func (s *Storage) Repository() *storage.Collection {
return s.Collection("repository")
index := mgo.Index{Key: []string{"namespace", "name"}, Unique: true}
c := s.Collection("repository")
c.EnsureIndex(index)
return c
}

// User returns a reference to the "user" collection in MongoDB.
Expand Down
37 changes: 37 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,40 @@ Example result::
}],
next: "1267b5de5943632e47cb6f8bf5b2147bc0be5cf123"
}

Namespaces
----------

Gandalf supports namespaces for repositories. In order to operate these repositories, one should prepend `/namespace/:namespace` to the URI of the desired operation. Examples:

* Creates a repository in a namespace:

* Method: POST
* URI: /namespace/`:namespace`/repository
* Format: JSON

Where:

* `:namespace` is the name of the namespace;

Example URL (http://gandalf-server omitted for clarity)::

$ curl -XPOST /namespace/mynamespace/repository \
-d '{"namespace": "mynamespace", \
"name": "myrepository", \
"users": ["joe"]}'

* Returns a list of all the branches of the specified `repository`.

* Method: GET
* URI: /namespace/`:namespace`/repository/`:name`/branches
* Format: JSON

Where:

* `:namespace` is the name of the namespace;
* `:name` is the name of the repository.

Example URL (http://gandalf-server omitted for clarity)::

$ curl /namespace/mynamespace/repository/myrepository/branches # gets list of branches
12 changes: 6 additions & 6 deletions repository/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ func bareLocation() string {
return bare
}

func barePath(name string) string {
return path.Join(bareLocation(), name+".git")
func barePath(namespace, name string) string {
return path.Join(bareLocation(), namespace, name+".git")
}

func newBare(name string) error {
args := []string{"init", barePath(name), "--bare"}
func newBare(namespace, name string) error {
args := []string{"init", barePath(namespace, name), "--bare"}
if bareTempl, err := config.GetString("git:bare:template"); err == nil {
args = append(args, "--template="+bareTempl)
}
Expand All @@ -43,8 +43,8 @@ func newBare(name string) error {
return nil
}

func removeBare(name string) error {
err := fs.Filesystem().RemoveAll(barePath(name))
func removeBare(namespace, name string) error {
err := fs.Filesystem().RemoveAll(barePath(namespace, name))
if err != nil {
return fmt.Errorf("Could not remove git bare repository: %s", err)
}
Expand Down
12 changes: 6 additions & 6 deletions repository/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (s *S) TestNewBareShouldCreateADir(c *gocheck.C) {
dir, err := commandmocker.Add("git", "$*")
c.Check(err, gocheck.IsNil)
defer commandmocker.Remove(dir)
err = newBare("myBare")
err = newBare("", "myBare")
c.Assert(err, gocheck.IsNil)
c.Assert(commandmocker.Ran(dir), gocheck.Equals, true)
}
Expand All @@ -40,7 +40,7 @@ func (s *S) TestNewBareShouldReturnMeaningfullErrorWhenBareCreationFails(c *goch
dir, err := commandmocker.Error("git", "cmd output", 1)
c.Assert(err, gocheck.IsNil)
defer commandmocker.Remove(dir)
err = newBare("foo")
err = newBare("", "foo")
c.Check(err, gocheck.NotNil)
got := err.Error()
expected := "Could not create git bare repository: exit status 1. cmd output"
Expand All @@ -56,7 +56,7 @@ func (s *S) TestNewBareShouldPassTemplateOptionWhenItExistsOnConfig(c *gocheck.C
dir, err := commandmocker.Add("git", "$*")
c.Assert(err, gocheck.IsNil)
defer commandmocker.Remove(dir)
err = newBare("foo")
err = newBare("", "foo")
c.Assert(err, gocheck.IsNil)
c.Assert(commandmocker.Ran(dir), gocheck.Equals, true)
expected := fmt.Sprintf("init %s --bare --template=%s", barePath, bareTemplate)
Expand All @@ -71,7 +71,7 @@ func (s *S) TestNewBareShouldNotPassTemplateOptionWhenItsNotSetInConfig(c *goche
dir, err := commandmocker.Add("git", "$*")
c.Assert(err, gocheck.IsNil)
defer commandmocker.Remove(dir)
err = newBare("foo")
err = newBare("", "foo")
c.Assert(err, gocheck.IsNil)
c.Assert(commandmocker.Ran(dir), gocheck.Equals, true)
expected := fmt.Sprintf("init %s --bare", barePath)
Expand All @@ -82,7 +82,7 @@ func (s *S) TestRemoveBareShouldRemoveBareDirFromFileSystem(c *gocheck.C) {
rfs := &testing.RecordingFs{FileContent: "foo"}
fs.Fsystem = rfs
defer func() { fs.Fsystem = nil }()
err := removeBare("myBare")
err := removeBare("", "myBare")
c.Assert(err, gocheck.IsNil)
action := "removeall " + path.Join(bareLocation(), "myBare.git")
c.Assert(rfs.HasAction(action), gocheck.Equals, true)
Expand All @@ -92,6 +92,6 @@ func (s *S) TestRemoveBareShouldReturnDescriptiveErrorWhenRemovalFails(c *gochec
rfs := &testing.RecordingFs{FileContent: "foo"}
fs.Fsystem = &testing.FileNotFoundFs{RecordingFs: *rfs}
defer func() { fs.Fsystem = nil }()
err := removeBare("fooo")
err := removeBare("", "fooo")
c.Assert(err, gocheck.ErrorMatches, "^Could not remove git bare repository: .*")
}
Loading

0 comments on commit 9c38566

Please sign in to comment.