diff --git a/Makefile b/Makefile index 19e40518..bfe9d94c 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ PKGS := $(addprefix github.com/drone/drone/pkg/,$(PKGS)) all: embed build -deps: +deps: go-gitlab-client [ -d $$GOPATH/src/code.google.com/p/go ] || hg clone -u default https://code.google.com/p/go $$GOPATH/src/code.google.com/p/go [ -d $$GOPATH/src/github.com/dotcloud/docker ] || git clone git://github.com/dotcloud/docker $$GOPATH/src/github.com/dotcloud/docker go get code.google.com/p/go.crypto/bcrypt @@ -97,3 +97,7 @@ dpkg: run: bin/droned --port=":8080" --datasource="drone.sqlite" + +go-gitlab-client: + rm -rf $$GOPATH/src/github.com/plouc/go-gitlab-client + git clone -b raw-request https://github.com/fudanchii/go-gitlab-client $$GOPATH/src/github.com/plouc/go-gitlab-client diff --git a/cmd/droned/drone.go b/cmd/droned/drone.go index 616d87ce..ede6cfe8 100644 --- a/cmd/droned/drone.go +++ b/cmd/droned/drone.go @@ -133,6 +133,7 @@ func setupHandlers() { queue := queue.Start(workers, queueRunner) hookHandler := handler.NewHookHandler(queue) + gitlab := handler.NewGitlabHandler(queue) m := pat.New() m.Get("/login", handler.ErrorHandler(handler.Login)) @@ -152,16 +153,24 @@ func setupHandlers() { m.Post("/new/github.com", handler.UserHandler(handler.RepoCreateGithub)) m.Get("/new/github.com", handler.UserHandler(handler.RepoAddGithub)) + // handlers for linking your GitHub account + m.Get("/auth/login/github", handler.UserHandler(handler.LinkGithub)) + // handlers for setting up your Bitbucket repository m.Post("/new/bitbucket.org", handler.UserHandler(handler.RepoCreateBitbucket)) m.Get("/new/bitbucket.org", handler.UserHandler(handler.RepoAddBitbucket)) - // handlers for linking your GitHub account - m.Get("/auth/login/github", handler.UserHandler(handler.LinkGithub)) - // handlers for linking your Bitbucket account m.Get("/auth/login/bitbucket", handler.UserHandler(handler.LinkBitbucket)) + // handlers for setting up your GitLab repository + m.Post("/new/gitlab", handler.UserHandler(gitlab.Create)) + m.Get("/new/gitlab", handler.UserHandler(gitlab.Add)) + + // handler for linking GitLab account + m.Post("/link/gitlab", handler.UserHandler(gitlab.Link)) + m.Get("/link/gitlab", handler.UserHandler(gitlab.ReLink)) + // handlers for dashboard pages m.Get("/dashboard/team/:team", handler.UserHandler(handler.TeamShow)) m.Get("/dashboard", handler.UserHandler(handler.UserShow)) @@ -205,6 +214,9 @@ func setupHandlers() { // handlers for Bitbucket post-commit hooks m.Post("/hook/bitbucket.org", handler.ErrorHandler(hookHandler.HookBitbucket)) + // handlers for GitLab post-commit hooks + m.Post("/hook/gitlab", handler.ErrorHandler(gitlab.Hook)) + // handlers for first-time installation m.Get("/install", handler.ErrorHandler(handler.Install)) m.Post("/install", handler.ErrorHandler(handler.InstallPost)) diff --git a/pkg/database/migrate/20140328201430_add_gitlab_columns.go b/pkg/database/migrate/20140328201430_add_gitlab_columns.go new file mode 100644 index 00000000..d1948a09 --- /dev/null +++ b/pkg/database/migrate/20140328201430_add_gitlab_columns.go @@ -0,0 +1,43 @@ +package migrate + +type rev20140328201430 struct{} + +var AddGitlabColumns = &rev20140328201430{} + +func (r *rev20140328201430) Revision() int64 { + return 20140328201430 +} + +func (r *rev20140328201430) Up(mg *MigrationDriver) error { + // Migration steps here + if _, err := mg.AddColumn("settings", mg.T.String("gitlab_domain")); err != nil { + return err + } + if _, err := mg.AddColumn("settings", mg.T.String("gitlab_apiurl")); err != nil { + return err + } + + if _, err := mg.Tx.Exec(`update settings set gitlab_domain=?`, "gitlab.com"); err != nil { + return err + } + + if _, err := mg.Tx.Exec(`update settings set gitlab_apiurl=?`, "https://gitlab.com"); err != nil { + return err + } + + if _, err := mg.AddColumn("users", mg.T.String("gitlab_token")); err != nil { + return err + } + + _, err := mg.Tx.Exec(`update users set gitlab_token=?`, "") + return err +} + +func (r *rev20140328201430) Down(mg *MigrationDriver) error { + // Revert migration steps here + if _, err := mg.DropColumns("users", "gitlab_token"); err != nil { + return err + } + _, err := mg.DropColumns("settings", "gitlab_domain", "gitlab_apiurl") + return err +} diff --git a/pkg/database/migrate/all.go b/pkg/database/migrate/all.go index 1748f18f..c745a4ed 100644 --- a/pkg/database/migrate/all.go +++ b/pkg/database/migrate/all.go @@ -12,6 +12,7 @@ func (m *Migration) All() *Migration { m.Add(RenamePrivelegedToPrivileged) m.Add(GitHubEnterpriseSupport) m.Add(AddOpenInvitationColumn) + m.Add(AddGitlabColumns) // m.Add(...) // ... diff --git a/pkg/database/settings.go b/pkg/database/settings.go index 883b87d3..fb22d226 100644 --- a/pkg/database/settings.go +++ b/pkg/database/settings.go @@ -11,8 +11,8 @@ const settingsTable = "settings" // SQL Queries to retrieve the system settings const settingsStmt = ` SELECT id, github_key, github_secret, github_domain, github_apiurl, bitbucket_key, bitbucket_secret, -smtp_server, smtp_port, smtp_address, smtp_username, smtp_password, hostname, scheme, open_invitations -FROM settings WHERE id = 1 +gitlab_domain, gitlab_apiurl, smtp_server, smtp_port, smtp_address, smtp_username, smtp_password, +hostname, scheme, open_invitations FROM settings WHERE id = 1 ` //var ( diff --git a/pkg/database/testing/testing.go b/pkg/database/testing/testing.go index d906f717..c252204c 100644 --- a/pkg/database/testing/testing.go +++ b/pkg/database/testing/testing.go @@ -32,26 +32,29 @@ func Setup() { // create dummy user data user1 := User{ - Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", - Name: "Brad Rydzewski", - Email: "brad.rydzewski@gmail.com", - Gravatar: "8c58a0be77ee441bb8f8595b7f1b4e87", - Token: "123", - Admin: true} + Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", + Name: "Brad Rydzewski", + Email: "brad.rydzewski@gmail.com", + Gravatar: "8c58a0be77ee441bb8f8595b7f1b4e87", + Token: "123", + GitlabToken: "123", + Admin: true} user2 := User{ - Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", - Name: "Thomas Burke", - Email: "cavepig@gmail.com", - Gravatar: "c62f7126273f7fa786274274a5dec8ce", - Token: "456", - Admin: false} + Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", + Name: "Thomas Burke", + Email: "cavepig@gmail.com", + Gravatar: "c62f7126273f7fa786274274a5dec8ce", + Token: "456", + GitlabToken: "456", + Admin: false} user3 := User{ - Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", - Name: "Carlos Morales", - Email: "ytsejammer@gmail.com", - Gravatar: "c2180a539620d90d68eaeb848364f1c2", - Token: "789", - Admin: false} + Password: "$2a$10$b8d63QsTL38vx7lj0HEHfOdbu1PCAg6Gfca74UavkXooIBx9YxopS", + Name: "Carlos Morales", + Email: "ytsejammer@gmail.com", + Gravatar: "c2180a539620d90d68eaeb848364f1c2", + Token: "789", + GitlabToken: "789", + Admin: false} database.SaveUser(&user1) database.SaveUser(&user2) diff --git a/pkg/database/users.go b/pkg/database/users.go index 3bea9745..5e229e5b 100644 --- a/pkg/database/users.go +++ b/pkg/database/users.go @@ -13,21 +13,24 @@ const userTable = "users" // SQL Queries to retrieve a user by their unique database key const userFindIdStmt = ` SELECT id, email, password, token, name, gravatar, created, updated, admin, -github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret +github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret, +gitlab_token FROM users WHERE id = ? ` // SQL Queries to retrieve a user by their email address const userFindEmailStmt = ` SELECT id, email, password, token, name, gravatar, created, updated, admin, -github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret +github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret, +gitlab_token FROM users WHERE email = ? ` // SQL Queries to retrieve a list of all users const userStmt = ` SELECT id, email, password, token, name, gravatar, created, updated, admin, -github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret +github_login, github_token, bitbucket_login, bitbucket_token, bitbucket_secret, +gitlab_token FROM users ORDER BY name ASC ` diff --git a/pkg/handler/admin.go b/pkg/handler/admin.go index ff21090f..858e615c 100644 --- a/pkg/handler/admin.go +++ b/pkg/handler/admin.go @@ -3,6 +3,7 @@ package handler import ( "fmt" "net/http" + "net/url" "strconv" "time" @@ -170,6 +171,14 @@ func AdminSettingsUpdate(w http.ResponseWriter, r *http.Request, u *User) error settings.GitHubDomain = r.FormValue("GitHubDomain") settings.GitHubApiUrl = r.FormValue("GitHubApiUrl") + // update gitlab settings + settings.GitlabApiUrl = r.FormValue("GitlabApiUrl") + glUrl, err := url.Parse(settings.GitlabApiUrl) + if err != nil { + return RenderError(w, err, http.StatusBadRequest) + } + settings.GitlabDomain = glUrl.Host + // update smtp settings settings.SmtpServer = r.FormValue("SmtpServer") settings.SmtpPort = r.FormValue("SmtpPort") @@ -252,6 +261,8 @@ func InstallPost(w http.ResponseWriter, r *http.Request) error { settings.Scheme = r.FormValue("Scheme") settings.GitHubApiUrl = "https://api.github.com" settings.GitHubDomain = "github.com" + settings.GitlabApiUrl = "https://gitlab.com" + settings.GitlabDomain = "gitlab.com" database.SaveSettings(&settings) // add the user to the session object diff --git a/pkg/handler/auth.go b/pkg/handler/auth.go index 14219a2f..0949c3b4 100644 --- a/pkg/handler/auth.go +++ b/pkg/handler/auth.go @@ -6,10 +6,10 @@ import ( "github.com/drone/drone/pkg/database" . "github.com/drone/drone/pkg/model" - "github.com/drone/go-github/github" - "github.com/drone/go-github/oauth2" "github.com/drone/go-bitbucket/bitbucket" "github.com/drone/go-bitbucket/oauth1" + "github.com/drone/go-github/github" + "github.com/drone/go-github/oauth2" ) // Create the User session. diff --git a/pkg/handler/gitlab.go b/pkg/handler/gitlab.go new file mode 100644 index 00000000..f22c9d88 --- /dev/null +++ b/pkg/handler/gitlab.go @@ -0,0 +1,333 @@ +package handler + +import ( + "database/sql" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" + + "github.com/drone/drone/pkg/build/script" + "github.com/drone/drone/pkg/database" + . "github.com/drone/drone/pkg/model" + "github.com/drone/drone/pkg/queue" + "github.com/plouc/go-gitlab-client" +) + +type GitlabHandler struct { + queue *queue.Queue + apiPath string +} + +func NewGitlabHandler(queue *queue.Queue) *GitlabHandler { + return &GitlabHandler{ + queue: queue, + apiPath: "/api/v3", + } +} + +func (g *GitlabHandler) Add(w http.ResponseWriter, r *http.Request, u *User) error { + settings := database.SettingsMust() + teams, err := database.ListTeams(u.ID) + if err != nil { + return err + } + data := struct { + User *User + Teams []*Team + Settings *Settings + }{u, teams, settings} + // if the user hasn't linked their GitLab account + // render a different template + if len(u.GitlabToken) == 0 { + return RenderTemplate(w, "gitlab_link.html", &data) + } + // otherwise display the template for adding + // a new GitLab repository. + return RenderTemplate(w, "gitlab_add.html", &data) +} + +func (g *GitlabHandler) Link(w http.ResponseWriter, r *http.Request, u *User) error { + token := strings.TrimSpace(r.FormValue("token")) + + if len(u.GitlabToken) == 0 || token != u.GitlabToken && len(token) > 0 { + u.GitlabToken = token + settings := database.SettingsMust() + gl := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, u.GitlabToken) + _, err := gl.CurrentUser() + if err != nil { + return fmt.Errorf("Private Token is not valid: %q", err) + } + if err := database.SaveUser(u); err != nil { + return RenderError(w, err, http.StatusBadRequest) + } + } + + http.Redirect(w, r, "/new/gitlab", http.StatusSeeOther) + return nil +} + +func (g *GitlabHandler) ReLink(w http.ResponseWriter, r *http.Request, u *User) error { + data := struct { + User *User + }{u} + return RenderTemplate(w, "gitlab_link.html", &data) +} + +func (g *GitlabHandler) Create(w http.ResponseWriter, r *http.Request, u *User) error { + teamName := r.FormValue("team") + owner := r.FormValue("owner") + name := r.FormValue("name") + + repo, err := g.newGitlabRepo(u, owner, name) + if err != nil { + return err + } + + if len(teamName) > 0 { + team, err := database.GetTeamSlug(teamName) + if err != nil { + return fmt.Errorf("Unable to find Team %s.", teamName) + } + + // user must be an admin member of the team + if ok, _ := database.IsMemberAdmin(u.ID, team.ID); !ok { + return fmt.Errorf("Invalid permission to access Team %s.", teamName) + } + repo.TeamID = team.ID + } + + // Save to the database + if err := database.SaveRepo(repo); err != nil { + return fmt.Errorf("Error saving repository to the database. %s", err) + } + + return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) +} + +func (g *GitlabHandler) newGitlabRepo(u *User, owner, name string) (*Repo, error) { + settings := database.SettingsMust() + gl := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, u.GitlabToken) + + project, err := gl.Project(ns(owner, name)) + if err != nil { + return nil, err + } + + var cloneUrl string + if project.Public { + cloneUrl = project.HttpRepoUrl + } else { + cloneUrl = project.SshRepoUrl + } + + repo, err := NewRepo(settings.GitlabDomain, owner, name, ScmGit, cloneUrl) + if err != nil { + return nil, err + } + + repo.UserID = u.ID + repo.Private = !project.Public + if repo.Private { + // name the key + keyName := fmt.Sprintf("%s@%s", repo.Owner, settings.Domain) + + // TODO: (fudanchii) check if we already opted to use UserKey + + // create the github key, or update if one already exists + if err := gl.AddProjectDeployKey(ns(owner, name), keyName, repo.PublicKey); err != nil { + return nil, fmt.Errorf("Unable to add Public Key to your GitLab repository.") + } + } + + link := fmt.Sprintf("%s://%s/hook/gitlab?id=%s", settings.Scheme, settings.Domain, repo.Slug) + if err := gl.AddProjectHook(ns(owner, name), link, true, false, true); err != nil { + return nil, fmt.Errorf("Unable to add Hook to your GitLab repository.") + } + + return repo, err +} + +func (g *GitlabHandler) Hook(w http.ResponseWriter, r *http.Request) error { + rID := r.FormValue("id") + repo, err := database.GetRepoSlug(rID) + if err != nil { + return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + } + + user, err := database.GetUser(repo.UserID) + if err != nil { + return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + } + + payload, _ := ioutil.ReadAll(r.Body) + parsed, err := gogitlab.ParseHook(payload) + if err != nil { + return err + } + if parsed.ObjectKind == "merge_request" { + fmt.Println(string(payload)) + if err := g.PullRequestHook(parsed, repo, user); err != nil { + return err + } + return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) + } + + if len(parsed.After) == 0 { + return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + } + + _, err = database.GetCommitHash(parsed.After, repo.ID) + if err != nil && err != sql.ErrNoRows { + fmt.Println("commit already exists") + return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway) + } + + commit := &Commit{} + commit.RepoID = repo.ID + commit.Branch = parsed.Branch() + commit.Hash = parsed.After + commit.Status = "Pending" + commit.Created = time.Now().UTC() + + head := parsed.Head() + commit.Message = head.Message + commit.Timestamp = head.Timestamp + if head.Author != nil { + commit.SetAuthor(head.Author.Email) + } else { + commit.SetAuthor(parsed.UserName) + } + + // get the github settings from the database + settings := database.SettingsMust() + + // get the drone.yml file from GitHub + client := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, user.GitlabToken) + + content, err := client.RepoRawFile(ns(repo.Owner, repo.Name), commit.Hash, ".drone.yml") + if err != nil { + msg := "No .drone.yml was found in this repository. You need to add one.\n" + if err := saveFailedBuild(commit, msg); err != nil { + return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + + // parse the build script + buildscript, err := script.ParseBuild(content, repo.Params) + if err != nil { + msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n" + if err := saveFailedBuild(commit, msg); err != nil { + return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + + // save the commit to the database + if err := database.SaveCommit(commit); err != nil { + return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + + // save the build to the database + build := &Build{} + build.Slug = "1" // TODO + build.CommitID = commit.ID + build.Created = time.Now().UTC() + build.Status = "Pending" + if err := database.SaveBuild(build); err != nil { + return RenderText(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + + g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript}) + + // OK! + return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) + +} + +func (g *GitlabHandler) PullRequestHook(p *gogitlab.HookPayload, repo *Repo, user *User) error { + obj := p.ObjectAttributes + + // Gitlab may trigger multiple hooks upon updating merge requests status + // only build when it was just opened and the merge hasn't been checked yet. + if !(obj.State == "opened" && obj.MergeStatus == "unchecked") { + fmt.Println("Ignore GitLab Merge Requests") + return nil + } + + settings := database.SettingsMust() + + client := gogitlab.NewGitlab(settings.GitlabApiUrl, g.apiPath, user.GitlabToken) + + // GitLab merge-requests hook doesn't include repository data. + // Have to fetch it manually + src, err := client.RepoBranch(strconv.Itoa(obj.SourceProjectId), obj.SourceBranch) + if err != nil { + return err + } + + _, err = database.GetCommitHash(src.Commit.Id, repo.ID) + if err != nil && err != sql.ErrNoRows { + fmt.Println("commit already exists") + return err + } + + commit := &Commit{} + commit.RepoID = repo.ID + commit.Branch = src.Name + commit.Hash = src.Commit.Id + commit.Status = "Pending" + commit.Created = time.Now().UTC() + commit.PullRequest = strconv.Itoa(obj.IId) + + commit.Message = src.Commit.Message + commit.Timestamp = src.Commit.AuthoredDateRaw + commit.SetAuthor(src.Commit.Author.Email) + + content, err := client.RepoRawFile(strconv.Itoa(obj.SourceProjectId), commit.Hash, ".drone.yml") + if err != nil { + msg := "No .drone.yml was found in this repository. You need to add one.\n" + if err := saveFailedBuild(commit, msg); err != nil { + return fmt.Errorf("Failed to save build: %q", err) + } + return fmt.Errorf("Error to fetch build script: %q", err) + } + + // parse the build script + buildscript, err := script.ParseBuild(content, repo.Params) + if err != nil { + msg := "Could not parse your .drone.yml file. It needs to be a valid drone yaml file.\n\n" + err.Error() + "\n" + if err := saveFailedBuild(commit, msg); err != nil { + return fmt.Errorf("Failed to save build: %q", err) + } + return fmt.Errorf("Failed to parse build script: %q", err) + } + + // save the commit to the database + if err := database.SaveCommit(commit); err != nil { + return fmt.Errorf("Failed to save commit: %q", err) + } + + // save the build to the database + build := &Build{} + build.Slug = "1" // TODO + build.CommitID = commit.ID + build.Created = time.Now().UTC() + build.Status = "Pending" + if err := database.SaveBuild(build); err != nil { + return fmt.Errorf("Failed to save build: %q", err) + } + + g.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript}) + + return nil +} + +// ns namespaces user and repo. +// Returns user%2Frepo +func ns(user, repo string) string { + return fmt.Sprintf("%s%%2F%s", user, repo) +} diff --git a/pkg/handler/hooks.go b/pkg/handler/hooks.go index 89b09886..57c0e017 100644 --- a/pkg/handler/hooks.go +++ b/pkg/handler/hooks.go @@ -10,8 +10,8 @@ import ( "github.com/drone/drone/pkg/database" . "github.com/drone/drone/pkg/model" "github.com/drone/drone/pkg/queue" - "github.com/drone/go-github/github" "github.com/drone/go-bitbucket/bitbucket" + "github.com/drone/go-github/github" ) type HookHandler struct { diff --git a/pkg/handler/repos.go b/pkg/handler/repos.go index 20b3c9c3..93e83099 100644 --- a/pkg/handler/repos.go +++ b/pkg/handler/repos.go @@ -7,8 +7,8 @@ import ( "github.com/drone/drone/pkg/channel" "github.com/drone/drone/pkg/database" . "github.com/drone/drone/pkg/model" - "github.com/drone/go-github/github" "github.com/drone/go-bitbucket/bitbucket" + "github.com/drone/go-github/github" "launchpad.net/goyaml" ) diff --git a/pkg/handler/testing/gitlab_test.go b/pkg/handler/testing/gitlab_test.go new file mode 100644 index 00000000..77fca680 --- /dev/null +++ b/pkg/handler/testing/gitlab_test.go @@ -0,0 +1,328 @@ +package testing + +import ( + "database/sql" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/drone/drone/pkg/database" + "github.com/drone/drone/pkg/handler" + "github.com/drone/drone/pkg/model" + "github.com/drone/drone/pkg/queue" + + dbtest "github.com/drone/drone/pkg/database/testing" + . "github.com/smartystreets/goconvey/convey" +) + +// Tests the ability to create GitHub repositories. +func Test_GitLabCreate(t *testing.T) { + // seed the database with values + SetupGitlabFixtures() + defer TeardownGitlabFixtures() + + q := &queue.Queue{} + gl := handler.NewGitlabHandler(q) + + // mock request + req := http.Request{} + req.Form = url.Values{} + + // get user that will add repositories + user, _ := database.GetUser(1) + settings := database.SettingsMust() + + Convey("Given request to setup gitlab repo", t, func() { + + Convey("When repository is public", func() { + req.Form.Set("owner", "example") + req.Form.Set("name", "public") + req.Form.Set("team", "") + res := httptest.NewRecorder() + err := gl.Create(res, &req, user) + repo, _ := database.GetRepoSlug(settings.GitlabDomain + "/example/public") + + Convey("The repository is created", func() { + So(err, ShouldBeNil) + So(repo, ShouldNotBeNil) + So(repo.ID, ShouldNotEqual, 0) + So(repo.Owner, ShouldEqual, "example") + So(repo.Name, ShouldEqual, "public") + So(repo.Host, ShouldEqual, settings.GitlabDomain) + So(repo.TeamID, ShouldEqual, 0) + So(repo.UserID, ShouldEqual, user.ID) + So(repo.Private, ShouldEqual, false) + So(repo.SCM, ShouldEqual, "git") + }) + Convey("The repository is public", func() { + So(repo.Private, ShouldEqual, false) + }) + }) + + Convey("When repository is private", func() { + req.Form.Set("owner", "example") + req.Form.Set("name", "private") + req.Form.Set("team", "") + res := httptest.NewRecorder() + err := gl.Create(res, &req, user) + repo, _ := database.GetRepoSlug(settings.GitlabDomain + "/example/private") + + Convey("The repository is created", func() { + So(err, ShouldBeNil) + So(repo, ShouldNotBeNil) + So(repo.ID, ShouldNotEqual, 0) + }) + Convey("The repository is private", func() { + So(repo.Private, ShouldEqual, true) + }) + }) + + Convey("When repository is not found", func() { + req.Form.Set("owner", "example") + req.Form.Set("name", "notfound") + req.Form.Set("team", "") + res := httptest.NewRecorder() + err := gl.Create(res, &req, user) + + Convey("The result is an error", func() { + So(err, ShouldNotBeNil) + So(err.Error(), ShouldStartWith, "*Gitlab.buildAndExecRequestRaw") + }) + + Convey("The repository is not created", func() { + _, err := database.GetRepoSlug("example/notfound") + So(err, ShouldNotBeNil) + So(err, ShouldEqual, sql.ErrNoRows) + }) + }) + + Convey("When repository hook is not writable", func() { + req.Form.Set("owner", "example") + req.Form.Set("name", "hookerr") + req.Form.Set("team", "") + res := httptest.NewRecorder() + err := gl.Create(res, &req, user) + + Convey("The result is an error", func() { + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "Unable to add Hook to your GitLab repository.") + }) + + Convey("The repository is not created", func() { + _, err := database.GetRepoSlug("example/hookerr") + So(err, ShouldNotBeNil) + So(err, ShouldEqual, sql.ErrNoRows) + }) + }) + + Convey("When repository ssh key is not writable", func() { + req.Form.Set("owner", "example") + req.Form.Set("name", "keyerr") + req.Form.Set("team", "") + res := httptest.NewRecorder() + err := gl.Create(res, &req, user) + + Convey("The result is an error", func() { + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "Unable to add Public Key to your GitLab repository.") + }) + + Convey("The repository is not created", func() { + _, err := database.GetRepoSlug("example/keyerr") + So(err, ShouldNotBeNil) + So(err, ShouldEqual, sql.ErrNoRows) + }) + }) + + Convey("When a team is provided", func() { + req.Form.Set("owner", "example") + req.Form.Set("name", "team") + req.Form.Set("team", "drone") + res := httptest.NewRecorder() + + // invoke handler + err := gl.Create(res, &req, user) + team, _ := database.GetTeamSlug("drone") + repo, _ := database.GetRepoSlug(settings.GitlabDomain + "/example/team") + + Convey("The repository is created", func() { + So(err, ShouldBeNil) + So(repo, ShouldNotBeNil) + So(repo.ID, ShouldNotEqual, 0) + }) + + Convey("The team should be set", func() { + So(repo.TeamID, ShouldEqual, team.ID) + }) + }) + + Convey("When a team is not found", func() { + req.Form.Set("owner", "example") + req.Form.Set("name", "public") + req.Form.Set("team", "faketeam") + res := httptest.NewRecorder() + err := gl.Create(res, &req, user) + + Convey("The result is an error", func() { + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "Unable to find Team faketeam.") + }) + }) + + Convey("When a team is forbidden", func() { + req.Form.Set("owner", "example") + req.Form.Set("name", "public") + req.Form.Set("team", "golang") + res := httptest.NewRecorder() + err := gl.Create(res, &req, user) + + Convey("The result is an error", func() { + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "Invalid permission to access Team golang.") + }) + }) + }) +} + +// this code should be refactored and centralized, but for now +// it is just proof-of-concepting a testing strategy, so we'll +// revisit later. + +// server is a test HTTP server used to provide mock API responses. +var glServer *httptest.Server + +func SetupGitlabFixtures() { + dbtest.Setup() + + // test server + mux := http.NewServeMux() + glServer = httptest.NewServer(mux) + url, _ := url.Parse(glServer.URL) + + // set database to use a localhost url for GitHub + settings := model.Settings{} + settings.GitlabApiUrl = url.String() // normall would be "https://api.github.com" + settings.GitlabDomain = url.Host // normally would be "github.com" + settings.Scheme = url.Scheme + settings.Domain = "localhost" + database.SaveSettings(&settings) + + // ----------------------------------------------------------------------------------- + // fixture to return a public repository and successfully + // create a commit hook. + + mux.HandleFunc("/api/v3/projects/example/public", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{ + "name": "public", + "path_with_namespace": "example/public", + "public": true + }`) + }) + + mux.HandleFunc("/api/v3/projects/example/public/hooks", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{ + "url": "https://example.com/example/public/hooks/1", + "id": 1 + }`) + }) + + // ----------------------------------------------------------------------------------- + // fixture to return a private repository and successfully + // create a commit hook and ssh deploy key + + mux.HandleFunc("/api/v3/projects/example/private", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{ + "name": "private", + "path_with_namespace": "example/private", + "public": false + }`) + }) + + mux.HandleFunc("/api/v3/projects/example/private/hooks", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{ + "url": "https://example.com/example/private/hooks/1", + "id": 1 + }`) + }) + + mux.HandleFunc("/api/v3/projects/example/private/keys", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{ + "id": 1, + "key": "ssh-rsa AAA...", + "url": "https://api.github.com/user/keys/1", + "title": "octocat@octomac" + }`) + }) + + // ----------------------------------------------------------------------------------- + // fixture to return a not found when accessing a github repository. + + mux.HandleFunc("/api/v3/projects/example/notfound", func(w http.ResponseWriter, r *http.Request) { + http.NotFound(w, r) + }) + + // ----------------------------------------------------------------------------------- + // fixture to return a public repository and successfully + // create a commit hook. + + mux.HandleFunc("/api/v3/projects/example/hookerr", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{ + "name": "hookerr", + "path_with_namespace": "example/hookerr", + "public": true + }`) + }) + + mux.HandleFunc("/api/v3/projects/example/hookerr/hooks", func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Forbidden", http.StatusForbidden) + }) + + // ----------------------------------------------------------------------------------- + // fixture to return a private repository and successfully + // create a commit hook and ssh deploy key + + mux.HandleFunc("/api/v3/projects/example/keyerr", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{ + "name": "keyerr", + "path_with_namespace": "example/keyerr", + "public": false + }`) + }) + + mux.HandleFunc("/api/v3/projects/example/keyerr/hooks", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{ + "url": "https://api.github.com/api/v3/projects/example/keyerr/hooks/1", + "id": 1 + }`) + }) + + mux.HandleFunc("/api/v3/projects/example/keyerr/keys", func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Forbidden", http.StatusForbidden) + }) + + // ----------------------------------------------------------------------------------- + // fixture to return a public repository and successfully to + // test adding a team. + + mux.HandleFunc("/api/v3/projects/example/team", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{ + "name": "team", + "path_with_namespace": "example/team", + "public": true + }`) + }) + + mux.HandleFunc("/api/v3/projects/example/team/hooks", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{ + "url": "https://api.github.com/api/v3/projects/example/team/hooks/1", + "id": 1 + }`) + }) +} + +func TeardownGitlabFixtures() { + dbtest.Teardown() + glServer.Close() +} diff --git a/pkg/model/settings.go b/pkg/model/settings.go index 978b289f..8bab931e 100644 --- a/pkg/model/settings.go +++ b/pkg/model/settings.go @@ -32,6 +32,10 @@ type Settings struct { BitbucketKey string `meddler:"bitbucket_key"` BitbucketSecret string `meddler:"bitbucket_secret"` + // GitLab Domain + GitlabDomain string `meddler:"gitlab_domain"` + GitlabApiUrl string `meddler:"gitlab_apiurl"` + // Domain of the server, eg drone.io Domain string `meddler:"hostname"` diff --git a/pkg/model/user.go b/pkg/model/user.go index 55bddb51..41a76d8c 100644 --- a/pkg/model/user.go +++ b/pkg/model/user.go @@ -41,6 +41,8 @@ type User struct { BitbucketLogin string `meddler:"bitbucket_login" json:"-"` BitbucketToken string `meddler:"bitbucket_token" json:"-"` BitbucketSecret string `meddler:"bitbucket_secret" json:"-"` + + GitlabToken string `meddler:"gitlab_token" json:"-"` } // Creates a new User from the given Name and Email. diff --git a/pkg/template/pages/admin_settings.html b/pkg/template/pages/admin_settings.html index 5a1473d5..a6f825a7 100644 --- a/pkg/template/pages/admin_settings.html +++ b/pkg/template/pages/admin_settings.html @@ -50,6 +50,13 @@ +