refactoring github package to increase test coverage

This commit is contained in:
Brad Rydzewski 2016-05-02 17:47:58 -07:00
parent 78cfd3a0db
commit f930545410
20 changed files with 872 additions and 873 deletions

View file

@ -18,6 +18,13 @@ Drone documentation is organized into several categories:
* [CLI Reference](http://readme.drone.io/devs/cli/) * [CLI Reference](http://readme.drone.io/devs/cli/)
* [API Reference](http://readme.drone.io/devs/api/builds) * [API Reference](http://readme.drone.io/devs/api/builds)
### Documentation for 0.5 (unstable)
If you are using the 0.5 unstable release (master branch) please see the updated documentation:
* [Setup Guide](http://readme.drone.io/0.5/manage/server/)
* [Build Guide](http://readme.drone.io/0.5/usage/overview/)
### Community, Help ### Community, Help
Contributions, questions, and comments are welcomed and encouraged. Drone developers hang out in the [drone/drone](https://gitter.im/drone/drone) room on gitter. We ask that you please post your questions to [gitter](https://gitter.im/drone/drone) before creating an issue. Contributions, questions, and comments are welcomed and encouraged. Drone developers hang out in the [drone/drone](https://gitter.im/drone/drone) room on gitter. We ask that you please post your questions to [gitter](https://gitter.im/drone/drone) before creating an issue.
@ -51,10 +58,4 @@ make gen # Generate code
make build # Build the binary make build # Build the binary
``` ```
If you are seeing slow compile times please install the following:
```sh
go install github.com/mattn/go-sqlite3
```
If you are having trouble building this project please reference its `.drone.yml` file. Everything you need to know about building Drone is defined in that file. If you are having trouble building this project please reference its `.drone.yml` file. Everything you need to know about building Drone is defined in that file.

View file

@ -168,7 +168,7 @@ func start(c *cli.Context) {
for { for {
if err := r.run(); err != nil { if err := r.run(); err != nil {
dur := c.Duration("backoff") dur := c.Duration("backoff")
logrus.Warnf("Attempting to reconnect in %v", dur) logrus.Warnf("reconnect in %v. %s", dur, err.Error())
time.Sleep(dur) time.Sleep(dur)
} }
} }

View file

@ -320,141 +320,6 @@ func start(c *cli.Context) error {
) )
} }
//
// func setupCache(c *cli.Context) cache.Cache {
// return cache.NewTTL(
// c.Duration("cache-ttl"),
// )
// }
//
// func setupBus(c *cli.Context) bus.Bus {
// return bus.New()
// }
//
// func setupQueue(c *cli.Context) queue.Queue {
// return queue.New()
// }
//
// func setupStream(c *cli.Context) stream.Stream {
// return stream.New()
// }
//
// func setupStore(c *cli.Context) store.Store {
// return datastore.New(
// c.String("driver"),
// c.String("datasource"),
// )
// }
//
// func setupRemote(c *cli.Context) remote.Remote {
// var remote remote.Remote
// var err error
// switch {
// case c.Bool("github"):
// remote, err = setupGithub(c)
// case c.Bool("gitlab"):
// remote, err = setupGitlab(c)
// case c.Bool("bitbucket"):
// remote, err = setupBitbucket(c)
// case c.Bool("stash"):
// remote, err = setupStash(c)
// case c.Bool("gogs"):
// remote, err = setupGogs(c)
// default:
// err = fmt.Errorf("version control system not configured")
// }
// if err != nil {
// logrus.Fatalln(err)
// }
// return remote
// }
//
// func setupBitbucket(c *cli.Context) (remote.Remote, error) {
// return bitbucket.New(
// c.String("bitbucket-client"),
// c.String("bitbucket-server"),
// ), nil
// }
//
// func setupGogs(c *cli.Context) (remote.Remote, error) {
// return gogs.New(gogs.Opts{
// URL: c.String("gogs-server"),
// Username: c.String("gogs-git-username"),
// Password: c.String("gogs-git-password"),
// PrivateMode: c.Bool("gogs-private-mode"),
// SkipVerify: c.Bool("gogs-skip-verify"),
// })
// }
//
// func setupStash(c *cli.Context) (remote.Remote, error) {
// return bitbucketserver.New(bitbucketserver.Opts{
// URL: c.String("stash-server"),
// Username: c.String("stash-git-username"),
// Password: c.String("stash-git-password"),
// ConsumerKey: c.String("stash-consumer-key"),
// ConsumerRSA: c.String("stash-consumer-rsa"),
// SkipVerify: c.Bool("stash-skip-verify"),
// })
// }
//
// func setupGitlab(c *cli.Context) (remote.Remote, error) {
// return gitlab.New(gitlab.Opts{
// URL: c.String("gitlab-server"),
// Client: c.String("gitlab-client"),
// Secret: c.String("gitlab-sercret"),
// Username: c.String("gitlab-git-username"),
// Password: c.String("gitlab-git-password"),
// PrivateMode: c.Bool("gitlab-private-mode"),
// SkipVerify: c.Bool("gitlab-skip-verify"),
// })
// }
//
// func setupGithub(c *cli.Context) (remote.Remote, error) {
// return github.New(
// c.String("github-server"),
// c.String("github-client"),
// c.String("github-sercret"),
// c.StringSlice("github-scope"),
// c.Bool("github-private-mode"),
// c.Bool("github-skip-verify"),
// c.BoolT("github-merge-ref"),
// )
// }
//
// func setupConfig(c *cli.Context) *server.Config {
// return &server.Config{
// Open: c.Bool("open"),
// Yaml: c.String("yaml"),
// Secret: c.String("agent-secret"),
// Admins: sliceToMap(c.StringSlice("admin")),
// Orgs: sliceToMap(c.StringSlice("orgs")),
// }
// }
//
// func sliceToMap(s []string) map[string]bool {
// v := map[string]bool{}
// for _, ss := range s {
// v[ss] = true
// }
// return v
// }
//
// func printSecret(c *cli.Context) error {
// secret := c.String("agent-secret")
// if secret == "" {
// return fmt.Errorf("missing DRONE_AGENT_SECRET configuration parameter")
// }
// t := token.New(secret, "")
// s, err := t.Sign(secret)
// if err != nil {
// return fmt.Errorf("invalid value for DRONE_AGENT_SECRET. %s", s)
// }
//
// logrus.Infof("using agent secret %s", secret)
// logrus.Warnf("agents can connect with token %s", s)
// return nil
// }
var agreement = ` var agreement = `
--- ---

115
remote/gerrit/gerrit.go Normal file
View file

@ -0,0 +1,115 @@
package gerrit
import (
"fmt"
"net"
"net/http"
"net/url"
"github.com/drone/drone/model"
"github.com/drone/drone/remote"
)
// IMPORTANT Gerrit support is not yet implemented. This file is a placeholder
// for future implementaiton.
// Opts defines configuration options.
type Opts struct {
URL string // Gerrit server url.
Username string // Optional machine account username.
Password string // Optional machine account password.
PrivateMode bool // Gerrit is running in private mode.
SkipVerify bool // Skip ssl verification.
}
type client struct {
URL string
Machine string
Username string
Password string
PrivateMode bool
SkipVerify bool
}
// New returns a Remote implementation that integrates with Getter, an open
// source Git hosting service and code review system.
func New(opts Opts) (remote.Remote, error) {
url, err := url.Parse(opts.URL)
if err != nil {
return nil, err
}
host, _, err := net.SplitHostPort(url.Host)
if err == nil {
url.Host = host
}
return &client{
URL: opts.URL,
Machine: url.Host,
Username: opts.Username,
Password: opts.Password,
PrivateMode: opts.PrivateMode,
SkipVerify: opts.SkipVerify,
}, nil
}
// Login authenticates an account with Gerrit using oauth authenticaiton. The
// Gerrit account details are returned when the user is successfully authenticated.
func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) {
return nil, nil
}
// Auth is not supported by the Gerrit driver.
func (c *client) Auth(token, secret string) (string, error) {
return "", fmt.Errorf("Not Implemented")
}
// Teams is not supported by the Gerrit driver.
func (c *client) Teams(u *model.User) ([]*model.Team, error) {
var empty []*model.Team
return empty, nil
}
// Repo is not supported by the Gerrit driver.
func (c *client) Repo(u *model.User, owner, name string) (*model.Repo, error) {
return nil, nil
}
// Repos is not supported by the Gerrit driver.
func (c *client) Repos(u *model.User) ([]*model.RepoLite, error) {
return nil, nil
}
// Perm is not supported by the Gerrit driver.
func (c *client) Perm(u *model.User, owner, name string) (*model.Perm, error) {
return nil, nil
}
// File is not supported by the Gerrit driver.
func (c *client) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) {
return nil, nil
}
// Status is not supported by the Gogs driver.
func (c *client) Status(u *model.User, r *model.Repo, b *model.Build, link string) error {
return nil
}
// Netrc is not supported by the Gerrit driver.
func (c *client) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
return nil, nil
}
// Activate is not supported by the Gerrit driver.
func (c *client) Activate(u *model.User, r *model.Repo, link string) error {
return nil
}
// Deactivate is not supported by the Gogs driver.
func (c *client) Deactivate(u *model.User, r *model.Repo, link string) error {
return nil
}
// Hook is not supported by the Gerrit driver.
func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
return nil, nil, nil
}

