harness-drone/remote/bitbucket/bitbucket.go
2016-04-29 12:39:56 -07:00

355 lines
9 KiB
Go

package bitbucket
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/drone/drone/model"
"github.com/drone/drone/remote"
"github.com/drone/drone/remote/bitbucket/internal"
"github.com/drone/drone/shared/httputil"
"golang.org/x/oauth2"
"golang.org/x/oauth2/bitbucket"
)
type config struct {
Client string
Secret string
}
// New returns a new remote Configuration for integrating with the Bitbucket
// repository hosting service at https://bitbucket.org
func New(client, secret string) remote.Remote {
return &config{
Client: client,
Secret: secret,
}
}
// helper function to return the bitbucket oauth2 client
func (c *config) newClient(u *model.User) *internal.Client {
return internal.NewClientToken(
c.Client,
c.Secret,
&oauth2.Token{
AccessToken: u.Token,
RefreshToken: u.Secret,
},
)
}
func (c *config) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) {
config := &oauth2.Config{
ClientID: c.Client,
ClientSecret: c.Secret,
Endpoint: bitbucket.Endpoint,
RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(req)),
}
var code = req.FormValue("code")
if len(code) == 0 {
http.Redirect(res, req, config.AuthCodeURL("drone"), http.StatusSeeOther)
return nil, nil
}
var token, err = config.Exchange(oauth2.NoContext, code)
if err != nil {
return nil, err
}
client := internal.NewClient(config.Client(oauth2.NoContext, token))
curr, err := client.FindCurrent()
if err != nil {
return nil, err
}
return convertUser(curr, token), nil
}
func (c *config) Auth(token, secret string) (string, error) {
client := internal.NewClientToken(
c.Client,
c.Secret,
&oauth2.Token{
AccessToken: token,
RefreshToken: secret,
},
)
user, err := client.FindCurrent()
if err != nil {
return "", err
}
return user.Login, nil
}
func (c *config) Refresh(user *model.User) (bool, error) {
config := &oauth2.Config{
ClientID: c.Client,
ClientSecret: c.Secret,
Endpoint: bitbucket.Endpoint,
}
// creates a token source with just the refresh token.
// this will ensure an access token is automatically
// requested.
source := config.TokenSource(
oauth2.NoContext, &oauth2.Token{RefreshToken: user.Secret})
// requesting the token automatically refreshes and
// returns a new access token.
token, err := source.Token()
if err != nil || len(token.AccessToken) == 0 {
return false, err
}
// update the user to include tne new access token
user.Token = token.AccessToken
user.Secret = token.RefreshToken
user.Expiry = token.Expiry.UTC().Unix()
return true, nil
}
func (c *config) Teams(u *model.User) ([]*model.Team, error) {
opts := &internal.ListTeamOpts{
PageLen: 100,
Role: "member",
}
resp, err := c.newClient(u).ListTeams(opts)
if err != nil {
return nil, err
}
return convertTeamList(resp.Values), nil
}
func (c *config) Repo(u *model.User, owner, name string) (*model.Repo, error) {
repo, err := c.newClient(u).FindRepo(owner, name)
if err != nil {
return nil, err
}
return convertRepo(repo), nil
}
func (c *config) Repos(u *model.User) ([]*model.RepoLite, error) {
client := c.newClient(u)
var repos []*model.RepoLite
// gets a list of all accounts to query, including the
// user's account and all team accounts.
logins := []string{u.Login}
resp, err := client.ListTeams(&internal.ListTeamOpts{PageLen: 100, Role: "member"})
if err != nil {
return repos, err
}
for _, team := range resp.Values {
logins = append(logins, team.Login)
}
// for each account, get the list of repos
for _, login := range logins {
repos_, err := client.ListReposAll(login)
if err != nil {
return repos, err
}
for _, repo := range repos_ {
repos = append(repos, convertRepoLite(repo))
}
}
return repos, nil
}
func (c *config) Perm(u *model.User, owner, name string) (*model.Perm, error) {
client := c.newClient(u)
perms := new(model.Perm)
_, err := client.FindRepo(owner, name)
if err != nil {
return perms, err
}
// if we've gotten this far we know that the user at least has read access
// to the repository.
perms.Pull = true
// if the user has access to the repository hooks we can deduce that the user
// has push and admin access.
_, err = client.ListHooks(owner, name, &internal.ListOpts{})
if err == nil {
perms.Push = true
perms.Admin = true
}
return perms, nil
}
func (c *config) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) {
config, err := c.newClient(u).FindSource(r.Owner, r.Name, b.Commit, f)
if err != nil {
return nil, err
}
return []byte(config.Data), err
}
func (c *config) Status(u *model.User, r *model.Repo, b *model.Build, link string) error {
status := internal.BuildStatus{
State: getStatus(b.Status),
Desc: getDesc(b.Status),
Key: "Drone",
Url: link,
}
return c.newClient(u).CreateStatus(r.Owner, r.Name, b.Commit, &status)
}
func (c *config) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
return &model.Netrc{
Machine: "bitbucket.org",
Login: "x-token-auth",
Password: u.Token,
}, nil
}
func (c *config) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error {
rawurl, err := url.Parse(link)
if err != nil {
return err
}
// deletes any previously created hooks
if err := c.Deactivate(u, r, link); err != nil {
// we can live with failure here. Things happen and manually scrubbing
// hooks is certinaly not the end of the world.
}
return c.newClient(u).CreateHook(r.Owner, r.Name, &internal.Hook{
Active: true,
Desc: rawurl.Host,
Events: []string{"repo:push"},
Url: link,
})
}
func (c *config) Deactivate(u *model.User, r *model.Repo, link string) error {
client := c.newClient(u)
linkurl, err := url.Parse(link)
if err != nil {
return err
}
hooks, err := client.ListHooks(r.Owner, r.Name, &internal.ListOpts{})
if err != nil {
return nil // we can live with undeleted hooks
}
for _, hook := range hooks.Values {
hookurl, err := url.Parse(hook.Url)
if err != nil {
continue
}
if hookurl.Host == linkurl.Host {
client.DeleteHook(r.Owner, r.Name, hook.Uuid)
break // we can live with undeleted hooks
}
}
return nil
}
func (c *config) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
switch r.Header.Get("X-Event-Key") {
case "repo:push":
return c.pushHook(r)
case "pullrequest:created", "pullrequest:updated":
return c.pullHook(r)
}
return nil, nil, nil
}
func (c *config) pushHook(r *http.Request) (*model.Repo, *model.Build, error) {
payload := []byte(r.FormValue("payload"))
if len(payload) == 0 {
defer r.Body.Close()
payload, _ = ioutil.ReadAll(r.Body)
}
hook := internal.PushHook{}
err := json.Unmarshal(payload, &hook)
if err != nil {
return nil, nil, err
}
// the hook can container one or many changes. Since I don't
// fully understand this yet, we will just pick the first
// change that has branch information.
for _, change := range hook.Push.Changes {
// must have sha information
if change.New.Target.Hash == "" {
continue
}
// we only support tag and branch pushes for now
buildEventType := model.EventPush
buildRef := fmt.Sprintf("refs/heads/%s", change.New.Name)
if change.New.Type == "tag" || change.New.Type == "annotated_tag" || change.New.Type == "bookmark" {
buildEventType = model.EventTag
buildRef = fmt.Sprintf("refs/tags/%s", change.New.Name)
} else if change.New.Type != "branch" && change.New.Type != "named_branch" {
continue
}
// return the updated repository information and the
// build information.
// TODO(bradrydzewski) uses unit tested conversion function
return convertRepo(&hook.Repo), &model.Build{
Event: buildEventType,
Commit: change.New.Target.Hash,
Ref: buildRef,
Link: change.New.Target.Links.Html.Href,
Branch: change.New.Name,
Message: change.New.Target.Message,
Avatar: hook.Actor.Links.Avatar.Href,
Author: hook.Actor.Login,
Timestamp: change.New.Target.Date.UTC().Unix(),
}, nil
}
return nil, nil, nil
}
func (c *config) pullHook(r *http.Request) (*model.Repo, *model.Build, error) {
payload := []byte(r.FormValue("payload"))
if len(payload) == 0 {
defer r.Body.Close()
payload, _ = ioutil.ReadAll(r.Body)
}
hook := internal.PullRequestHook{}
err := json.Unmarshal(payload, &hook)
if err != nil {
return nil, nil, err
}
if hook.PullRequest.State != "OPEN" {
return nil, nil, nil
}
// TODO(bradrydzewski) uses unit tested conversion function
return convertRepo(&hook.Repo), &model.Build{
Event: model.EventPull,
Commit: hook.PullRequest.Dest.Commit.Hash,
Ref: fmt.Sprintf("refs/heads/%s", hook.PullRequest.Dest.Branch.Name),
Refspec: fmt.Sprintf("https://bitbucket.org/%s.git", hook.PullRequest.Source.Repo.FullName),
Remote: cloneLink(&hook.PullRequest.Dest.Repo),
Link: hook.PullRequest.Links.Html.Href,
Branch: hook.PullRequest.Dest.Branch.Name,
Message: hook.PullRequest.Desc,
Avatar: hook.Actor.Links.Avatar.Href,
Author: hook.Actor.Login,
Timestamp: hook.PullRequest.Updated.UTC().Unix(),
}, nil
}