diff --git a/remote/bitbucketserver/bitbucketserver.go b/remote/bitbucketserver/bitbucketserver.go index 1aa1f24a8..8b70edf82 100644 --- a/remote/bitbucketserver/bitbucketserver.go +++ b/remote/bitbucketserver/bitbucketserver.go @@ -5,21 +5,26 @@ package bitbucketserver import ( "crypto/rsa" + "crypto/tls" "crypto/x509" - "encoding/json" "encoding/pem" "fmt" + "github.com/drone/drone/model" + "github.com/drone/drone/remote" + "github.com/drone/drone/remote/bitbucketserver/internal" + "github.com/mrjones/oauth" "io/ioutil" "net/http" "net/url" - - log "github.com/Sirupsen/logrus" - "github.com/drone/drone/model" - "github.com/drone/drone/remote" - "github.com/mrjones/oauth" "strings" ) +const ( + requestTokenURL = "%s/plugins/servlet/oauth/request-token" + authorizeTokenURL = "%s/plugins/servlet/oauth/authorize" + accessTokenURL = "%s/plugins/servlet/oauth/access-token" +) + // Opts defines configuration options. type Opts struct { URL string // Stash server url. @@ -27,218 +32,128 @@ type Opts struct { Password string // Git machine account password. ConsumerKey string // Oauth1 consumer key. ConsumerRSA string // Oauth1 consumer key file. + SkipVerify bool // Skip ssl verification. +} - SkipVerify bool // Skip ssl verification. +type Config struct { + URL string + Username string + Password string + SkipVerify bool + Consumer *oauth.Consumer } // New returns a Remote implementation that integrates with Bitbucket Server, // the on-premise edition of Bitbucket Cloud, formerly known as Stash. func New(opts Opts) (remote.Remote, error) { - bb := &client{ - URL: opts.URL, - ConsumerKey: opts.ConsumerKey, - ConsumerRSA: opts.ConsumerRSA, - GitUserName: opts.Username, - GitPassword: opts.Password, + config := &Config{ + URL: opts.URL, + Username: opts.Username, + Password: opts.Password, + SkipVerify: opts.SkipVerify, } switch { - case bb.GitUserName == "": + case opts.Username == "": return nil, fmt.Errorf("Must have a git machine account username") - case bb.GitPassword == "": + case opts.Password == "": return nil, fmt.Errorf("Must have a git machine account password") - case bb.ConsumerKey == "": + case opts.ConsumerKey == "": return nil, fmt.Errorf("Must have a oauth1 consumer key") - case bb.ConsumerRSA == "": + case opts.ConsumerRSA == "": return nil, fmt.Errorf("Must have a oauth1 consumer key file") } - keyfile, err := ioutil.ReadFile(bb.ConsumerRSA) + keyFile, err := ioutil.ReadFile(opts.ConsumerRSA) if err != nil { return nil, err } - block, _ := pem.Decode(keyfile) - bb.PrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + block, _ := pem.Decode(keyFile) + PrivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, err } - - // TODO de-referencing is a bit weird and may not behave as expected, and could - // have race conditions. Instead store the parsed key (I already did this above) - // and then pass the parsed private key when creating the Bitbucket client. - bb.Consumer = *NewClient(bb.ConsumerRSA, bb.ConsumerKey, bb.URL) - return bb, nil + config.Consumer = CreateConsumer(opts.URL, opts.ConsumerKey, PrivateKey) + return config, nil } -type client struct { - URL string - ConsumerKey string - GitUserName string - GitPassword string - ConsumerRSA string - PrivateKey *rsa.PrivateKey - Consumer oauth.Consumer -} - -func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { +func (c *Config) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { requestToken, url, err := c.Consumer.GetRequestTokenAndUrl("oob") if err != nil { return nil, err } - var code = req.FormValue("oauth_verifier") if len(code) == 0 { http.Redirect(res, req, url, http.StatusSeeOther) return nil, nil } - requestToken.Token = req.FormValue("oauth_token") accessToken, err := c.Consumer.AuthorizeToken(requestToken, code) if err != nil { return nil, err } - client, err := c.Consumer.MakeHttpClient(accessToken) + client := internal.NewClientWithToken(c.URL, c.Consumer, accessToken.Token) + + user, err := client.FindCurrentUser() if err != nil { return nil, err } - response, err := client.Get(fmt.Sprintf("%s/plugins/servlet/applinks/whoami", c.URL)) - if err != nil { - return nil, err - } - defer response.Body.Close() - bits, err := ioutil.ReadAll(response.Body) - if err != nil { - return nil, err - } - login := string(bits) + return convertUser(user, accessToken), nil - // TODO errors should never be ignored like this - response1, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/users/%s", c.URL, login)) - if err != nil { - return nil, err - } - defer response1.Body.Close() - - contents, err := ioutil.ReadAll(response1.Body) - if err != nil { - return nil, err - } - - var user User - err = json.Unmarshal(contents, &user) - if err != nil { - return nil, err - } - return &model.User{ - Login: login, - Email: user.EmailAddress, - Token: accessToken.Token, - Avatar: avatarLink(user.EmailAddress), - }, nil } // Auth is not supported by the Stash driver. -func (*client) Auth(token, secret string) (string, error) { +func (*Config) Auth(token, secret string) (string, error) { return "", fmt.Errorf("Not Implemented") } // Teams is not supported by the Stash driver. -func (*client) Teams(u *model.User) ([]*model.Team, error) { +func (*Config) Teams(u *model.User) ([]*model.Team, error) { var teams []*model.Team return teams, nil } -func (c *client) Repo(u *model.User, owner, name string) (*model.Repo, error) { - client := NewClientWithToken(&c.Consumer, u.Token) - repo, err := c.FindRepo(client, owner, name) +func (c *Config) Repo(u *model.User, owner, name string) (*model.Repo, error) { + repo, err := internal.NewClientWithToken(c.URL, c.Consumer, u.Token).FindRepo(owner, name) if err != nil { return nil, err } - return repo, nil + return convertRepo(repo), nil } -func (c *client) Repos(u *model.User) ([]*model.RepoLite, error) { - - var repos = []*model.RepoLite{} - - client := NewClientWithToken(&c.Consumer, u.Token) - - response, err := client.Get(fmt.Sprintf("%s/rest/api/1.0/repos?limit=10000", c.URL)) - if err != nil { - log.Error(err) - } - defer response.Body.Close() - contents, err := ioutil.ReadAll(response.Body) +func (c *Config) Repos(u *model.User) ([]*model.RepoLite, error) { + repos, err := internal.NewClientWithToken(c.URL, c.Consumer, u.Token).FindRepos() if err != nil { return nil, err } - var repoResponse Repos - err = json.Unmarshal(contents, &repoResponse) - if err != nil { - return nil, err + var all []*model.RepoLite + for _, repo := range repos { + all = append(all, convertRepoLite(repo)) } - 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 + return all, nil } -func (c *client) Perm(u *model.User, owner, repo string) (*model.Perm, error) { - client := NewClientWithToken(&c.Consumer, u.Token) - perms := new(model.Perm) +func (c *Config) Perm(u *model.User, owner, repo string) (*model.Perm, error) { + client := internal.NewClientWithToken(c.URL, c.Consumer, u.Token) - // If you don't have access return none right away - _, err := c.FindRepo(client, owner, repo) - if err != nil { - return perms, err - } - - // Must have admin to be able to list hooks. If have access the enable perms - _, err = client.Get(fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s", c.URL, owner, repo, "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook")) - if err == nil { - perms.Push = true - perms.Admin = true - } - perms.Pull = true - return perms, nil + return client.FindRepoPerms(owner, repo) } -func (c *client) 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)) +func (c *Config) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { + client := internal.NewClientWithToken(c.URL, c.Consumer, u.Token) - client := NewClientWithToken(&c.Consumer, u.Token) - fileURL := fmt.Sprintf("%s/projects/%s/repos/%s/browse/%s?raw", c.URL, r.Owner, r.Name, f) - log.Info(fileURL) - response, err := client.Get(fileURL) - if err != nil { - log.Error(err) - } - if response.StatusCode == 404 { - return nil, nil - } - defer response.Body.Close() - responseBytes, err := ioutil.ReadAll(response.Body) - if err != nil { - log.Error(err) - } - - return responseBytes, nil + return client.FindFileForRepo(r.Owner, r.Name, f, b.Ref) } -// Status is not supported by the Gogs driver. -func (*client) Status(*model.User, *model.Repo, *model.Build, string) error { +// Status is not supported by the bitbucketserver driver. +func (*Config) Status(*model.User, *model.Repo, *model.Build, string) error { return nil } -func (c *client) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) { +func (c *Config) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) { u, err := url.Parse(c.URL) if err != nil { return nil, err @@ -252,132 +167,40 @@ func (c *client) Netrc(user *model.User, r *model.Repo) (*model.Netrc, error) { } return &model.Netrc{ Machine: host, - Login: c.GitUserName, - Password: c.GitPassword, + Login: c.Username, + Password: c.Password, }, nil } -func (c *client) Activate(u *model.User, r *model.Repo, link string) error { - client := NewClientWithToken(&c.Consumer, u.Token) - hook, err := c.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 (c *Config) Activate(u *model.User, r *model.Repo, link string) error { + client := internal.NewClientWithToken(c.URL, c.Consumer, u.Token) + + return client.CreateHook(r.Owner, r.Name, link) } -func (c *client) Deactivate(u *model.User, r *model.Repo, link string) error { - client := NewClientWithToken(&c.Consumer, u.Token) - err := c.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 (c *Config) Deactivate(u *model.User, r *model.Repo, link string) error { + client := internal.NewClientWithToken(c.URL, c.Consumer, u.Token) + return client.DeleteHook(r.Owner, r.Name, link) } -func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) { - hook := new(postHook) - if err := json.NewDecoder(r.Body).Decode(hook); err != nil { - return nil, nil, err - } - - build := &model.Build{ - Event: model.EventPush, - Ref: hook.RefChanges[0].RefID, // TODO check for index Values - Author: hook.Changesets.Values[0].ToCommit.Author.EmailAddress, // TODO check for index Values - Commit: hook.RefChanges[0].ToHash, // TODO check for index value - Avatar: avatarLink(hook.Changesets.Values[0].ToCommit.Author.EmailAddress), - } - - repo := &model.Repo{ - Name: hook.Repository.Slug, - Owner: hook.Repository.Project.Key, - FullName: fmt.Sprintf("%s/%s", hook.Repository.Project.Key, hook.Repository.Slug), - Branch: "master", - Kind: model.RepoGit, - } - - return repo, build, nil +func (c *Config) Hook(r *http.Request) (*model.Repo, *model.Build, error) { + return parseHook(r, c.URL) } -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 *client) 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 *client) 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 -} - -func (c *client) FindRepo(client *http.Client, owner string, name string) (*model.Repo, error) { - - urlString := fmt.Sprintf("%s/rest/api/1.0/projects/%s/repos/%s", c.URL, owner, name) - - response, err := client.Get(urlString) - if err != nil { - log.Error(err) +func CreateConsumer(URL string, ConsumerKey string, PrivateKey *rsa.PrivateKey) *oauth.Consumer { + consumer := oauth.NewRSAConsumer( + ConsumerKey, + PrivateKey, + oauth.ServiceProvider{ + RequestTokenUrl: fmt.Sprintf(requestTokenURL, URL), + AuthorizeTokenUrl: fmt.Sprintf(authorizeTokenURL, URL), + AccessTokenUrl: fmt.Sprintf(accessTokenURL, URL), + HttpMethod: "POST", + }) + consumer.HttpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, } - defer response.Body.Close() - contents, err := ioutil.ReadAll(response.Body) - bsRepo := BSRepo{} - err = json.Unmarshal(contents, &bsRepo) - if err != nil { - return nil, err - } - repo := &model.Repo{ - Name: bsRepo.Slug, - Owner: bsRepo.Project.Key, - Branch: "master", - Kind: model.RepoGit, - IsPrivate: true, // TODO(josmo) possibly set this as a setting - must always be private to use netrc - FullName: fmt.Sprintf("%s/%s", bsRepo.Project.Key, bsRepo.Slug), - } - - for _, item := range bsRepo.Links.Clone { - if item.Name == "http" { - uri, err := url.Parse(item.Href) - if err != nil { - return nil, err - } - uri.User = nil - repo.Clone = uri.String() - } - } - for _, item := range bsRepo.Links.Self { - if item.Href != "" { - repo.Link = item.Href - } - } - - return repo, nil + return consumer } diff --git a/remote/bitbucketserver/client.go b/remote/bitbucketserver/client.go deleted file mode 100644 index 6c6f280b6..000000000 --- a/remote/bitbucketserver/client.go +++ /dev/null @@ -1,52 +0,0 @@ -package bitbucketserver - -import ( - "crypto/tls" - "crypto/x509" - "encoding/pem" - log "github.com/Sirupsen/logrus" - "github.com/mrjones/oauth" - "io/ioutil" - "net/http" -) - -func NewClient(ConsumerRSA string, ConsumerKey string, URL string) *oauth.Consumer { - //TODO: make this configurable - privateKeyFileContents, err := ioutil.ReadFile(ConsumerRSA) - log.Info("Tried to read the key") - if err != nil { - log.Error(err) - } - - block, _ := pem.Decode([]byte(privateKeyFileContents)) - privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - log.Error(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(Consumer *oauth.Consumer, AccessToken string) *http.Client { - var token oauth.AccessToken - token.Token = AccessToken - client, err := Consumer.MakeHttpClient(&token) - if err != nil { - log.Error(err) - } - return client -} diff --git a/remote/bitbucketserver/convert.go b/remote/bitbucketserver/convert.go new file mode 100644 index 000000000..24841b98a --- /dev/null +++ b/remote/bitbucketserver/convert.go @@ -0,0 +1,110 @@ +package bitbucketserver + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/drone/drone/model" + "github.com/drone/drone/remote/bitbucketserver/internal" + "github.com/mrjones/oauth" + "net/url" + "strings" + "time" +) + +// convertRepo is a helper function used to convert a Bitbucket server repository +// structure to the common Drone repository structure. +func convertRepo(from *internal.Repo) *model.Repo { + + repo := model.Repo{ + Name: from.Slug, + Owner: from.Project.Key, + Branch: "master", + Kind: model.RepoGit, + IsPrivate: true, // Since we have to use Netrc it has to always be private :/ + FullName: fmt.Sprintf("%s/%s", from.Project.Key, from.Slug), + } + + for _, item := range from.Links.Clone { + if item.Name == "http" { + uri, err := url.Parse(item.Href) + if err != nil { + return nil + } + uri.User = nil + repo.Clone = uri.String() + } + } + for _, item := range from.Links.Self { + if item.Href != "" { + repo.Link = item.Href + } + } + log.Debug(fmt.Printf("Repo: %+v\n", repo)) + return &repo + +} + +// convertRepoLite is a helper function used to convert a Bitbucket repository +// structure to the simplified Drone repository structure. +func convertRepoLite(from *internal.Repo) *model.RepoLite { + return &model.RepoLite{ + Owner: from.Project.Key, + Name: from.Slug, + FullName: from.Project.Key + "/" + from.Slug, + //TODO: find the avatar for the repo + //Avatar: might need another ws call? + } + +} + +// convertPushHook is a helper function used to convert a Bitbucket push +// hook to the Drone build struct holding commit information. +func convertPushHook(hook *internal.PostHook, baseURL string) *model.Build { + //get the ref parts to see if it's a tags or heads + refParts := strings.Split(hook.RefChanges[0].RefID, "/") + name := refParts[2] + commitType := refParts[1] + + build := &model.Build{ + Commit: hook.RefChanges[0].ToHash, // TODO check for index value + //Link: TODO find link + Branch: name, + Message: hook.Changesets.Values[0].ToCommit.Message, //TODO check for index Values + Avatar: avatarLink(hook.Changesets.Values[0].ToCommit.Author.EmailAddress), + Author: fmt.Sprintf("%s <%s>", hook.Changesets.Values[0].ToCommit.Author.Name, hook.Changesets.Values[0].ToCommit.Author.EmailAddress), + Email: hook.Changesets.Values[0].ToCommit.Author.EmailAddress, + Timestamp: time.Now().UTC().Unix(), + Ref: hook.RefChanges[0].RefID, // TODO check for index Values + Link: fmt.Sprintf("%s/projects/%s/repos/%s/commits/%s", baseURL, hook.Repository.Project.Key, hook.Repository.Slug, hook.RefChanges[0].ToHash), + } + switch commitType { + case "tags": + build.Event = model.EventTag + default: + build.Event = model.EventPush + } + + return build +} + +// convertUser is a helper function used to convert a Bitbucket user account +// structure to the Drone User structure. +func convertUser(from *internal.User, token *oauth.AccessToken) *model.User { + return &model.User{ + Login: from.Slug, + Token: token.Token, + Email: from.EmailAddress, + Avatar: avatarLink(from.EmailAddress), + } +} + +func avatarLink(email string) string { + hasher := md5.New() + hasher.Write([]byte(strings.ToLower(email))) + emailHash := fmt.Sprintf("%v", hex.EncodeToString(hasher.Sum(nil))) + avatarURL := fmt.Sprintf("https://www.gravatar.com/avatar/%s.jpg", emailHash) + log.Debug(avatarURL) + return avatarURL +} diff --git a/remote/bitbucketserver/helper.go b/remote/bitbucketserver/helper.go deleted file mode 100644 index 9c3985113..000000000 --- a/remote/bitbucketserver/helper.go +++ /dev/null @@ -1,64 +0,0 @@ -package bitbucketserver - -import ( - "bytes" - "crypto/md5" - "encoding/hex" - "fmt" - log "github.com/Sirupsen/logrus" - "io/ioutil" - "net/http" - "strings" -) - -func avatarLink(email string) (url string) { - hasher := md5.New() - hasher.Write([]byte(strings.ToLower(email))) - emailHash := fmt.Sprintf("%v", hex.EncodeToString(hasher.Sum(nil))) - avatarURL := fmt.Sprintf("https://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.Error(err) - } else { - defer response.Body.Close() - contents, err := ioutil.ReadAll(response.Body) - if err != nil { - log.Error(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.Error(err) - } else { - defer response.Body.Close() - contents, err := ioutil.ReadAll(response.Body) - if err != nil { - log.Error(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/internal/client.go b/remote/bitbucketserver/internal/client.go new file mode 100644 index 000000000..058538481 --- /dev/null +++ b/remote/bitbucketserver/internal/client.go @@ -0,0 +1,181 @@ +package internal + +import ( + "bytes" + "encoding/json" + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/drone/drone/model" + "github.com/mrjones/oauth" + "io/ioutil" + "net/http" +) + +const ( + currentUserId = "%s/plugins/servlet/applinks/whoami" + pathUser = "%s/rest/api/1.0/users/%s" + pathRepo = "%s/rest/api/1.0/projects/%s/repos/%s" + pathRepos = "%s/rest/api/1.0/repos?limit=%s" + pathHook = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s" + pathSource = "%s/projects/%s/repos/%s/browse/%s?at=%s&raw" + hookName = "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook" + pathHookEnabled = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/enabled" +) + +type Client struct { + client *http.Client + base string + accessToken string +} + +func NewClientWithToken(url string, consumer *oauth.Consumer, AccessToken string) *Client { + var token oauth.AccessToken + token.Token = AccessToken + client, err := consumer.MakeHttpClient(&token) + log.Debug(fmt.Printf("Create client: %+v %s\n", token, url)) + if err != nil { + log.Error(err) + } + return &Client{client, url, AccessToken} +} + +func (c *Client) FindCurrentUser() (*User, error) { + CurrentUserIdResponse, err := c.client.Get(fmt.Sprintf(currentUserId, c.base)) + if err != nil { + return nil, err + } + defer CurrentUserIdResponse.Body.Close() + bits, err := ioutil.ReadAll(CurrentUserIdResponse.Body) + if err != nil { + return nil, err + } + login := string(bits) + + CurrentUserResponse, err := c.client.Get(fmt.Sprintf(pathUser, c.base, login)) + if err != nil { + return nil, err + } + defer CurrentUserResponse.Body.Close() + + contents, err := ioutil.ReadAll(CurrentUserResponse.Body) + if err != nil { + return nil, err + } + + var user User + err = json.Unmarshal(contents, &user) + if err != nil { + return nil, err + } + + return &user, nil + +} + +func (c *Client) FindRepo(owner string, name string) (*Repo, error) { + urlString := fmt.Sprintf(pathRepo, c.base, owner, name) + response, err := c.client.Get(urlString) + if err != nil { + log.Error(err) + } + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + repo := Repo{} + err = json.Unmarshal(contents, &repo) + if err != nil { + return nil, err + } + return &repo, nil +} + +func (c *Client) FindRepos() ([]*Repo, error) { + requestUrl := fmt.Sprintf(pathRepos, c.base, "1000") + log.Debug(fmt.Printf("request :%s", requestUrl)) + response, err := c.client.Get(requestUrl) + if err != nil { + return nil, err + } + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + var repoResponse Repos + err = json.Unmarshal(contents, &repoResponse) + if err != nil { + return nil, err + } + log.Debug(fmt.Printf("repoResponse: %+v\n", repoResponse)) + + return repoResponse.Values, nil +} + +func (c *Client) FindRepoPerms(owner string, repo string) (*model.Perm, error) { + perms := new(model.Perm) + // If you don't have access return none right away + _, err := c.FindRepo(owner, repo) + if err != nil { + return perms, err + } + // Must have admin to be able to list hooks. If have access the enable perms + _, err = c.client.Get(fmt.Sprintf(pathHook, c.base, owner, repo, hookName)) + if err == nil { + perms.Push = true + perms.Admin = true + } + perms.Pull = true + log.Debug(fmt.Printf("Perms: %+v\n", perms)) + return perms, nil +} + +func (c *Client) FindFileForRepo(owner string, repo string, fileName string, ref string) ([]byte, error) { + response, err := c.client.Get(fmt.Sprintf(pathSource, c.base, owner, repo, fileName, ref)) + if err != nil { + log.Error(err) + } + if response.StatusCode == 404 { + return nil, nil + } + defer response.Body.Close() + responseBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Error(err) + } + return responseBytes, nil +} + +func (c *Client) CreateHook(owner string, name string, callBackLink string) error { + // Set hook + //TODO: Check existing and add up to 5 + hookBytes := []byte(fmt.Sprintf(`{"hook-url-0":"%s"}`, callBackLink)) + return c.doPut(fmt.Sprintf(pathHookEnabled, c.base, owner, name, hookName), hookBytes) +} + +func (c *Client) DeleteHook(owner string, name string, link string) error { + //TODO: eventially should only delete the link callback + return c.doDelete(fmt.Sprintf(pathHookEnabled, c.base, owner, name, hookName)) +} + +//Helper function to help create the hook +func (c *Client) doPut(url string, body []byte) error { + request, err := http.NewRequest("PUT", url, bytes.NewBuffer(body)) + request.Header.Add("Content-Type", "application/json") + response, err := c.client.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + return nil + +} + +//Helper function to do delete on the hook +func (c *Client) doDelete(url string) error { + request, err := http.NewRequest("DELETE", url, nil) + response, err := c.client.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + return nil +} diff --git a/remote/bitbucketserver/types.go b/remote/bitbucketserver/internal/types.go similarity index 78% rename from remote/bitbucketserver/types.go rename to remote/bitbucketserver/internal/types.go index 3a1fb9083..8e4622ce2 100644 --- a/remote/bitbucketserver/types.go +++ b/remote/bitbucketserver/internal/types.go @@ -1,6 +1,76 @@ -package bitbucketserver +package internal -type postHook struct { +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 Repo 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"` +} + +type Repos struct { + IsLastPage bool `json:"isLastPage"` + Limit int `json:"limit"` + Size int `json:"size"` + Start int `json:"start"` + Values []*Repo `json:"values"` +} + +type Hook struct { + Enabled bool `json:"enabled"` + Details *HookDetail `json:"details"` +} + +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 PostHook struct { Changesets struct { Filter interface{} `json:"filter"` IsLastPage bool `json:"isLastPage"` @@ -83,90 +153,3 @@ type postHook struct { 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/bitbucketserver/parse.go b/remote/bitbucketserver/parse.go new file mode 100644 index 000000000..be5352538 --- /dev/null +++ b/remote/bitbucketserver/parse.go @@ -0,0 +1,28 @@ +package bitbucketserver + +import ( + "encoding/json" + "fmt" + "github.com/drone/drone/model" + "github.com/drone/drone/remote/bitbucketserver/internal" + "net/http" +) + +// parseHook parses a Bitbucket hook from an http.Request request and returns +// Repo and Build detail. TODO: find a way to support PR hooks +func parseHook(r *http.Request, baseURL string) (*model.Repo, *model.Build, error) { + hook := new(internal.PostHook) + if err := json.NewDecoder(r.Body).Decode(hook); err != nil { + return nil, nil, err + } + build := convertPushHook(hook, baseURL) + repo := &model.Repo{ + Name: hook.Repository.Slug, + Owner: hook.Repository.Project.Key, + FullName: fmt.Sprintf("%s/%s", hook.Repository.Project.Key, hook.Repository.Slug), + Branch: "master", + Kind: model.RepoGit, + } + + return repo, build, nil +}