From 8f4f747c864f7bfd4b11653127f2a7c2ac911cfa Mon Sep 17 00:00:00 2001 From: Ali Sabil Date: Fri, 28 Mar 2014 19:40:26 +0100 Subject: [PATCH] Add Bitbucket integration --- cmd/droned/drone.go | 14 +++- pkg/handler/auth.go | 77 +++++++++++++++++++ pkg/handler/hooks.go | 102 ++++++++++++++++++++++++- pkg/handler/repos.go | 102 ++++++++++++++++++++++++- pkg/template/pages/admin_settings.html | 8 +- pkg/template/pages/bitbucket_add.html | 99 ++++++++++++++++++++++++ pkg/template/pages/bitbucket_link.html | 31 ++++++++ pkg/template/pages/github_add.html | 2 +- pkg/template/pages/github_link.html | 2 +- pkg/template/template.go | 2 + 10 files changed, 426 insertions(+), 13 deletions(-) create mode 100644 pkg/template/pages/bitbucket_add.html create mode 100644 pkg/template/pages/bitbucket_link.html diff --git a/cmd/droned/drone.go b/cmd/droned/drone.go index 93dfa9c6..607716fe 100644 --- a/cmd/droned/drone.go +++ b/cmd/droned/drone.go @@ -133,11 +133,18 @@ func setupHandlers() { // handlers for setting up your GitHub repository m.Post("/new/github.com", handler.UserHandler(handler.RepoCreateGithub)) - m.Get("/new/github.com", handler.UserHandler(handler.RepoAdd)) + m.Get("/new/github.com", handler.UserHandler(handler.RepoAddGithub)) + + // 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 dashboard pages m.Get("/dashboard/team/:team", handler.UserHandler(handler.TeamShow)) m.Get("/dashboard", handler.UserHandler(handler.UserShow)) @@ -176,7 +183,10 @@ func setupHandlers() { m.Get("/account/admin/users", handler.AdminHandler(handler.AdminUserList)) // handlers for GitHub post-commit hooks - m.Post("/hook/github.com", handler.ErrorHandler(hookHandler.Hook)) + m.Post("/hook/github.com", handler.ErrorHandler(hookHandler.HookGithub)) + + // handlers for Bitbucket post-commit hooks + m.Post("/hook/bitbucket.org", handler.ErrorHandler(hookHandler.HookBitbucket)) // handlers for first-time installation m.Get("/install", handler.ErrorHandler(handler.Install)) diff --git a/pkg/handler/auth.go b/pkg/handler/auth.go index 77166b55..14219a2f 100644 --- a/pkg/handler/auth.go +++ b/pkg/handler/auth.go @@ -8,6 +8,8 @@ import ( . "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" ) // Create the User session. @@ -94,3 +96,78 @@ func LinkGithub(w http.ResponseWriter, r *http.Request, u *User) error { http.Redirect(w, r, "/new/github.com", http.StatusSeeOther) return nil } + +func LinkBitbucket(w http.ResponseWriter, r *http.Request, u *User) error { + + // get settings from database + settings := database.SettingsMust() + + // bitbucket oauth1 consumer + var consumer = oauth1.Consumer{ + RequestTokenURL: "https://bitbucket.org/api/1.0/oauth/request_token/", + AuthorizationURL: "https://bitbucket.org/!api/1.0/oauth/authenticate", + AccessTokenURL: "https://bitbucket.org/api/1.0/oauth/access_token/", + CallbackURL: settings.URL().String() + "/auth/login/bitbucket", + ConsumerKey: settings.BitbucketKey, + ConsumerSecret: settings.BitbucketSecret, + } + + // get the oauth verifier + verifier := r.FormValue("oauth_verifier") + if len(verifier) == 0 { + // Generate a Request Token + requestToken, err := consumer.RequestToken() + if err != nil { + return err + } + + // add the request token as a signed cookie + SetCookie(w, r, "bitbucket_token", requestToken.Encode()) + + url, _ := consumer.AuthorizeRedirect(requestToken) + http.Redirect(w, r, url, http.StatusSeeOther) + return nil + } + + // remove bitbucket token data once before redirecting + // back to the application. + defer DelCookie(w, r, "bitbucket_token") + + // get the tokens from the request + requestTokenStr := GetCookie(r, "bitbucket_token") + requestToken, err := oauth1.ParseRequestTokenStr(requestTokenStr) + if err != nil { + return err + } + + // exchange for an access token + accessToken, err := consumer.AuthorizeToken(requestToken, verifier) + if err != nil { + return err + } + + // create the Bitbucket client + client := bitbucket.New( + settings.BitbucketKey, + settings.BitbucketSecret, + accessToken.Token(), + accessToken.Secret(), + ) + + // get the currently authenticated Bitbucket User + user, err := client.Users.Current() + if err != nil { + return err + } + + // update the user account + u.BitbucketLogin = user.User.Username + u.BitbucketToken = accessToken.Token() + u.BitbucketSecret = accessToken.Secret() + if err := database.SaveUser(u); err != nil { + return err + } + + http.Redirect(w, r, "/new/bitbucket.org", http.StatusSeeOther) + return nil +} diff --git a/pkg/handler/hooks.go b/pkg/handler/hooks.go index 8a572d19..89b09886 100644 --- a/pkg/handler/hooks.go +++ b/pkg/handler/hooks.go @@ -11,6 +11,7 @@ import ( . "github.com/drone/drone/pkg/model" "github.com/drone/drone/pkg/queue" "github.com/drone/go-github/github" + "github.com/drone/go-bitbucket/bitbucket" ) type HookHandler struct { @@ -23,9 +24,9 @@ func NewHookHandler(queue *queue.Queue) *HookHandler { } } -// Processes a generic POST-RECEIVE hook and +// Processes a generic POST-RECEIVE GitHub hook and // attempts to trigger a build. -func (h *HookHandler) Hook(w http.ResponseWriter, r *http.Request) error { +func (h *HookHandler) HookGithub(w http.ResponseWriter, r *http.Request) error { // handle github ping if r.Header.Get("X-Github-Event") == "ping" { return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) @@ -34,7 +35,7 @@ func (h *HookHandler) Hook(w http.ResponseWriter, r *http.Request) error { // if this is a pull request route // to a different handler if r.Header.Get("X-Github-Event") == "pull_request" { - h.PullRequestHook(w, r) + h.PullRequestHookGithub(w, r) return nil } @@ -175,7 +176,7 @@ func (h *HookHandler) Hook(w http.ResponseWriter, r *http.Request) error { return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) } -func (h *HookHandler) PullRequestHook(w http.ResponseWriter, r *http.Request) { +func (h *HookHandler) PullRequestHookGithub(w http.ResponseWriter, r *http.Request) { // get the payload of the message // this should contain a json representation of the // repository and commit details @@ -291,6 +292,99 @@ func (h *HookHandler) PullRequestHook(w http.ResponseWriter, r *http.Request) { RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) } +// Processes a generic POST-RECEIVE Bitbucket hook and +// attempts to trigger a build. +func (h *HookHandler) HookBitbucket(w http.ResponseWriter, r *http.Request) error { + // get the payload from the request + payload := r.FormValue("payload") + + // parse the post-commit hook + hook, err := bitbucket.ParseHook([]byte(payload)) + if err != nil { + return err + } + + // get the repo from the URL + repoId := r.FormValue("id") + + // get the repo from the database, return error if not found + repo, err := database.GetRepoSlug(repoId) + if err != nil { + return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + } + + // Get the user that owns the repository + user, err := database.GetUser(repo.UserID) + if err != nil { + return RenderText(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + + // Verify that the commit doesn't already exist. + // We should never build the same commit twice. + _, err = database.GetCommitHash(hook.Commits[len(hook.Commits)-1].Hash, repo.ID) + if err != nil && err != sql.ErrNoRows { + return RenderText(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway) + } + + commit := &Commit{} + commit.RepoID = repo.ID + commit.Branch = hook.Commits[len(hook.Commits)-1].Branch + commit.Hash = hook.Commits[len(hook.Commits)-1].Hash + commit.Status = "Pending" + commit.Created = time.Now().UTC() + commit.Message = hook.Commits[len(hook.Commits)-1].Message + commit.Timestamp = time.Now().UTC().String() + commit.SetAuthor(hook.Commits[len(hook.Commits)-1].Author) + + // get the github settings from the database + settings := database.SettingsMust() + + // create the Bitbucket client + client := bitbucket.New( + settings.BitbucketKey, + settings.BitbucketSecret, + user.BitbucketToken, + user.BitbucketSecret, + ) + + // get the yaml from the database + raw, err := client.Sources.Find(repo.Owner, repo.Name, commit.Hash, ".drone.yml") + if err != nil { + return RenderText(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + } + + // parse the build script + buildscript, err := script.ParseBuild([]byte(raw.Data), 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) + } + + // send the build to the queue + h.queue.Add(&queue.BuildTask{Repo: repo, Commit: commit, Build: build, Script: buildscript}) + + // OK! + return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) +} + // Helper method for saving a failed build or commit in the case where it never starts to build. // This can happen if the yaml is bad or doesn't exist. func saveFailedBuild(commit *Commit, msg string) error { diff --git a/pkg/handler/repos.go b/pkg/handler/repos.go index 36b15ed4..405f1928 100644 --- a/pkg/handler/repos.go +++ b/pkg/handler/repos.go @@ -8,6 +8,7 @@ import ( "github.com/drone/drone/pkg/database" . "github.com/drone/drone/pkg/model" "github.com/drone/go-github/github" + "github.com/drone/go-bitbucket/bitbucket" "launchpad.net/goyaml" ) @@ -52,7 +53,7 @@ func RepoDashboard(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) return RenderTemplate(w, "repo_dashboard.html", &data) } -func RepoAdd(w http.ResponseWriter, r *http.Request, u *User) error { +func RepoAddGithub(w http.ResponseWriter, r *http.Request, u *User) error { settings := database.SettingsMust() teams, err := database.ListTeams(u.ID) if err != nil { @@ -73,6 +74,27 @@ func RepoAdd(w http.ResponseWriter, r *http.Request, u *User) error { return RenderTemplate(w, "github_add.html", &data) } +func RepoAddBitbucket(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 Bitbucket account + // render a different template + if len(u.BitbucketToken) == 0 { + return RenderTemplate(w, "bitbucket_link.html", &data) + } + // otherwise display the template for adding + // a new Bitbucket repository. + return RenderTemplate(w, "bitbucket_add.html", &data) +} + func RepoCreateGithub(w http.ResponseWriter, r *http.Request, u *User) error { teamName := r.FormValue("team") owner := r.FormValue("owner") @@ -146,6 +168,84 @@ func RepoCreateGithub(w http.ResponseWriter, r *http.Request, u *User) error { return RenderText(w, http.StatusText(http.StatusOK), http.StatusOK) } +func RepoCreateBitbucket(w http.ResponseWriter, r *http.Request, u *User) error { + teamName := r.FormValue("team") + owner := r.FormValue("owner") + name := r.FormValue("name") + + // get the bitbucket settings from the database + settings := database.SettingsMust() + + // create the Bitbucket client + client := bitbucket.New( + settings.BitbucketKey, + settings.BitbucketSecret, + u.BitbucketToken, + u.BitbucketSecret, + ) + + bitbucketRepo, err := client.Repos.Find(owner, name) + if err != nil { + return fmt.Errorf("Unable to find Bitbucket repository %s/%s.", owner, name) + } + + repo, err := NewBitbucketRepo(owner, name, bitbucketRepo.Private) + if err != nil { + return err + } + + repo.UserID = u.ID + repo.Private = bitbucketRepo.Private + + // if the user chose to assign to a team account + // we need to retrieve the team, verify the user + // has access, and then set the team id. + 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 + } + + // if the repository is private we'll need + // to upload a bitbucket key to the repository + if repo.Private { + // name the key + keyName := fmt.Sprintf("%s@%s", repo.Owner, settings.Domain) + + // create the bitbucket key, or update if one already exists + _, err := client.RepoKeys.CreateUpdate(owner, name, repo.PublicKey, keyName) + if err != nil { + return fmt.Errorf("Unable to add Public Key to your Bitbucket repository: %s", err) + } + } else { + + } + + // create a hook so that we get notified when code + // is pushed to the repository and can execute a build. + link := fmt.Sprintf("%s://%s/hook/bitbucket.org?id=%s", settings.Scheme, settings.Domain, repo.Slug) + + // add the hook + if _, err := client.Brokers.CreateUpdate(owner, name, link, bitbucket.BrokerTypePost); err != nil { + return fmt.Errorf("Unable to add Hook to your Bitbucket repository. %s", err.Error()) + } + + // 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) +} + // Repository Settings func RepoSettingsForm(w http.ResponseWriter, r *http.Request, u *User, repo *Repo) error { diff --git a/pkg/template/pages/admin_settings.html b/pkg/template/pages/admin_settings.html index d3f1b66e..5a1473d5 100644 --- a/pkg/template/pages/admin_settings.html +++ b/pkg/template/pages/admin_settings.html @@ -50,12 +50,12 @@ -
+
Bitbucket OAuth Consumer Key and Secret.
- - + +
@@ -119,4 +119,4 @@ return false; }; -{{ end }} \ No newline at end of file +{{ end }} diff --git a/pkg/template/pages/bitbucket_add.html b/pkg/template/pages/bitbucket_add.html new file mode 100644 index 00000000..80b5886a --- /dev/null +++ b/pkg/template/pages/bitbucket_add.html @@ -0,0 +1,99 @@ +{{ define "title" }}Bitbucket · Add Repository{{ end }} + +{{ define "content" }} +
+
+

