From 59edcd338948cff72e245cc3bfafad26cf063f7a Mon Sep 17 00:00:00 2001 From: Joachim Hill-Grannec Date: Tue, 19 Apr 2016 00:40:49 -0400 Subject: [PATCH] Initial take at the bitbucket server remote additions. --- Makefile | 1 + remote/bitbucketserver/bitbucketserver.go | 343 ++++++++++++++++++++++ remote/bitbucketserver/client.go | 61 ++++ remote/bitbucketserver/helper.go | 63 ++++ remote/bitbucketserver/types.go | 173 +++++++++++ remote/remote.go | 3 + 6 files changed, 644 insertions(+) create mode 100644 remote/bitbucketserver/bitbucketserver.go create mode 100644 remote/bitbucketserver/client.go create mode 100644 remote/bitbucketserver/helper.go create mode 100644 remote/bitbucketserver/types.go diff --git a/Makefile b/Makefile index 632c8bfe9..f8478d8dc 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ deps: go get -u github.com/elazarl/go-bindata-assetfs/... go get -u github.com/dchest/jsmin go get -u github.com/franela/goblin + go get -u github.com/mrjones/oauth gen: gen_static gen_template gen_migrations diff --git a/remote/bitbucketserver/bitbucketserver.go b/remote/bitbucketserver/bitbucketserver.go new file mode 100644 index 000000000..d119a6ef8 --- /dev/null +++ b/remote/bitbucketserver/bitbucketserver.go @@ -0,0 +1,343 @@ +package bitbucketserver + +import ( + "github.com/drone/drone/shared/envconfig" + "net/url" + log "github.com/Sirupsen/logrus" + "net/http" + "github.com/drone/drone/model" + "fmt" + "io/ioutil" + "strconv" + "encoding/json" + +) + +type BitbucketServer struct { + URL string + ConsumerKey string + GitUserName string + GitPassword string + Open bool +} + +func Load(env envconfig.Env) *BitbucketServer{ + + //Read + config := env.String("REMOTE_CONFIG", "") + gitUserName := env.String("GIT_USERNAME", "") + gitUserPassword := env.String("GIT_USERPASSWORD","") + + url_, err := url.Parse(config) + if err != nil { + log.Fatalln("unable to parse remote dsn. %s", err) + } + params := url_.Query() + url_.Path = "" + url_.RawQuery = "" + + bitbucketserver := BitbucketServer{} + bitbucketserver.URL = url_.String() + bitbucketserver.GitUserName = gitUserName + bitbucketserver.GitPassword = gitUserPassword + bitbucketserver.ConsumerKey = params.Get("consumer_key") + bitbucketserver.Open, _ = strconv.ParseBool(params.Get("open")) + + return &bitbucketserver +} + +func (bs *BitbucketServer) Login(res http.ResponseWriter, req *http.Request) (*model.User, bool, error){ + log.Info("Starting to login for bitbucketServer") + + c := NewClient(bs.ConsumerKey, bs.URL) + + log.Info("getting the requestToken") + requestToken, url, err := c.GetRequestTokenAndUrl("oob") + if err != nil { + log.Fatal(err) + } + + var code = req.FormValue("oauth_verifier") + if len(code) == 0 { + log.Info("redirecting to %s", url) + http.Redirect(res, req, url, http.StatusSeeOther) + return nil, false, nil + } + + var request_oauth_token = req.FormValue("oauth_token") + requestToken.Token = request_oauth_token + accessToken, err := c.AuthorizeToken(requestToken, code) + if err !=nil { + log.Error(err) + } + + client, err := c.MakeHttpClient(accessToken) + if err != nil { + log.Fatal(err) + } + + response, err := client.Get(bs.URL + "/plugins/servlet/applinks/whoami") + if err != nil { + log.Fatal(err) + } + defer response.Body.Close() + bits, err := ioutil.ReadAll(response.Body) + userName := string(bits) + + response1, err := client.Get(bs.URL + "/rest/api/1.0/users/" +userName) + contents, err := ioutil.ReadAll(response1.Body) + defer response1.Body.Close() + var mUser User + json.Unmarshal(contents, &mUser) + + user := model.User{} + user.Login = userName + user.Email = mUser.EmailAddress + user.Token = accessToken.Token + + user.Avatar = avatarLink(mUser.EmailAddress) + + + return &user, bs.Open, nil +} + +func (bs *BitbucketServer) Auth(token, secret string) (string, error) { + log.Info("Staring to auth for bitbucketServer. %s", token) + if len(token) == 0 { + return "", fmt.Errorf("Hasn't logged in yet") + } + return token, nil; +} + +func (bs *BitbucketServer) Repo(u *model.User, owner, name string) (*model.Repo, error){ + log.Info("Staring repo for bitbucketServer with user " + u.Login + " " + owner + " " + name ) + + client := NewClientWithToken(bs.ConsumerKey, bs.URL, u.Token) + + url := bs.URL + "/rest/api/1.0/projects/" + owner + "/repos/" + name + log.Info("Trying to get " + url) + response, err := client.Get(url) + if err != nil { + log.Fatal(err) + } + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + bsRepo := BSRepo{} + json.Unmarshal(contents, &bsRepo) + + cloneLink := "" + repoLink := "" + + for _, item := range bsRepo.Links.Clone { + if item.Name == "http" { + cloneLink = item.Href + } + } + for _, item := range bsRepo.Links.Self { + if item.Href != "" { + repoLink = item.Href + } + } + //TODO: get the real allow tag+ infomration + repo := &model.Repo{} + repo.Clone = cloneLink + repo.Link = repoLink + repo.Name=bsRepo.Slug + repo.Owner=bsRepo.Project.Key + repo.AllowTag=false + repo.AllowDeploy=false + repo.AllowPull=false + repo.AllowPush=true + repo.FullName = bsRepo.Project.Key +"/" +bsRepo.Slug + repo.Branch = "master" + repo.Kind = model.RepoGit + + + return repo, nil; +} + + +func (bs *BitbucketServer) Repos(u *model.User) ([]*model.RepoLite, error){ + log.Info("Staring repos for bitbucketServer " + u.Login) + var repos = []*model.RepoLite{} + + client := NewClientWithToken(bs.ConsumerKey, bs.URL, u.Token) + + response, err := client.Get(bs.URL + "/rest/api/1.0/repos?limit=10000") + if err != nil { + log.Fatal(err) + } + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + var repoResponse Repos + json.Unmarshal(contents, &repoResponse) + + for _, repo := range repoResponse.Values { + repos = append(repos, &model.RepoLite{ + Name: repo.Slug, + FullName: repo.Project.Key + "/" + repo.Slug, + Owner: repo.Project.Key, + }) + } + + + return repos, nil; +} + +func (bs *BitbucketServer) Perm(u *model.User, owner, repo string) (*model.Perm, error){ + + //TODO: find the real permissions + log.Info("Staring perm for bitbucketServer") + perms := new(model.Perm) + perms.Pull = true + perms.Admin = true + perms.Push = true + return perms , nil +} + +func (bs *BitbucketServer) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error){ + log.Info(fmt.Sprintf("Staring file for bitbucketServer login: %s repo: %s buildevent: %s string: %s",u.Login, r.Name, b.Event, f)) + + client := NewClientWithToken(bs.ConsumerKey, bs.URL, u.Token) + fileURL := fmt.Sprintf("%s/projects/%s/repos/%s/browse/%s?raw", bs.URL,r.Owner,r.Name,f) + log.Info(fileURL) + response, err := client.Get(fileURL) + if err != nil { + log.Fatal(err) + } + if response.StatusCode == 404 { + return nil,nil + } + defer response.Body.Close() + responseBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + + + return responseBytes, nil; +} + +func (bs *BitbucketServer) Status(u *model.User, r *model.Repo, b *model.Build, link string) error{ + log.Info("Staring status for bitbucketServer") + return nil; +} + +func (bs *BitbucketServer) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error){ + log.Info("Starting the Netrc lookup") + u, err := url.Parse(bs.URL) + if err != nil { + panic(err) + } + return &model.Netrc{ + Machine: u.Host, + Login: bs.GitUserName, + Password: bs.GitPassword, + }, nil +} + +func (bs *BitbucketServer) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error{ + log.Info(fmt.Sprintf("Staring activate for bitbucketServer user: %s repo: %s key: %s link: %s",u.Login,r.Name,k,link)) + client := NewClientWithToken(bs.ConsumerKey, bs.URL, u.Token) + hook, err := bs.CreateHook(client, r.Owner,r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook",link) + if err !=nil { + return err + } + log.Info(hook) + return nil; +} + +func (bs *BitbucketServer) Deactivate(u *model.User, r *model.Repo, link string) error{ + log.Info(fmt.Sprintf("Staring deactivating for bitbucketServer user: %s repo: %s link: %s",u.Login,r.Name,link)) + client := NewClientWithToken(bs.ConsumerKey, bs.URL, u.Token) + err := bs.DeleteHook(client, r.Owner,r.Name, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook",link) + if err !=nil { + return err + } + return nil; +} + +func (bs *BitbucketServer) Hook(r *http.Request) (*model.Repo, *model.Build, error){ + log.Info("Staring hook for bitbucketServer") + defer r.Body.Close() + contents, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Info(err) + } + + var hookPost postHook + json.Unmarshal(contents, &hookPost) + + + + buildModel := &model.Build{} + buildModel.Event = model.EventPush + buildModel.Ref = hookPost.RefChanges[0].RefID + buildModel.Author = hookPost.Changesets.Values[0].ToCommit.Author.EmailAddress + buildModel.Commit = hookPost.RefChanges[0].RefID + buildModel.Avatar = avatarLink(hookPost.Changesets.Values[0].ToCommit.Author.EmailAddress) + + //All you really need is the name and owner. That's what creates the lookup key, so it needs to match the repo info. Just an FYI + repo := &model.Repo{} + repo.Name=hookPost.Repository.Slug + repo.Owner = hookPost.Repository.Project.Key + repo.AllowTag=false + repo.AllowDeploy=false + repo.AllowPull=false + repo.AllowPush=true + repo.FullName = hookPost.Repository.Project.Key +"/" +hookPost.Repository.Slug + repo.Branch = "master" + repo.Kind = model.RepoGit + + return repo, buildModel, nil; +} +func (bs *BitbucketServer) String() string { + return "bitbucketserver" +} + + + +type HookDetail struct { + Key string `"json:key"` + Name string `"json:name"` + Type string `"json:type"` + Description string `"json:description"` + Version string `"json:version"` + ConfigFormKey string `"json:configFormKey"` +} + +type Hook struct { + Enabled bool `"json:enabled"` + Details *HookDetail `"json:details"` +} + + + +// Enable hook for named repository +func (bs *BitbucketServer)CreateHook(client *http.Client, project, slug, hook_key, link string) (*Hook, error) { + + // Set hook + hookBytes := []byte(fmt.Sprintf(`{"hook-url-0":"%s"}`,link)) + + // Enable hook + enablePath := fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/enabled", + project, slug, hook_key) + + doPut(client, bs.URL + enablePath, hookBytes) + + return nil, nil +} + +// Disable hook for named repository +func (bs *BitbucketServer)DeleteHook(client *http.Client, project, slug, hook_key, link string) error { + enablePath := fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/enabled", + project, slug, hook_key) + doDelete(client, bs.URL + enablePath) + + return nil +} + + + + + diff --git a/remote/bitbucketserver/client.go b/remote/bitbucketserver/client.go new file mode 100644 index 000000000..b65985971 --- /dev/null +++ b/remote/bitbucketserver/client.go @@ -0,0 +1,61 @@ +package bitbucketserver + +import ( + "net/http" + "crypto/tls" + log "github.com/Sirupsen/logrus" + "io/ioutil" + "encoding/pem" + "crypto/x509" + "github.com/mrjones/oauth" +) + + +func NewClient(ConsumerKey string, URL string) *oauth.Consumer{ + privateKeyFileContents, err := ioutil.ReadFile("/private_key.pem") + log.Info("Tried to read the key") + if err != nil { + log.Fatal(err) + } + + block, _ := pem.Decode([]byte(privateKeyFileContents)) + privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + log.Fatal(err) + } + + c := oauth.NewRSAConsumer( + ConsumerKey, + privateKey, + oauth.ServiceProvider{ + RequestTokenUrl: URL + "/plugins/servlet/oauth/request-token", + AuthorizeTokenUrl: URL + "/plugins/servlet/oauth/authorize", + AccessTokenUrl: URL + "/plugins/servlet/oauth/access-token", + HttpMethod: "POST", + }) + c.HttpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + return c +} + +func NewClientWithToken(ConsumerKey string, URL string, AccessToken string) *http.Client{ + NewClient(ConsumerKey, URL) + c := NewClient(ConsumerKey, URL) + + var token oauth.AccessToken + token.Token = AccessToken + client, err := c.MakeHttpClient(&token) + if err != nil { + log.Fatal(err) + } + return client +} + + + + + + diff --git a/remote/bitbucketserver/helper.go b/remote/bitbucketserver/helper.go new file mode 100644 index 000000000..f53a08324 --- /dev/null +++ b/remote/bitbucketserver/helper.go @@ -0,0 +1,63 @@ +package bitbucketserver + +import ( + "net/http" + "bytes" + "log" + "io/ioutil" + "fmt" + "strings" + "crypto/md5" + log "github.com/Sirupsen/logrus" +) + +func avatarLink(email string) (url string) { + data := []byte(strings.ToLower(email)) + emailHash := md5.Sum(data) + avatarURL := fmt.Sprintf("http://www.gravatar.com/avatar/%s.jpg",emailHash) + log.Info(avatarURL) + return avatarURL +} + +func doPut(client *http.Client, url string, body []byte) { + request, err := http.NewRequest("PUT", url, bytes.NewBuffer(body)) + request.Header.Add("Content-Type","application/json") + response, err := client.Do(request) + if err != nil { + log.Fatal(err) + } else { + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + fmt.Println("The calculated length is:", len(string(contents)), "for the url:", url) + fmt.Println(" ", response.StatusCode) + hdr := response.Header + for key, value := range hdr { + fmt.Println(" ", key, ":", value) + } + fmt.Println(string(contents)) + } +} + +func doDelete(client *http.Client, url string) { + request, err := http.NewRequest("DELETE", url, nil) + response, err := client.Do(request) + if err != nil { + log.Fatal(err) + } else { + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + fmt.Println("The calculated length is:", len(string(contents)), "for the url:", url) + fmt.Println(" ", response.StatusCode) + hdr := response.Header + for key, value := range hdr { + fmt.Println(" ", key, ":", value) + } + fmt.Println(string(contents)) + } +} diff --git a/remote/bitbucketserver/types.go b/remote/bitbucketserver/types.go new file mode 100644 index 000000000..4983bbe4d --- /dev/null +++ b/remote/bitbucketserver/types.go @@ -0,0 +1,173 @@ +package bitbucketserver + + +type postHook struct { + Changesets struct { + Filter interface{} `json:"filter"` + IsLastPage bool `json:"isLastPage"` + Limit int `json:"limit"` + Size int `json:"size"` + Start int `json:"start"` + Values []struct { + Changes struct { + Filter interface{} `json:"filter"` + IsLastPage bool `json:"isLastPage"` + Limit int `json:"limit"` + Size int `json:"size"` + Start int `json:"start"` + Values []struct { + ContentID string `json:"contentId"` + Executable bool `json:"executable"` + Link struct { + Rel string `json:"rel"` + URL string `json:"url"` + } `json:"link"` + NodeType string `json:"nodeType"` + Path struct { + Components []string `json:"components"` + Extension string `json:"extension"` + Name string `json:"name"` + Parent string `json:"parent"` + ToString string `json:"toString"` + } `json:"path"` + PercentUnchanged int `json:"percentUnchanged"` + SrcExecutable bool `json:"srcExecutable"` + Type string `json:"type"` + } `json:"values"` + } `json:"changes"` + FromCommit struct { + DisplayID string `json:"displayId"` + ID string `json:"id"` + } `json:"fromCommit"` + Link struct { + Rel string `json:"rel"` + URL string `json:"url"` + } `json:"link"` + ToCommit struct { + Author struct { + EmailAddress string `json:"emailAddress"` + Name string `json:"name"` + } `json:"author"` + AuthorTimestamp int `json:"authorTimestamp"` + DisplayID string `json:"displayId"` + ID string `json:"id"` + Message string `json:"message"` + Parents []struct { + DisplayID string `json:"displayId"` + ID string `json:"id"` + } `json:"parents"` + } `json:"toCommit"` + } `json:"values"` + } `json:"changesets"` + RefChanges []struct { + FromHash string `json:"fromHash"` + RefID string `json:"refId"` + ToHash string `json:"toHash"` + Type string `json:"type"` + } `json:"refChanges"` + Repository struct { + Forkable bool `json:"forkable"` + ID int `json:"id"` + Name string `json:"name"` + Project struct { + ID int `json:"id"` + IsPersonal bool `json:"isPersonal"` + Key string `json:"key"` + Name string `json:"name"` + Public bool `json:"public"` + Type string `json:"type"` + } `json:"project"` + Public bool `json:"public"` + ScmID string `json:"scmId"` + Slug string `json:"slug"` + State string `json:"state"` + StatusMessage string `json:"statusMessage"` + } `json:"repository"` +} + +type Repos struct { + IsLastPage bool `json:"isLastPage"` + Limit int `json:"limit"` + Size int `json:"size"` + Start int `json:"start"` + Values []struct { + Forkable bool `json:"forkable"` + ID int `json:"id"` + Links struct { + Clone []struct { + Href string `json:"href"` + Name string `json:"name"` + } `json:"clone"` + Self []struct { + Href string `json:"href"` + } `json:"self"` + } `json:"links"` + Name string `json:"name"` + Project struct { + Description string `json:"description"` + ID int `json:"id"` + Key string `json:"key"` + Links struct { + Self []struct { + Href string `json:"href"` + } `json:"self"` + } `json:"links"` + Name string `json:"name"` + Public bool `json:"public"` + Type string `json:"type"` + } `json:"project"` + Public bool `json:"public"` + ScmID string `json:"scmId"` + Slug string `json:"slug"` + State string `json:"state"` + StatusMessage string `json:"statusMessage"` + } `json:"values"` +} + +type User struct { + Active bool `json:"active"` + DisplayName string `json:"displayName"` + EmailAddress string `json:"emailAddress"` + ID int `json:"id"` + Links struct { + Self []struct { + Href string `json:"href"` + } `json:"self"` + } `json:"links"` + Name string `json:"name"` + Slug string `json:"slug"` + Type string `json:"type"` +} + +type BSRepo struct { + Forkable bool `json:"forkable"` + ID int `json:"id"` + Links struct { + Clone []struct { + Href string `json:"href"` + Name string `json:"name"` + } `json:"clone"` + Self []struct { + Href string `json:"href"` + } `json:"self"` + } `json:"links"` + Name string `json:"name"` + Project struct { + Description string `json:"description"` + ID int `json:"id"` + Key string `json:"key"` + Links struct { + Self []struct { + Href string `json:"href"` + } `json:"self"` + } `json:"links"` + Name string `json:"name"` + Public bool `json:"public"` + Type string `json:"type"` + } `json:"project"` + Public bool `json:"public"` + ScmID string `json:"scmId"` + Slug string `json:"slug"` + State string `json:"state"` + StatusMessage string `json:"statusMessage"` +} diff --git a/remote/remote.go b/remote/remote.go index 192154eab..b0a89832e 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -7,6 +7,7 @@ import ( "github.com/drone/drone/model" "github.com/drone/drone/remote/bitbucket" + "github.com/drone/drone/remote/bitbucketserver" "github.com/drone/drone/remote/github" "github.com/drone/drone/remote/gitlab" "github.com/drone/drone/remote/gogs" @@ -28,6 +29,8 @@ func Load(env envconfig.Env) Remote { return gitlab.Load(env) case "gogs": return gogs.Load(env) + case "bitbucketserver": + return bitbucketserver.Load(env) default: logrus.Fatalf("unknown remote driver %s", driver)