126
remote/github/convert.go Normal file
View file

@ -0,0 +1,126 @@
package github
import (
"github.com/drone/drone/model"
"github.com/google/go-github/github"
)
const defaultBranch = "master"
const (
statusPending = "pending"
statusSuccess = "success"
statusFailure = "failure"
statusError = "error"
)
const (
descPending = "this build is pending"
descSuccess = "the build was successful"
descFailure = "the build failed"
descError = "oops, something went wrong"
)
// convertStatus is a helper function used to convert a Drone status to a
// GitHub commit status.
func convertStatus(status string) string {
switch status {
case model.StatusPending, model.StatusRunning:
return statusPending
case model.StatusSuccess:
return statusSuccess
case model.StatusFailure:
return statusFailure
default:
return statusError
}
}
// convertDesc is a helper function used to convert a Drone status to a
// GitHub status description.
func convertDesc(status string) string {
switch status {
case model.StatusPending, model.StatusRunning:
return descPending
case model.StatusSuccess:
return descSuccess
case model.StatusFailure:
return descFailure
default:
return descError
}
}
// convertRepo is a helper function used to convert a GitHub repository
// structure to the common Drone repository structure.
func convertRepo(from *github.Repository, private bool) *model.Repo {
repo := &model.Repo{
Owner: *from.Owner.Login,
Name: *from.Name,
FullName: *from.FullName,
Link: *from.HTMLURL,
IsPrivate: *from.Private,
Clone: *from.CloneURL,
Avatar: *from.Owner.AvatarURL,
Kind: model.RepoGit,
Branch: defaultBranch,
}
if from.DefaultBranch != nil {
repo.Branch = *from.DefaultBranch
}
if private {
repo.IsPrivate = true
}
return repo
}
// convertPerm is a helper function used to convert a GitHub repository
// permissions to the common Drone permissions structure.
func convertPerm(from *github.Repository) *model.Perm {
return &model.Perm{
Admin: (*from.Permissions)["admin"],
Push: (*from.Permissions)["push"],
Pull: (*from.Permissions)["pull"],
}
}
// convertRepoList is a helper function used to convert a GitHub repository
// list to the common Drone repository structure.
func convertRepoList(from []github.Repository) []*model.RepoLite {
var repos []*model.RepoLite
for _, repo := range from {
repos = append(repos, convertRepoLite(repo))
}
return repos
}
// convertRepoLite is a helper function used to convert a GitHub repository
// structure to the common Drone repository structure.
func convertRepoLite(from github.Repository) *model.RepoLite {
return &model.RepoLite{
Owner: *from.Owner.Login,
Name: *from.Name,
FullName: *from.FullName,
Avatar: *from.Owner.AvatarURL,
}
}
// convertTeamList is a helper function used to convert a GitHub team list to
// the common Drone repository structure.
func convertTeamList(from []github.Organization) []*model.Team {
var teams []*model.Team
for _, team := range from {
teams = append(teams, convertTeam(team))
}
return teams
}
// convertTeam is a helper function used to convert a GitHub team structure
// to the common Drone repository structure.
func convertTeam(from github.Organization) *model.Team {
return &model.Team{
Login: *from.Login,
Avatar: *from.AvatarURL,
}
}

View file

@ -0,0 +1,171 @@
package github
import (
"testing"
"github.com/drone/drone/model"
"github.com/google/go-github/github"
"github.com/franela/goblin"
)
func Test_helper(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("GitHub converter", func() {
g.It("should convert passing status", func() {
g.Assert(convertStatus(model.StatusSuccess)).Equal(statusSuccess)
})
g.It("should convert pending status", func() {
g.Assert(convertStatus(model.StatusPending)).Equal(statusPending)
g.Assert(convertStatus(model.StatusRunning)).Equal(statusPending)
})
g.It("should convert failing status", func() {
g.Assert(convertStatus(model.StatusFailure)).Equal(statusFailure)
})
g.It("should convert error status", func() {
g.Assert(convertStatus(model.StatusKilled)).Equal(statusError)
g.Assert(convertStatus(model.StatusError)).Equal(statusError)
})
g.It("should convert passing desc", func() {
g.Assert(convertDesc(model.StatusSuccess)).Equal(descSuccess)
})
g.It("should convert pending desc", func() {
g.Assert(convertDesc(model.StatusPending)).Equal(descPending)
g.Assert(convertDesc(model.StatusRunning)).Equal(descPending)
})
g.It("should convert failing desc", func() {
g.Assert(convertDesc(model.StatusFailure)).Equal(descFailure)
})
g.It("should convert error desc", func() {
g.Assert(convertDesc(model.StatusKilled)).Equal(descError)
g.Assert(convertDesc(model.StatusError)).Equal(descError)
})
g.It("should convert repository lite", func() {
from := github.Repository{
FullName: github.String("octocat/hello-world"),
Name: github.String("hello-world"),
Owner: &github.User{
AvatarURL: github.String("http://..."),
Login: github.String("octocat"),
},
}
to := convertRepoLite(from)
g.Assert(to.Avatar).Equal("http://...")
g.Assert(to.FullName).Equal("octocat/hello-world")
g.Assert(to.Owner).Equal("octocat")
g.Assert(to.Name).Equal("hello-world")
})
g.It("should convert repository list", func() {
from := []github.Repository{
{
FullName: github.String("octocat/hello-world"),
Name: github.String("hello-world"),
Owner: &github.User{
AvatarURL: github.String("http://..."),
Login: github.String("octocat"),
},
},
}
to := convertRepoList(from)
g.Assert(to[0].Avatar).Equal("http://...")
g.Assert(to[0].FullName).Equal("octocat/hello-world")
g.Assert(to[0].Owner).Equal("octocat")
g.Assert(to[0].Name).Equal("hello-world")
})
g.It("should convert repository", func() {
from := github.Repository{
FullName: github.String("octocat/hello-world"),
Name: github.String("hello-world"),
HTMLURL: github.String("https://github.com/octocat/hello-world"),
CloneURL: github.String("https://github.com/octocat/hello-world.git"),
DefaultBranch: github.String("develop"),
Private: github.Bool(true),
Owner: &github.User{
AvatarURL: github.String("http://..."),
Login: github.String("octocat"),
},
}
to := convertRepo(&from, false)
g.Assert(to.Avatar).Equal("http://...")
g.Assert(to.FullName).Equal("octocat/hello-world")
g.Assert(to.Owner).Equal("octocat")
g.Assert(to.Name).Equal("hello-world")
g.Assert(to.Branch).Equal("develop")
g.Assert(to.Kind).Equal("git")
g.Assert(to.IsPrivate).IsTrue()
g.Assert(to.Clone).Equal("https://github.com/octocat/hello-world.git")
g.Assert(to.Link).Equal("https://github.com/octocat/hello-world")
})
g.It("should convert repository permissions", func() {
from := &github.Repository{
Permissions: &map[string]bool{
"admin": true,
"push": true,
"pull": true,
},
}
to := convertPerm(from)
g.Assert(to.Push).IsTrue()
g.Assert(to.Pull).IsTrue()
g.Assert(to.Admin).IsTrue()
})
g.It("should convert team", func() {
from := github.Organization{
Login: github.String("octocat"),
AvatarURL: github.String("http://..."),
}
to := convertTeam(from)
g.Assert(to.Login).Equal("octocat")
g.Assert(to.Avatar).Equal("http://...")
})
g.It("should convert team list", func() {
from := []github.Organization{
{
Login: github.String("octocat"),
AvatarURL: github.String("http://..."),
},
}
to := convertTeamList(from)
g.Assert(to[0].Login).Equal("octocat")
g.Assert(to[0].Avatar).Equal("http://...")
})
//
// g.It("should convert user", func() {
// token := &oauth2.Token{
// AccessToken: "foo",
// RefreshToken: "bar",
// Expiry: time.Now(),
// }
// user := &internal.Account{Login: "octocat"}
// user.Links.Avatar.Href = "http://..."
//
// result := convertUser(user, token)
// g.Assert(result.Avatar).Equal(user.Links.Avatar.Href)
// g.Assert(result.Login).Equal(user.Login)
// g.Assert(result.Token).Equal(token.AccessToken)
// g.Assert(result.Token).Equal(token.AccessToken)
// g.Assert(result.Secret).Equal(token.RefreshToken)
// g.Assert(result.Expiry).Equal(token.Expiry.UTC().Unix())
// })
})
}