+ Repository Setup + Bitbucket +

+
+
+ +
+
+
+ +
+ +
+
+ Enter your repository details + Re-Link Account +
+
+
+
+ +
+ +
+
+
+
/
+
+
+ +
+ +
+
+
+
+
Select your Drone account
+
    +
  • + + + Me +
  • + {{ range .Teams }} +
  • + + + {{ .Name }} +
  • + {{ end }} +
+
+
+
+ + Cancel +
+
+
+
+
+{{ end }} + +{{ define "script" }} + +{{ end }} diff --git a/pkg/template/pages/bitbucket_link.html b/pkg/template/pages/bitbucket_link.html new file mode 100644 index 00000000..b323010b --- /dev/null +++ b/pkg/template/pages/bitbucket_link.html @@ -0,0 +1,31 @@ +{{ define "title" }}Bitbucket · Add Repository{{ end }} + +{{ define "content" }} +
+
+

+ Repository Setup + Bitbucket +

+
+
+ +
+
+
+ +
+ +
+
Link Your Bitbucket Account + Link Now +
+
+
+
+{{ end }} + +{{ define "script" }}{{ end }} diff --git a/pkg/template/pages/github_add.html b/pkg/template/pages/github_add.html index f1c5c233..7be09172 100644 --- a/pkg/template/pages/github_add.html +++ b/pkg/template/pages/github_add.html @@ -15,7 +15,7 @@ diff --git a/pkg/template/pages/github_link.html b/pkg/template/pages/github_link.html index a59d0294..1bb0e5d4 100644 --- a/pkg/template/pages/github_link.html +++ b/pkg/template/pages/github_link.html @@ -15,7 +15,7 @@ diff --git a/pkg/template/template.go b/pkg/template/template.go index 95fe3e85..c016aab4 100644 --- a/pkg/template/template.go +++ b/pkg/template/template.go @@ -82,6 +82,8 @@ func init() { "admin_settings.html", "github_add.html", "github_link.html", + "bitbucket_add.html", + "bitbucket_link.html", } // extract the base template as a string