diff --git a/cmd/droned/drone.go b/cmd/droned/drone.go index 4c8d552ff..56bcd4546 100644 --- a/cmd/droned/drone.go +++ b/cmd/droned/drone.go @@ -16,7 +16,6 @@ import ( "github.com/drone/drone/pkg/channel" "github.com/drone/drone/pkg/database" "github.com/drone/drone/pkg/handler" - "github.com/drone/drone/pkg/handler/gitlab" "github.com/drone/drone/pkg/queue" ) @@ -117,6 +116,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)) @@ -136,13 +136,16 @@ func setupHandlers() { m.Post("/new/github.com", handler.UserHandler(handler.RepoCreateGithub)) m.Get("/new/github.com", handler.UserHandler(handler.RepoAdd)) - // handlers for setting up your GitLab repository - m.Post("/new/gitlab", handler.UserHandler(gitlab.RepoCreate)) - m.Get("/new/gitlab", handler.UserHandler(gitlab.RepoAdd)) - // handlers for linking your GitHub account m.Get("/auth/login/github", handler.UserHandler(handler.LinkGithub)) + // 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)) + // handlers for dashboard pages m.Get("/dashboard/team/:team", handler.UserHandler(handler.TeamShow)) m.Get("/dashboard", handler.UserHandler(handler.UserShow)) @@ -184,7 +187,7 @@ func setupHandlers() { m.Post("/hook/github.com", handler.ErrorHandler(hookHandler.Hook)) // handlers for GitLab post-commit hooks - m.Post("/hook/gitlab", handler.ErrorHandler(gitlab.Hook)) + //m.Post("/hook/gitlab", handler.ErrorHandler(gitlab.Hook)) // handlers for first-time installation m.Get("/install", handler.ErrorHandler(handler.Install)) 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 000000000..f0d6ee5ca --- /dev/null +++ b/pkg/database/migrate/20140328201430_add_gitlab_columns.go @@ -0,0 +1,34 @@ +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', gitlab_apiurl='https://gitlab.com'`); err != nil { + return err + } + _, err := mg.AddColumn("users", mg.T.String("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 1748f18f8..c745a4edb 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 883b87d3d..fb22d2260 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 d906f7174..c252204c5 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/handler/gitlab.go b/pkg/handler/gitlab.go new file mode 100644 index 000000000..d38f6ca7a --- /dev/null +++ b/pkg/handler/gitlab.go @@ -0,0 +1,129 @@ +package handler + +import ( + "fmt" + "net/http" + + "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 { + var err error + return err +} + +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 +} + +// ns namespaces user and repo. +// Returns user%2Frepo +func ns(user, repo string) string { + return fmt.Sprintf("%s%%252F%s", user, repo) +} diff --git a/pkg/handler/testing/gitlab_test.go b/pkg/handler/testing/gitlab_test.go new file mode 100644 index 000000000..aa4c623d5 --- /dev/null +++ b/pkg/handler/testing/gitlab_test.go @@ -0,0 +1,330 @@ +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/queue" + "github.com/drone/drone/pkg/model" + + 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(), ShouldEqual, "*Gitlab.buildAndExecRequest failed: 404 Not Found") + }) + + 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%2Fpublic", 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%2Fpublic/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%2Fprivate", 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%2Fprivate/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%2Fprivate/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%2Fnotfound", 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%2Fhookerr", 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%2Fhookerr/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%2Fkeyerr", 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%2Fkeyerr/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%2Fkeyerr/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%2Fteam", 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%2Fteam/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 978b289fe..8bab931e0 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 55bddb51f..41a76d8cb 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/plugin/publish/publish.go b/pkg/plugin/publish/publish.go index 33a4dc43c..9fd336030 100644 --- a/pkg/plugin/publish/publish.go +++ b/pkg/plugin/publish/publish.go @@ -8,7 +8,7 @@ import ( // for publishing build artifacts when // a Build has succeeded type Publish struct { - S3 *S3 `yaml:"s3,omitempty"` + S3 *S3 `yaml:"s3,omitempty"` Swift *Swift `yaml:"swift,omitempty"` } diff --git a/pkg/template/pages/github_add.html b/pkg/template/pages/github_add.html index f1c5c233a..523266795 100644 --- a/pkg/template/pages/github_add.html +++ b/pkg/template/pages/github_add.html @@ -15,6 +15,7 @@