View file

@ -2,8 +2,9 @@ package github
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/base32"
"fmt" "fmt"
"net"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
@ -13,104 +14,111 @@ import (
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/drone/drone/remote" "github.com/drone/drone/remote"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/oauth2" "github.com/gorilla/securecookie"
"github.com/google/go-github/github" "github.com/google/go-github/github"
"golang.org/x/oauth2"
) )
const ( const (
DefaultURL = "https://github.com" // Default GitHub URL defaultURL = "https://github.com" // Default GitHub URL
DefaultAPI = "https://api.github.com" // Default GitHub API URL defaultAPI = "https://api.github.com" // Default GitHub API URL
) )
type Github struct { // Opts defines configuration options.
type Opts struct {
URL string // GitHub server url.
Client string // GitHub oauth client id.
Secret string // GitHub oauth client secret.
Scopes []string // GitHub oauth scopes
Username string // Optional machine account username.
Password string // Optional machine account password.
PrivateMode bool // GitHub is running in private mode.
SkipVerify bool // Skip ssl verification.
MergeRef bool // Clone pull requests using the merge ref.
}
// New returns a Remote implementation that integrates with a GitHub Cloud or
// GitHub Enterprise version control hosting provider.
func New(opts Opts) (remote.Remote, error) {
url, err := url.Parse(opts.URL)
if err != nil {
return nil, err
}
host, _, err := net.SplitHostPort(url.Host)
if err == nil {
url.Host = host
}
remote := &client{
API: defaultAPI,
URL: defaultURL,
Client: opts.Client,
Secret: opts.Secret,
Scope: strings.Join(opts.Scopes, ","),
PrivateMode: opts.PrivateMode,
SkipVerify: opts.SkipVerify,
MergeRef: opts.MergeRef,
Machine: url.Host,
Username: opts.Username,
Password: opts.Password,
}
if opts.URL != defaultURL {
remote.URL = strings.TrimSuffix(opts.URL, "/")
remote.API = remote.URL + "/api/v3/"
}
return remote, nil
}
type client struct {
URL string URL string
API string API string
Client string Client string
Secret string Secret string
Scope string Scope string
MergeRef string Machine string
Username string
Password string
PrivateMode bool PrivateMode bool
SkipVerify bool SkipVerify bool
} MergeRef bool
func New(url, client, secret string, scope []string, private, skipverify, mergeref bool) (remote.Remote, error) {
remote := &Github{
URL: strings.TrimSuffix(url, "/"),
Client: client,
Secret: secret,
Scope: strings.Join(scope, ","),
PrivateMode: private,
SkipVerify: skipverify,
MergeRef: "head",
}
if remote.URL == DefaultURL {
remote.API = DefaultAPI
} else {
remote.API = remote.URL + "/api/v3/"
}
if mergeref {
remote.MergeRef = "merge"
}
return remote, nil
} }
// Login authenticates the session and returns the remote user details. // Login authenticates the session and returns the remote user details.
func (g *Github) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) { func (c *client) Login(res http.ResponseWriter, req *http.Request) (*model.User, error) {
config := c.newConfig(httputil.GetURL(req))
var config = &oauth2.Config{ code := req.FormValue("code")
ClientId: g.Client,
ClientSecret: g.Secret,
Scope: g.Scope,
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", g.URL),
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", g.URL),
RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(req)),
}
// get the OAuth code
var code = req.FormValue("code")
if len(code) == 0 { if len(code) == 0 {
var random = GetRandom() rand := base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
http.Redirect(res, req, config.AuthCodeURL(random), http.StatusSeeOther) http.Redirect(res, req, config.AuthCodeURL(rand), http.StatusSeeOther)
return nil, nil return nil, nil
} }
var trans = &oauth2.Transport{ // TODO(bradrydzewski) what is the best way to provide a SkipVerify flag
Config: config, // when exchanging the token?
}
if g.SkipVerify { token, err := config.Exchange(oauth2.NoContext, code)
trans.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
var token, err = trans.Exchange(code)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error exchanging token. %s", err) return nil, err
} }
var client = NewClient(g.API, token.AccessToken, g.SkipVerify) client := c.newClientToken(token.AccessToken)
var useremail, errr = GetUserEmail(client) useremail, err := GetUserEmail(client)
if errr != nil { if err != nil {
return nil, fmt.Errorf("Error retrieving user or verified email. %s", errr) return nil, err
} }
user := model.User{} return &model.User{
user.Login = *useremail.Login Login: *useremail.Login,
user.Email = *useremail.Email Email: *useremail.Email,
user.Token = token.AccessToken Token: token.AccessToken,
user.Avatar = *useremail.AvatarURL Avatar: *useremail.AvatarURL,
return &user, nil }, nil
} }
// Auth authenticates the session and returns the remote user // Auth returns the GitHub user login for the given access token.
// login for the given token and secret func (c *client) Auth(token, secret string) (string, error) {
func (g *Github) Auth(token, secret string) (string, error) { client := c.newClientToken(token)
client := NewClient(g.API, token, g.SkipVerify)
user, _, err := client.Users.Get("") user, _, err := client.Users.Get("")
if err != nil { if err != nil {
return "", err return "", err
@ -118,117 +126,150 @@ func (g *Github) Auth(token, secret string) (string, error) {
return *user.Login, nil return *user.Login, nil
} }
func (g *Github) Teams(u *model.User) ([]*model.Team, error) { // Teams returns a list of all team membership for the GitHub account.
client := NewClient(g.API, u.Token, g.SkipVerify) func (c *client) Teams(u *model.User) ([]*model.Team, error) {
orgs, err := GetOrgs(client) client := c.newClientToken(u.Token)
if err != nil {
return nil, err opts := new(github.ListOptions)
} opts.Page = 1
var teams []*model.Team var teams []*model.Team
for _, org := range orgs { for opts.Page > 0 {
teams = append(teams, &model.Team{ list, resp, err := client.Organizations.List("", opts)
Login: *org.Login, if err != nil {
Avatar: *org.AvatarURL, return nil, err
}) }
teams = append(teams, convertTeamList(list)...)
opts.Page = resp.NextPage
} }
return teams, nil return teams, nil
} }
// Repo fetches the named repository from the remote system. // Repo returns the named GitHub repository.
func (g *Github) Repo(u *model.User, owner, name string) (*model.Repo, error) { func (c *client) Repo(u *model.User, owner, name string) (*model.Repo, error) {
client := NewClient(g.API, u.Token, g.SkipVerify) client := c.newClientToken(u.Token)
r, err := GetRepo(client, owner, name) repo, _, err := client.Repositories.Get(owner, name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return convertRepo(repo, c.PrivateMode), nil
repo := &model.Repo{
Owner: owner,
Name: name,
FullName: *r.FullName,
Link: *r.HTMLURL,
IsPrivate: *r.Private,
Clone: *r.CloneURL,
Avatar: *r.Owner.AvatarURL,
Kind: model.RepoGit,
}
if r.DefaultBranch != nil {
repo.Branch = *r.DefaultBranch
} else {
repo.Branch = "master"
}
if g.PrivateMode {
repo.IsPrivate = true
}
return repo, err
} }
// Repos fetches a list of repos from the remote system. // Repos returns a list of all repositories for GitHub account, including
func (g *Github) Repos(u *model.User) ([]*model.RepoLite, error) { // organization repositories.
client := NewClient(g.API, u.Token, g.SkipVerify) func (c *client) Repos(u *model.User) ([]*model.RepoLite, error) {
client := c.newClientToken(u.Token)
all, err := GetUserRepos(client) opts := new(github.RepositoryListOptions)
opts.PerPage = 100
opts.Page = 1
var repos []*model.RepoLite
for opts.Page > 0 {
list, resp, err := client.Repositories.List("", opts)
if err != nil {
return nil, err
}
repos = append(repos, convertRepoList(list)...)
opts.Page = resp.NextPage
}
return repos, nil
}
// Perm returns the user permissions for the named GitHub repository.
func (c *client) Perm(u *model.User, owner, name string) (*model.Perm, error) {
client := c.newClientToken(u.Token)
repo, _, err := client.Repositories.Get(owner, name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return convertPerm(repo), nil
var repos = []*model.RepoLite{}
for _, repo := range all {
repos = append(repos, &model.RepoLite{
Owner: *repo.Owner.Login,
Name: *repo.Name,
FullName: *repo.FullName,
Avatar: *repo.Owner.AvatarURL,
})
}
return repos, err
} }
// Perm fetches the named repository permissions from // File fetches the file from the Bitbucket repository and returns its contents.
// the remote system for the specified user. func (c *client) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) {
func (g *Github) Perm(u *model.User, owner, name string) (*model.Perm, error) { client := c.newClientToken(u.Token)
client := NewClient(g.API, u.Token, g.SkipVerify) opts := new(github.RepositoryContentGetOptions)
repo, err := GetRepo(client, owner, name) opts.Ref = b.Commit
data, _, _, err := client.Repositories.GetContents(r.Owner, r.Name, f, opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
m := &model.Perm{} return data.Decode()
m.Admin = (*repo.Permissions)["admin"]
m.Push = (*repo.Permissions)["push"]
m.Pull = (*repo.Permissions)["pull"]
return m, nil
} }
// File fetches a file from the remote repository and returns in string format. // Netrc returns a netrc file capable of authenticating GitHub requests and
func (g *Github) File(u *model.User, r *model.Repo, b *model.Build, f string) ([]byte, error) { // cloning GitHub repositories. The netrc will use the global machine account
client := NewClient(g.API, u.Token, g.SkipVerify) // when configured.
cfg, err := GetFile(client, r.Owner, r.Name, f, b.Commit) func (c *client) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
return cfg, err if c.Password != "" {
return &model.Netrc{
Login: c.Username,
Password: c.Password,
Machine: c.Machine,
}, nil
}
return &model.Netrc{
Login: u.Token,
Password: "x-oauth-basic",
Machine: c.Machine,
}, nil
} }
// helper function to return the bitbucket oauth2 config
func (c *client) newConfig(redirect string) *oauth2.Config {
return &oauth2.Config{
ClientID: c.Client,
ClientSecret: c.Secret,
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", c.URL),
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", c.URL),
},
RedirectURL: fmt.Sprintf("%s/authorize", redirect),
}
}
// helper function to return the bitbucket oauth2 client
func (c *client) newClientToken(token string) *github.Client {
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
tc := oauth2.NewClient(oauth2.NoContext, ts)
if c.SkipVerify {
tc.Transport.(*oauth2.Transport).Base = &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
github := github.NewClient(tc)
github.BaseURL, _ = url.Parse(c.API)
return github
}
//
// TODO(bradrydzewski) refactor below functions
//
// Status sends the commit status to the remote system. // Status sends the commit status to the remote system.
// An example would be the GitHub pull request status. // An example would be the GitHub pull request status.
func (g *Github) Status(u *model.User, r *model.Repo, b *model.Build, link string) error { func (c *client) Status(u *model.User, r *model.Repo, b *model.Build, link string) error {
client := NewClient(g.API, u.Token, g.SkipVerify) client := c.newClientToken(u.Token)
if b.Event == "deployment" { switch b.Event {
case "deployment":
return deploymentStatus(client, r, b, link) return deploymentStatus(client, r, b, link)
} else { default:
return repoStatus(client, r, b, link) return repoStatus(client, r, b, link)
} }
} }
func repoStatus(client *github.Client, r *model.Repo, b *model.Build, link string) error { func repoStatus(client *github.Client, r *model.Repo, b *model.Build, link string) error {
status := getStatus(b.Status)
desc := getDesc(b.Status)
data := github.RepoStatus{ data := github.RepoStatus{
Context: github.String("continuous-integration/drone"), Context: github.String("continuous-integration/drone"),
State: github.String(status), State: github.String(convertStatus(b.Status)),
Description: github.String(desc), Description: github.String(convertDesc(b.Status)),
TargetURL: github.String(link), TargetURL: github.String(link),
} }
_, _, err := client.Repositories.CreateStatus(r.Owner, r.Name, b.Commit, &data) _, _, err := client.Repositories.CreateStatus(r.Owner, r.Name, b.Commit, &data)
@ -239,283 +280,46 @@ var reDeploy = regexp.MustCompile(".+/deployments/(\\d+)")
func deploymentStatus(client *github.Client, r *model.Repo, b *model.Build, link string) error { func deploymentStatus(client *github.Client, r *model.Repo, b *model.Build, link string) error {
matches := reDeploy.FindStringSubmatch(b.Link) matches := reDeploy.FindStringSubmatch(b.Link)
// if the deployment was not triggered from github, don't send a deployment status
if len(matches) != 2 { if len(matches) != 2 {
return nil return nil
} }
// the deployment ID is only available in the the link to the build as the last element in the URL
id, _ := strconv.Atoi(matches[1]) id, _ := strconv.Atoi(matches[1])
status := getStatus(b.Status)
desc := getDesc(b.Status)
data := github.DeploymentStatusRequest{ data := github.DeploymentStatusRequest{
State: github.String(status), State: github.String(convertStatus(b.Status)),
Description: github.String(desc), Description: github.String(convertDesc(b.Status)),
TargetURL: github.String(link), TargetURL: github.String(link),
} }
_, _, err := client.Repositories.CreateDeploymentStatus(r.Owner, r.Name, id, &data) _, _, err := client.Repositories.CreateDeploymentStatus(r.Owner, r.Name, id, &data)
return err return err
} }
// Netrc returns a .netrc file that can be used to clone
// private repositories from a remote system.
func (g *Github) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) {
url_, err := url.Parse(g.URL)
if err != nil {
return nil, err
}
netrc := &model.Netrc{}
netrc.Login = u.Token
netrc.Password = "x-oauth-basic"
netrc.Machine = url_.Host
return netrc, nil
}
// Activate activates a repository by creating the post-commit hook and // Activate activates a repository by creating the post-commit hook and
// adding the SSH deploy key, if applicable. // adding the SSH deploy key, if applicable.
func (g *Github) Activate(u *model.User, r *model.Repo, link string) error { func (c *client) Activate(u *model.User, r *model.Repo, link string) error {
client := NewClient(g.API, u.Token, g.SkipVerify) client := c.newClientToken(u.Token)
_, err := CreateUpdateHook(client, r.Owner, r.Name, link) _, err := CreateUpdateHook(client, r.Owner, r.Name, link)
return err return err
} }
// Deactivate removes a repository by removing all the post-commit hooks // Deactivate removes a repository by removing all the post-commit hooks
// which are equal to link and removing the SSH deploy key. // which are equal to link and removing the SSH deploy key.
func (g *Github) Deactivate(u *model.User, r *model.Repo, link string) error { func (c *client) Deactivate(u *model.User, r *model.Repo, link string) error {
client := NewClient(g.API, u.Token, g.SkipVerify) client := c.newClientToken(u.Token)
return DeleteHook(client, r.Owner, r.Name, link) return DeleteHook(client, r.Owner, r.Name, link)
} }
// Hook parses the post-commit hook from the Request body // Hook parses the post-commit hook from the Request body
// and returns the required data in a standard format. // and returns the required data in a standard format.
func (g *Github) Hook(r *http.Request) (*model.Repo, *model.Build, error) { func (c *client) Hook(r *http.Request) (*model.Repo, *model.Build, error) {
switch r.Header.Get("X-Github-Event") { switch r.Header.Get("X-Github-Event") {
case "pull_request": case "pull_request":
return g.pullRequest(r) return c.pullRequest(r)
case "push": case "push":
return g.push(r) return c.push(r)
case "deployment": case "deployment":
return g.deployment(r) return c.deployment(r)
default: default:
return nil, nil, nil return nil, nil, nil
} }
} }
// push parses a hook with event type `push` and returns
// the commit data.
func (g *Github) push(r *http.Request) (*model.Repo, *model.Build, error) {
payload := GetPayload(r)
hook := &pushHook{}
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, nil, err
}
if hook.Deleted {
return nil, nil, err
}
repo := &model.Repo{}
repo.Owner = hook.Repo.Owner.Login
if len(repo.Owner) == 0 {
repo.Owner = hook.Repo.Owner.Name
}
repo.Name = hook.Repo.Name
// Generating rather than using hook.Repo.FullName as it's
// not always present
repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name)
repo.Link = hook.Repo.HTMLURL
repo.IsPrivate = hook.Repo.Private
repo.Clone = hook.Repo.CloneURL
repo.Branch = hook.Repo.DefaultBranch
repo.Kind = model.RepoGit
build := &model.Build{}
build.Event = model.EventPush
build.Commit = hook.Head.ID
build.Ref = hook.Ref
build.Link = hook.Head.URL
build.Branch = strings.Replace(build.Ref, "refs/heads/", "", -1)
build.Message = hook.Head.Message
// build.Timestamp = hook.Head.Timestamp
build.Email = hook.Head.Author.Email
build.Avatar = hook.Sender.Avatar
build.Author = hook.Sender.Login
build.Remote = hook.Repo.CloneURL
if len(build.Author) == 0 {
build.Author = hook.Head.Author.Username
// default gravatar?
}
if strings.HasPrefix(build.Ref, "refs/tags/") {
// just kidding, this is actually a tag event
build.Event = model.EventTag
}
return repo, build, nil
}
// pullRequest parses a hook with event type `pullRequest`
// and returns the commit data.
func (g *Github) pullRequest(r *http.Request) (*model.Repo, *model.Build, error) {
payload := GetPayload(r)
hook := &struct {
Action string `json:"action"`
PullRequest *github.PullRequest `json:"pull_request"`
Repo *github.Repository `json:"repository"`
}{}
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, nil, err
}
// ignore these
if hook.Action != "opened" && hook.Action != "synchronize" {
return nil, nil, nil
}
if *hook.PullRequest.State != "open" {
return nil, nil, nil
}
repo := &model.Repo{}
repo.Owner = *hook.Repo.Owner.Login
repo.Name = *hook.Repo.Name
repo.FullName = *hook.Repo.FullName
repo.Link = *hook.Repo.HTMLURL
repo.IsPrivate = *hook.Repo.Private
repo.Clone = *hook.Repo.CloneURL
repo.Kind = model.RepoGit
repo.Branch = "master"
if hook.Repo.DefaultBranch != nil {
repo.Branch = *hook.Repo.DefaultBranch
}
build := &model.Build{}
build.Event = model.EventPull
build.Commit = *hook.PullRequest.Head.SHA
build.Ref = fmt.Sprintf("refs/pull/%d/%s", *hook.PullRequest.Number, g.MergeRef)
build.Link = *hook.PullRequest.HTMLURL
build.Branch = *hook.PullRequest.Head.Ref
build.Message = *hook.PullRequest.Title
build.Author = *hook.PullRequest.User.Login
build.Avatar = *hook.PullRequest.User.AvatarURL
build.Remote = *hook.PullRequest.Base.Repo.CloneURL
build.Title = *hook.PullRequest.Title
// build.Timestamp = time.Now().UTC().Format("2006-01-02 15:04:05.000000000 +0000 MST")
return repo, build, nil
}
func (g *Github) deployment(r *http.Request) (*model.Repo, *model.Build, error) {
payload := GetPayload(r)
hook := &deployHook{}
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, nil, err
}
// for older versions of GitHub. Remove.
if hook.Deployment.ID == 0 {
hook.Deployment.ID = hook.ID
hook.Deployment.Sha = hook.Sha
hook.Deployment.Ref = hook.Ref
hook.Deployment.Task = hook.Name
hook.Deployment.Env = hook.Env
hook.Deployment.Desc = hook.Desc
}
repo := &model.Repo{}
repo.Owner = hook.Repo.Owner.Login
if len(repo.Owner) == 0 {
repo.Owner = hook.Repo.Owner.Name
}
repo.Name = hook.Repo.Name
repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name)
repo.Link = hook.Repo.HTMLURL
repo.IsPrivate = hook.Repo.Private
repo.Clone = hook.Repo.CloneURL
repo.Branch = hook.Repo.DefaultBranch
repo.Kind = model.RepoGit
// ref can be
// branch, tag, or sha
build := &model.Build{}
build.Event = model.EventDeploy
build.Commit = hook.Deployment.Sha
build.Link = hook.Deployment.Url
build.Message = hook.Deployment.Desc
build.Avatar = hook.Sender.Avatar
build.Author = hook.Sender.Login
build.Ref = hook.Deployment.Ref
build.Branch = hook.Deployment.Ref
build.Deploy = hook.Deployment.Env
// if the ref is a sha or short sha we need to manually
// construct the ref.
if strings.HasPrefix(build.Commit, build.Ref) || build.Commit == build.Ref {
build.Branch = repo.Branch
build.Ref = fmt.Sprintf("refs/heads/%s", repo.Branch)
}
// if the ref is a branch we should make sure it has refs/heads prefix
if !strings.HasPrefix(build.Ref, "refs/") { // branch or tag
build.Ref = fmt.Sprintf("refs/heads/%s", build.Branch)
}
return repo, build, nil
}
func (g *Github) String() string {
return "github"
}
const (
StatusPending = "pending"
StatusSuccess = "success"
StatusFailure = "failure"
StatusError = "error"
)
const (
DescPending = "this build is pending"
DescSuccess = "the build was successful"
DescFailure = "the build failed"
DescError = "oops, something went wrong"
)
// getStatus is a helper functin that converts a Drone
// status to a GitHub status.
func getStatus(status string) string {
switch status {
case model.StatusPending, model.StatusRunning:
return StatusPending
case model.StatusSuccess:
return StatusSuccess
case model.StatusFailure:
return StatusFailure
case model.StatusError, model.StatusKilled:
return StatusError
default:
return StatusError
}
}
// getDesc is a helper function that generates a description
// message for the build based on the status.
func getDesc(status string) string {
switch status {
case model.StatusPending, model.StatusRunning:
return DescPending
case model.StatusSuccess:
return DescSuccess
case model.StatusFailure:
return DescFailure
case model.StatusError, model.StatusKilled:
return DescError
default:
return DescError
}
}

View file

@ -11,7 +11,7 @@ import (
func TestHook(t *testing.T) { func TestHook(t *testing.T) {
var ( var (
github Github github client
r *http.Request r *http.Request
body *bytes.Buffer body *bytes.Buffer
) )
@ -20,7 +20,7 @@ func TestHook(t *testing.T) {
g.Describe("Hook", func() { g.Describe("Hook", func() {
g.BeforeEach(func() { g.BeforeEach(func() {
github = Github{} github = client{}
body = bytes.NewBuffer([]byte{}) body = bytes.NewBuffer([]byte{})
r, _ = http.NewRequest("POST", "https://drone.com/hook", body) r, _ = http.NewRequest("POST", "https://drone.com/hook", body)
}) })
@ -31,11 +31,11 @@ func TestHook(t *testing.T) {
}) })
g.It("Should set build author to the pull request author", func() { g.It("Should set build author to the pull request author", func() {
hookJson, err := ioutil.ReadFile("fixtures/pull_request.json") hookJSON, ioerr := ioutil.ReadFile("fixtures/pull_request.json")
if err != nil { if ioerr != nil {
panic(err) panic(ioerr)
} }
body.Write(hookJson) body.Write(hookJSON)
_, build, err := github.Hook(r) _, build, err := github.Hook(r)
g.Assert(err).Equal(nil) g.Assert(err).Equal(nil)

View file

@ -1,40 +1,14 @@
package github package github
import ( import (
"crypto/tls"
"encoding/base32"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"strings" "strings"
"github.com/drone/drone/shared/oauth2"
"github.com/google/go-github/github" "github.com/google/go-github/github"
"github.com/gorilla/securecookie"
) )
// NewClient is a helper function that returns a new GitHub
// client using the provided OAuth token.
func NewClient(uri, token string, skipVerify bool) *github.Client {
t := &oauth2.Transport{
Token: &oauth2.Token{AccessToken: token},
}
// this is for GitHub enterprise users that are using
// self-signed certificates.
if skipVerify {
t.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
c := github.NewClient(t.Client())
c.BaseURL, _ = url.Parse(uri)
return c
}
// GetUserEmail is a heper function that retrieves the currently // GetUserEmail is a heper function that retrieves the currently
// authenticated user from GitHub + Email address. // authenticated user from GitHub + Email address.
func GetUserEmail(client *github.Client) (*github.User, error) { func GetUserEmail(client *github.Client) (*github.User, error) {
@ -58,7 +32,7 @@ func GetUserEmail(client *github.Client) (*github.User, error) {
// WARNING, HACK // WARNING, HACK
// for out-of-date github enterprise editions the primary // for out-of-date github enterprise editions the primary
// and verified fields won't exist. // and verified fields won't exist.
if !strings.HasPrefix(*user.URL, DefaultAPI) && len(emails) != 0 { if !strings.HasPrefix(*user.URL, defaultAPI) && len(emails) != 0 {
user.Email = emails[0].Email user.Email = emails[0].Email
return user, nil return user, nil
} }
@ -66,56 +40,6 @@ func GetUserEmail(client *github.Client) (*github.User, error) {
return nil, fmt.Errorf("No verified Email address for GitHub account") return nil, fmt.Errorf("No verified Email address for GitHub account")
} }
// GetRepo is a helper function that returns a named repo
func GetRepo(client *github.Client, owner, repo string) (*github.Repository, error) {
r, _, err := client.Repositories.Get(owner, repo)
return r, err
}
// GetUserRepos is a helper function that returns a list of
// all user repositories. Paginated results are aggregated into
// a single list.
func GetUserRepos(client *github.Client) ([]github.Repository, error) {
var repos []github.Repository
var opts = github.RepositoryListOptions{}
opts.PerPage = 100
opts.Page = 1
// loop through user repository list
for opts.Page > 0 {
list, resp, err := client.Repositories.List("", &opts)
if err != nil {
return nil, err
}
repos = append(repos, list...)
// increment the next page to retrieve
opts.Page = resp.NextPage
}
return repos, nil
}
// GetOrgs is a helper function that returns a list of
// all orgs that a user belongs to.
func GetOrgs(client *github.Client) ([]github.Organization, error) {
var orgs []github.Organization
var opts = github.ListOptions{}
opts.Page = 1
for opts.Page > 0 {
list, resp, err := client.Organizations.List("", &opts)
if err != nil {
return nil, err
}
orgs = append(orgs, list...)
// increment the next page to retrieve
opts.Page = resp.NextPage
}
return orgs, nil
}
// GetHook is a heper function that retrieves a hook by // GetHook is a heper function that retrieves a hook by
// hostname. To do this, it will retrieve a list of all hooks // hostname. To do this, it will retrieve a list of all hooks
// and iterate through the list. // and iterate through the list.
@ -136,6 +60,7 @@ func GetHook(client *github.Client, owner, name, url string) (*github.Hook, erro
return nil, nil return nil, nil
} }
// DeleteHook does exactly what you think it does.
func DeleteHook(client *github.Client, owner, name, url string) error { func DeleteHook(client *github.Client, owner, name, url string) error {
hook, err := GetHook(client, owner, name, url) hook, err := GetHook(client, owner, name, url)
if err != nil { if err != nil {
@ -179,24 +104,6 @@ func CreateUpdateHook(client *github.Client, owner, name, url string) (*github.H
return CreateHook(client, owner, name, url) return CreateHook(client, owner, name, url)
} }
// GetFile is a heper function that retrieves a file from
// GitHub and returns its contents in byte array format.
func GetFile(client *github.Client, owner, name, path, ref string) ([]byte, error) {
var opts = new(github.RepositoryContentGetOptions)
opts.Ref = ref
content, _, _, err := client.Repositories.GetContents(owner, name, path, opts)
if err != nil {
return nil, err
}
return content.Decode()
}
// GetRandom is a helper function that generates a 32-bit random
// key, base32 encoded as a string value.
func GetRandom() string {
return base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
}
// GetPayload is a helper function that will parse the JSON payload. It will // GetPayload is a helper function that will parse the JSON payload. It will
// first check for a `payload` parameter in a POST, but can fallback to a // first check for a `payload` parameter in a POST, but can fallback to a
// raw JSON body as well. // raw JSON body as well.

184
remote/github/parser.go Normal file
View file

@ -0,0 +1,184 @@
package github
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/drone/drone/model"
"github.com/google/go-github/github"
)
// push parses a hook with event type `push` and returns
// the commit data.
func (c *client) push(r *http.Request) (*model.Repo, *model.Build, error) {
payload := GetPayload(r)
hook := &pushHook{}
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, nil, err
}
if hook.Deleted {
return nil, nil, err
}
repo := &model.Repo{}
repo.Owner = hook.Repo.Owner.Login
if len(repo.Owner) == 0 {
repo.Owner = hook.Repo.Owner.Name
}
repo.Name = hook.Repo.Name
// Generating rather than using hook.Repo.FullName as it's
// not always present
repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name)
repo.Link = hook.Repo.HTMLURL
repo.IsPrivate = hook.Repo.Private
repo.Clone = hook.Repo.CloneURL
repo.Branch = hook.Repo.DefaultBranch
repo.Kind = model.RepoGit
build := &model.Build{}
build.Event = model.EventPush
build.Commit = hook.Head.ID
build.Ref = hook.Ref
build.Link = hook.Head.URL
build.Branch = strings.Replace(build.Ref, "refs/heads/", "", -1)
build.Message = hook.Head.Message
// build.Timestamp = hook.Head.Timestamp
build.Email = hook.Head.Author.Email
build.Avatar = hook.Sender.Avatar
build.Author = hook.Sender.Login
build.Remote = hook.Repo.CloneURL
if len(build.Author) == 0 {
build.Author = hook.Head.Author.Username
// default gravatar?
}
if strings.HasPrefix(build.Ref, "refs/tags/") {
// just kidding, this is actually a tag event
build.Event = model.EventTag
}
return repo, build, nil
}
// pullRequest parses a hook with event type `pullRequest`
// and returns the commit data.
func (c *client) pullRequest(r *http.Request) (*model.Repo, *model.Build, error) {
payload := GetPayload(r)
hook := &struct {
Action string `json:"action"`
PullRequest *github.PullRequest `json:"pull_request"`
Repo *github.Repository `json:"repository"`
}{}
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, nil, err
}
// ignore these
if hook.Action != "opened" && hook.Action != "synchronize" {
return nil, nil, nil
}
if *hook.PullRequest.State != "open" {
return nil, nil, nil
}
repo := &model.Repo{}
repo.Owner = *hook.Repo.Owner.Login
repo.Name = *hook.Repo.Name
repo.FullName = *hook.Repo.FullName
repo.Link = *hook.Repo.HTMLURL
repo.IsPrivate = *hook.Repo.Private
repo.Clone = *hook.Repo.CloneURL
repo.Kind = model.RepoGit
repo.Branch = "master"
if hook.Repo.DefaultBranch != nil {
repo.Branch = *hook.Repo.DefaultBranch
}
build := &model.Build{}
build.Event = model.EventPull
build.Commit = *hook.PullRequest.Head.SHA
build.Link = *hook.PullRequest.HTMLURL
build.Branch = *hook.PullRequest.Head.Ref
build.Message = *hook.PullRequest.Title
build.Author = *hook.PullRequest.User.Login
build.Avatar = *hook.PullRequest.User.AvatarURL
build.Remote = *hook.PullRequest.Base.Repo.CloneURL
build.Title = *hook.PullRequest.Title
if c.MergeRef {
build.Ref = fmt.Sprintf("refs/pull/%d/merge", *hook.PullRequest.Number)
} else {
build.Ref = fmt.Sprintf("refs/pull/%d/head", *hook.PullRequest.Number)
}
// build.Timestamp = time.Now().UTC().Format("2006-01-02 15:04:05.000000000 +0000 MST")
return repo, build, nil
}
func (c *client) deployment(r *http.Request) (*model.Repo, *model.Build, error) {
payload := GetPayload(r)
hook := &deployHook{}
err := json.Unmarshal(payload, hook)
if err != nil {
return nil, nil, err
}
// for older versions of GitHub. Remove.
if hook.Deployment.ID == 0 {
hook.Deployment.ID = hook.ID
hook.Deployment.Sha = hook.Sha
hook.Deployment.Ref = hook.Ref
hook.Deployment.Task = hook.Name
hook.Deployment.Env = hook.Env
hook.Deployment.Desc = hook.Desc
}
repo := &model.Repo{}
repo.Owner = hook.Repo.Owner.Login
if len(repo.Owner) == 0 {
repo.Owner = hook.Repo.Owner.Name
}
repo.Name = hook.Repo.Name
repo.FullName = fmt.Sprintf("%s/%s", repo.Owner, repo.Name)
repo.Link = hook.Repo.HTMLURL
repo.IsPrivate = hook.Repo.Private
repo.Clone = hook.Repo.CloneURL
repo.Branch = hook.Repo.DefaultBranch
repo.Kind = model.RepoGit
// ref can be
// branch, tag, or sha
build := &model.Build{}
build.Event = model.EventDeploy
build.Commit = hook.Deployment.Sha
build.Link = hook.Deployment.URL
build.Message = hook.Deployment.Desc
build.Avatar = hook.Sender.Avatar
build.Author = hook.Sender.Login
build.Ref = hook.Deployment.Ref
build.Branch = hook.Deployment.Ref
build.Deploy = hook.Deployment.Env
// if the ref is a sha or short sha we need to manually
// construct the ref.
if strings.HasPrefix(build.Commit, build.Ref) || build.Commit == build.Ref {
build.Branch = repo.Branch
build.Ref = fmt.Sprintf("refs/heads/%s", repo.Branch)
}
// if the ref is a branch we should make sure it has refs/heads prefix
if !strings.HasPrefix(build.Ref, "refs/") { // branch or tag
build.Ref = fmt.Sprintf("refs/heads/%s", build.Branch)
}
return repo, build, nil
}

View file

@ -0,0 +1 @@
package github

View file

@ -1,8 +1,5 @@
package github package github
type postHook struct {
}
type pushHook struct { type pushHook struct {
Ref string `json:"ref"` Ref string `json:"ref"`
Deleted bool `json:"deleted"` Deleted bool `json:"deleted"`
@ -54,7 +51,7 @@ type deployHook struct {
Ref string `json:"ref"` Ref string `json:"ref"`
Task string `json:"task"` Task string `json:"task"`
Env string `json:"environment"` Env string `json:"environment"`
Url string `json:"url"` URL string `json:"url"`
Desc string `json:"description"` Desc string `json:"description"`
} `json:"deployment"` } `json:"deployment"`
@ -78,9 +75,8 @@ type deployHook struct {
DefaultBranch string `json:"default_branch"` DefaultBranch string `json:"default_branch"`
} `json:"repository"` } `json:"repository"`
// these are legacy fields that have been moded // these are legacy fields that have been added to the deployment section.
// to the deployment section. They are here for // They are here for older versions of GitHub and will be removed.
// older versions of GitHub and will be removed
ID int64 `json:"id"` ID int64 `json:"id"`
Sha string `json:"sha"` Sha string `json:"sha"`

View file

@ -90,13 +90,15 @@ func setupGitlab(c *cli.Context) (remote.Remote, error) {
// helper function to setup the GitHub remote from the CLI arguments. // helper function to setup the GitHub remote from the CLI arguments.
func setupGithub(c *cli.Context) (remote.Remote, error) { func setupGithub(c *cli.Context) (remote.Remote, error) {
return github.New( return github.New(github.Opts{
c.String("github-server"), URL: c.String("github-server"),
c.String("github-client"), Client: c.String("github-client"),
c.String("github-sercret"), Secret: c.String("github-sercret"),
c.StringSlice("github-scope"), Scopes: c.StringSlice("github-scope"),
c.Bool("github-private-mode"), Username: c.String("github-git-username"),
c.Bool("github-skip-verify"), Password: c.String("github-git-password"),
c.BoolT("github-merge-ref"), PrivateMode: c.Bool("github-private-mode"),
) SkipVerify: c.Bool("github-skip-verify"),
MergeRef: c.BoolT("github-merge-ref"),
})
} }

View file

@ -8,14 +8,18 @@ import (
// AuthorizeAgent authorizes requsts from build agents to access the queue. // AuthorizeAgent authorizes requsts from build agents to access the queue.
func AuthorizeAgent(c *gin.Context) { func AuthorizeAgent(c *gin.Context) {
secret := c.MustGet("agent").(string) secret := c.MustGet("agent").(string)
if secret == "" {
c.String(401, "invalid or empty token.")
return
}
parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) {
return secret, nil return secret, nil
}) })
if err != nil { if err != nil {
c.AbortWithError(403, err) c.String(500, "invalid or empty token. %s", err)
} else if parsed.Kind != token.AgentToken { } else if parsed.Kind != token.AgentToken {
c.AbortWithStatus(403) c.String(403, "invalid token. please use an agent token")
} else { } else {
c.Next() c.Next()
} }

View file

@ -1,15 +1,16 @@
package server package server
import ( import (
"encoding/base32"
"net/http" "net/http"
"time" "time"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/drone/drone/remote" "github.com/drone/drone/remote"
"github.com/drone/drone/shared/crypto"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/token" "github.com/drone/drone/shared/token"
"github.com/drone/drone/store" "github.com/drone/drone/store"
"github.com/gorilla/securecookie"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -39,12 +40,23 @@ func GetLogin(c *gin.Context) {
if err != nil { if err != nil {
// if self-registration is disabled we should return a not authorized error // if self-registration is disabled we should return a not authorized error
if !config.Open { if !config.Open && !config.IsAdmin(tmpuser) {
logrus.Errorf("cannot register %s. registration closed", tmpuser.Login) logrus.Errorf("cannot register %s. registration closed", tmpuser.Login)
c.Redirect(303, "/login?error=access_denied") c.Redirect(303, "/login?error=access_denied")
return return
} }
// if self-registration is enabled for whitelisted organizations we need to
// check the user's organization membership.
if len(config.Orgs) != 0 {
teams, terr := remote.Teams(c, tmpuser)
if terr != nil || config.IsMember(teams) == false {
logrus.Errorf("cannot verify team membership for %s.", u.Login)
c.Redirect(303, "/login?error=access_denied")
return
}
}
// create the user account // create the user account
u = &model.User{ u = &model.User{
Login: tmpuser.Login, Login: tmpuser.Login,
@ -52,7 +64,9 @@ func GetLogin(c *gin.Context) {
Secret: tmpuser.Secret, Secret: tmpuser.Secret,
Email: tmpuser.Email, Email: tmpuser.Email,
Avatar: tmpuser.Avatar, Avatar: tmpuser.Avatar,
Hash: crypto.Rand(), Hash: base32.StdEncoding.EncodeToString(
securecookie.GenerateRandomKey(32),
),
} }
// insert the user into the database // insert the user into the database
@ -69,33 +83,23 @@ func GetLogin(c *gin.Context) {
u.Email = tmpuser.Email u.Email = tmpuser.Email
u.Avatar = tmpuser.Avatar u.Avatar = tmpuser.Avatar
// if self-registration is enabled for whitelisted organizations we need to
// check the user's organization membership.
if len(config.Orgs) != 0 {
teams, terr := remote.Teams(c, u)
if terr != nil || config.IsMember(teams) == false {
logrus.Errorf("cannot verify team membership for %s.", u.Login)
c.Redirect(303, "/login?error=access_denied")
return
}
}
if err := store.UpdateUser(c, u); err != nil { if err := store.UpdateUser(c, u); err != nil {
logrus.Errorf("cannot update %s. %s", u.Login, err) logrus.Errorf("cannot update %s. %s", u.Login, err)
c.Redirect(303, "/login?error=internal_error") c.Redirect(303, "/login?error=internal_error")
return return
} }
if len(config.Orgs) != 0 {
teams, terr := remote.Teams(c, u)
if terr != nil {
logrus.Errorf("cannot verify team membership for %s. %s.", tmpuser.Login, terr)
c.Redirect(303, "/login?error=access_denied")
return
}
var member bool
for _, team := range teams {
if config.Orgs[team.Login] {
member = true
break
}
}
if !member {
logrus.Errorf("cannot verify team membership for %s. %s.", tmpuser.Login, terr)
c.Redirect(303, "/login?error=access_denied")
return
}
}
exp := time.Now().Add(time.Hour * 72).Unix() exp := time.Now().Add(time.Hour * 72).Unix()
token := token.New(token.SessToken, u.Login) token := token.New(token.SessToken, u.Login)
tokenstr, err := token.SignExpires(u.Hash, exp) tokenstr, err := token.SignExpires(u.Hash, exp)

View file

@ -1,15 +1,16 @@
package server package server
import ( import (
"encoding/base32"
"fmt" "fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/securecookie"
"github.com/drone/drone/cache" "github.com/drone/drone/cache"
"github.com/drone/drone/remote" "github.com/drone/drone/remote"
"github.com/drone/drone/router/middleware/session" "github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/shared/crypto"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/token" "github.com/drone/drone/shared/token"
"github.com/drone/drone/store" "github.com/drone/drone/store"
@ -54,7 +55,9 @@ func PostRepo(c *gin.Context) {
r.AllowPush = true r.AllowPush = true
r.AllowPull = true r.AllowPull = true
r.Timeout = 60 // 1 hour default build time r.Timeout = 60 // 1 hour default build time
r.Hash = crypto.Rand() r.Hash = base32.StdEncoding.EncodeToString(
securecookie.GenerateRandomKey(32),
)
// crates the jwt token used to verify the repository // crates the jwt token used to verify the repository
t := token.New(token.HookToken, r.FullName) t := token.New(token.HookToken, r.FullName)

View file

@ -5,9 +5,9 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/drone/drone-exp/shared/crypto"
"github.com/drone/drone/cache" "github.com/drone/drone/cache"
"github.com/drone/drone/router/middleware/session" "github.com/drone/drone/router/middleware/session"
"github.com/drone/drone/shared/crypto"
"github.com/drone/drone/shared/token" "github.com/drone/drone/shared/token"
"github.com/drone/drone/store" "github.com/drone/drone/store"
) )

View file

@ -1,12 +1,13 @@
package server package server
import ( import (
"encoding/base32"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/securecookie"
"github.com/drone/drone/model" "github.com/drone/drone/model"
"github.com/drone/drone/shared/crypto"
"github.com/drone/drone/store" "github.com/drone/drone/store"
) )
@ -14,9 +15,9 @@ func GetUsers(c *gin.Context) {
users, err := store.GetUserList(c) users, err := store.GetUserList(c)
if err != nil { if err != nil {
c.String(500, "Error getting user list. %s", err) c.String(500, "Error getting user list. %s", err)
} else { return
c.JSON(200, users)
} }
c.JSON(200, users)
} }
func GetUser(c *gin.Context) { func GetUser(c *gin.Context) {
@ -41,7 +42,6 @@ func PatchUser(c *gin.Context) {
c.AbortWithStatus(http.StatusNotFound) c.AbortWithStatus(http.StatusNotFound)
return return
} }
user.Admin = in.Admin
user.Active = in.Active user.Active = in.Active
err = store.UpdateUser(c, user) err = store.UpdateUser(c, user)
@ -65,7 +65,9 @@ func PostUser(c *gin.Context) {
Login: in.Login, Login: in.Login,
Email: in.Email, Email: in.Email,
Avatar: in.Avatar, Avatar: in.Avatar,
Hash: crypto.Rand(), Hash: base32.StdEncoding.EncodeToString(
securecookie.GenerateRandomKey(32),
),
} }
if err = store.CreateUser(c, user); err != nil { if err = store.CreateUser(c, user); err != nil {
c.String(http.StatusInternalServerError, err.Error()) c.String(http.StatusInternalServerError, err.Error())

View file

@ -1,118 +0,0 @@
package crypto
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io"
"code.google.com/p/go.crypto/ssh"
"github.com/square/go-jose"
)
const (
RSA_BITS = 2048 // Default number of bits in an RSA key
RSA_BITS_MIN = 768 // Minimum number of bits in an RSA key
)
// standard characters allowed in token string.
var chars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
// default token length
var length = 32
// Rand generates a 32-bit random string.
func Rand() string {
b := make([]byte, length)
r := make([]byte, length+(length/4)) // storage for random bytes.
clen := byte(len(chars))
maxrb := byte(256 - (256 % len(chars)))
i := 0
for {
io.ReadFull(rand.Reader, r)
for _, c := range r {
if c >= maxrb {
// Skip this number to avoid modulo bias.
continue
}
b[i] = chars[c%clen]
i++
if i == length {
return string(b)
}
}
}
}
// helper function to generate an RSA Private Key.
func GeneratePrivateKey() (*rsa.PrivateKey, error) {
return rsa.GenerateKey(rand.Reader, RSA_BITS)
}
// helper function that marshalls an RSA Public Key to an SSH
// .authorized_keys format
func MarshalPublicKey(public *rsa.PublicKey) []byte {
private, err := ssh.NewPublicKey(public)
if err != nil {
return []byte{}
}
return ssh.MarshalAuthorizedKey(private)
}
// helper function that marshalls an RSA Private Key to
// a PEM encoded file.
func MarshalPrivateKey(private *rsa.PrivateKey) []byte {
marshaled := x509.MarshalPKCS1PrivateKey(private)
encoded := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Headers: nil, Bytes: marshaled})
return encoded
}
// UnmarshalPrivateKey is a helper function that unmarshals a PEM
// bytes to an RSA Private Key
func UnmarshalPrivateKey(private []byte) *rsa.PrivateKey {
decoded, _ := pem.Decode(private)
parsed, err := x509.ParsePKCS1PrivateKey(decoded.Bytes)
if err != nil {
return nil
}
return parsed
}
// Encrypt encrypts a secret string.
func Encrypt(in, privKey string) (string, error) {
rsaPrivKey, err := decodePrivateKey(privKey)
if err != nil {
return "", err
}
return encrypt(in, &rsaPrivKey.PublicKey)
}
// decodePrivateKey is a helper function that unmarshals a PEM
// bytes to an RSA Private Key
func decodePrivateKey(privateKey string) (*rsa.PrivateKey, error) {
derBlock, _ := pem.Decode([]byte(privateKey))
return x509.ParsePKCS1PrivateKey(derBlock.Bytes)
}
// encrypt encrypts a plaintext variable using JOSE with
// RSA_OAEP and A128GCM algorithms.
func encrypt(text string, pubKey *rsa.PublicKey) (string, error) {
var encrypted string
var plaintext = []byte(text)
// Creates a new encrypter using defaults
encrypter, err := jose.NewEncrypter(jose.RSA_OAEP, jose.A128GCM, pubKey)
if err != nil {
return encrypted, err
}
// Encrypts the plaintext value and serializes
// as a JOSE string.
object, err := encrypter.Encrypt(plaintext)
if err != nil {
return encrypted, err
}
return object.CompactSerialize()
}

View file

@ -1,68 +0,0 @@
package crypto
import (
"testing"
"github.com/franela/goblin"
"github.com/square/go-jose"
)
func TestKeys(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Generate Key", func() {
g.It("Generates a private key", func() {
_, err := GeneratePrivateKey()
g.Assert(err == nil).IsTrue()
})
})
}
func Test_Encrypt(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Secure", func() {
g.It("Should encrypt a string", func() {
ciphertext, err := Encrypt("top_secret", fakePriv)
g.Assert(err == nil).IsTrue()
object, _ := jose.ParseEncrypted(ciphertext)
privKey, _ := decodePrivateKey(fakePriv)
plaintext, _ := object.Decrypt(privKey)
g.Assert(string(plaintext)).Equal("top_secret")
})
})
}
var fakePriv = `
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA71FaA+otDak2rXF/4h69Tz+OxS6NOWaOc/n7dinHXnlo3Toy
ZzvwweJGQKIOfPNBMncz+8h6oLOByFvb95Z1UEM0d+KCFCCutOeN9NNMw4fkUtSZ
7sm6T35wQUkDOiO1YAGy27hQfT7iryhPwA8KmgZmt7toNNf+WymPR8DMwAAYeqHA
5DIEWWsg+RLohOJ0itIk9q6Us9WYhng0sZ9+U+C87FospjKRMyAinSvKx0Uan4ap
YGbLjDQHimWtimfT4XWCGTO1cWno378Vm/newUN6WVaeZ2CSHcWgD2fWcjFixX2A
SvcvfuCo7yZPUPWeiYKrc5d1CC3ncocu43LhSQIDAQABAoIBAQDIbYKM+sfmxAwF
8KOg1gvIXjuNCrK+GxU9LmSajtzpU5cuiHoEGaBGUOJzaQXnQbcds9W2ji2dfxk3
my87SShRIyfDK9GzV7fZzIAIRhrpO1tOv713zj0aLJOJKcPpIlTZ5jJMcC4A5vTk
q0c3W6GOY8QNJohckXT2FnVoK6GPPiaZnavkwH33cJk0j1vMsbADdKF7Jdfq9FBF
Lx+Za7wo79MQIr68KEqsqMpmrawIf1T3TqOCNbkPCL2tu5EfoyGIItrH33SBOV/B
HbIfe4nJYZMWXhe3kZ/xCFqiRx6/wlc5pGCwCicgHJJe/l8Y9OticDCCyJDQtD8I
6927/j2NAoGBAPNRRY8r5ES5f8ftEktcLwh2zw08PNkcolTeqsEMbWAQspV/v+Ay
4niEXIN3ix2yTnMgrtxRGO7zdPnMaTN8E88FsSDKQ97lm7m3jo7lZtDMz16UxGmd
AOOuXwUtpngz7OrQ25NXhvFYLTgLoPsv3PbFbF1pwbhZqPTttTdg5so3AoGBAPvK
ta/n7DMZd/HptrkdkxxHaGN19ZjBVIqyeORhIDznEYjv9Z90JvzRxCmUriD4fyJC
/XSTytORa34UgmOk1XFtxWusXhnYqCTIHG/MKCy9D4ifzFzii9y/M+EnQIMb658l
+edLyrGFla+t5NS1XAqDYjfqpUFbMvU1kVoDJ/B/AoGBANBQe3o5PMSuAD19tdT5
Rnc7qMcPFJVZE44P2SdQaW/+u7aM2gyr5AMEZ2RS+7LgDpQ4nhyX/f3OSA75t/PR
PfBXUi/dm8AA2pNlGNM0ihMn1j6GpaY6OiG0DzwSulxdMHBVgjgijrCgKo66Pgfw
EYDgw4cyXR1k/ec8gJK6Dr1/AoGBANvmSY77Kdnm4E4yIxbAsX39DznuBzQFhGQt
Qk+SU6lc1H+Xshg0ROh/+qWl5/17iOzPPLPXb0getJZEKywDBTYu/D/xJa3E/fRB
oDQzRNLtuudDSCPG5wc/JXv53+mhNMKlU/+gvcEUPYpUgIkUavHzlI/pKbJOh86H
ng3Su8rZAn9w/zkoJu+n7sHta/Hp6zPTbvjZ1EijZp0+RygBgiv9UjDZ6D9EGcjR
ZiFwuc8I0g7+GRkgG2NbfqX5Cewb/nbJQpHPO31bqJrcLzU0KurYAwQVx6WGW0He
ERIlTeOMxVo6M0OpI+rH5bOLdLLEVhNtM/4HUFi1Qy6CCMbN2t3H
-----END RSA PRIVATE KEY-----
`