added handlers, rest, angular skeleton

This commit is contained in:
Brad Rydzewski 2015-04-08 15:43:59 -07:00
parent 09bd7cf71a
commit 9298f16155
37 changed files with 2625 additions and 4 deletions

View file

@ -83,3 +83,26 @@ func (db *DB) DeleteRepo(repo *common.Repo) error {
// TODO(bradrydzewski) delete all tasks
return t.Commit()
}
// GetSubscriber gets the subscriber by login for the
// named repository.
func (db *DB) GetSubscriber(repo string, login string) (*common.Subscriber, error) {
sub := &common.Subscriber{}
key := []byte(login + "/" + repo)
err := get(db, bucketUserRepos, key, sub)
return sub, err
}
// InsertSubscriber inserts a subscriber for the named
// repository.
func (db *DB) InsertSubscriber(repo string, sub *common.Subscriber) error {
key := []byte(sub.Login + "/" + repo)
return insert(db, bucketUserRepos, key, sub)
}
// DeleteSubscriber removes the subscriber by login for the
// named repository.
func (db *DB) DeleteSubscriber(repo string, sub *common.Subscriber) error {
key := []byte(sub.Login + "/" + repo)
return delete(db, bucketUserRepos, key)
}

View file

@ -42,6 +42,18 @@ type Datastore interface {
// DeleteUser deletes the token.
DeleteToken(*common.Token) error
// GetSubscriber gets the subscriber by login for the
// named repository.
GetSubscriber(string, string) (*common.Subscriber, error)
// InsertSubscriber inserts a subscriber for the named
// repository.
InsertSubscriber(string, *common.Subscriber) error
// DeleteSubscriber removes the subscriber by login for the
// named repository.
DeleteSubscriber(string, *common.Subscriber) error
// GetRepo gets the repository by name.
GetRepo(string) (*common.Repo, error)
@ -125,7 +137,3 @@ type Datastore interface {
// named repository and build number.
UpsertTaskLogs(string, int, int, []byte) error
}
// GetSubscriber(string, string) (*common.Subscriber, error)
// InsertSubscriber(string, *common.Subscriber) error
// DeleteSubscriber(string, string) error

75
server/badge.go Normal file
View file

@ -0,0 +1,75 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/drone/drone/common"
"github.com/drone/drone/common/ccmenu"
"github.com/drone/drone/common/httputil"
)
var (
badgeSuccess = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="91" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="91" height="20" fill="#555"/><rect rx="3" x="37" width="54" height="20" fill="#4c1"/><path fill="#4c1" d="M37 0h4v20h-4z"/><rect rx="3" width="91" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="63" y="15" fill="#010101" fill-opacity=".3">success</text><text x="63" y="14">success</text></g></svg>`)
badgeFailure = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="83" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="83" height="20" fill="#555"/><rect rx="3" x="37" width="46" height="20" fill="#e05d44"/><path fill="#e05d44" d="M37 0h4v20h-4z"/><rect rx="3" width="83" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="59" y="15" fill="#010101" fill-opacity=".3">failure</text><text x="59" y="14">failure</text></g></svg>`)
badgeStarted = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="87" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="87" height="20" fill="#555"/><rect rx="3" x="37" width="50" height="20" fill="#dfb317"/><path fill="#dfb317" d="M37 0h4v20h-4z"/><rect rx="3" width="87" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="61" y="15" fill="#010101" fill-opacity=".3">started</text><text x="61" y="14">started</text></g></svg>`)
badgeError = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="76" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="76" height="20" fill="#555"/><rect rx="3" x="37" width="39" height="20" fill="#9f9f9f"/><path fill="#9f9f9f" d="M37 0h4v20h-4z"/><rect rx="3" width="76" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="55.5" y="15" fill="#010101" fill-opacity=".3">error</text><text x="55.5" y="14">error</text></g></svg>`)
badgeNone = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="75" height="20"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><rect rx="3" width="75" height="20" fill="#555"/><rect rx="3" x="37" width="38" height="20" fill="#9f9f9f"/><path fill="#9f9f9f" d="M37 0h4v20h-4z"/><rect rx="3" width="75" height="20" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="19.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="14">build</text><text x="55" y="15" fill="#010101" fill-opacity=".3">none</text><text x="55" y="14">none</text></g></svg>`)
)
// GetBadge accepts a request to retrieve the named
// repo and branhes latest build details from the datastore
// and return an SVG badges representing the build results.
//
// GET /api/badge/:owner/:name/status.svg
//
func GetBadge(c *gin.Context) {
var repo = ToRepo(c)
// an SVG response is always served, even when error, so
// we can go ahead and set the content type appropriately.
c.Writer.Header().Set("Content-Type", "image/svg+xml")
// if no commit was found then display
// the 'none' badge, instead of throwing
// an error response
if repo.Last == nil {
c.Writer.Write(badgeNone)
return
}
switch repo.Last.State {
case common.StateSuccess:
c.Writer.Write(badgeSuccess)
case common.StateFailure:
c.Writer.Write(badgeFailure)
case common.StateError, common.StateKilled:
c.Writer.Write(badgeError)
case common.StatePending, common.StateRunning:
c.Writer.Write(badgeStarted)
default:
c.Writer.Write(badgeNone)
}
}
// GetCC accepts a request to retrieve the latest build
// status for the given repository from the datastore and
// in CCTray XML format.
//
// GET /api/badge/:host/:owner/:name/cc.xml
//
// TODO(bradrydzewski) this will not return in-progress builds, which it should
func GetCC(c *gin.Context) {
ds := ToDatastore(c)
repo := ToRepo(c)
last, err := ds.GetBuildLast(repo.FullName)
if err != nil {
c.Fail(404, err)
return
}
link := httputil.GetURL(c.Request) + "/" + repo.FullName
cc := ccmenu.NewCC(repo, last, link)
c.Writer.Header().Set("Content-Type", "application/xml")
c.XML(200, cc)
}

45
server/builds.go Normal file
View file

@ -0,0 +1,45 @@
package server
import (
"strconv"
"github.com/gin-gonic/gin"
)
// GetBuild accepts a request to retrieve a build
// from the datastore for the given repository and
// build number.
//
// GET /api/builds/:owner/:name/:number
//
func GetBuild(c *gin.Context) {
ds := ToDatastore(c)
repo := ToRepo(c)
num, err := strconv.Atoi(c.Params.ByName("number"))
if err != nil {
c.Fail(400, err)
return
}
build, err := ds.GetBuild(repo.FullName, num)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, build)
}
}
// GetBuild accepts a request to retrieve a list
// of builds from the datastore for the given repository.
//
// GET /api/builds/:owner/:name
//
func GetBuilds(c *gin.Context) {
ds := ToDatastore(c)
repo := ToRepo(c)
builds, err := ds.GetBuildList(repo.FullName)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, builds)
}
}

90
server/hooks.go Normal file
View file

@ -0,0 +1,90 @@
package server
import (
"strings"
"github.com/drone/drone/common"
// "github.com/bradrydzewski/drone/worker"
"github.com/gin-gonic/gin"
)
// PostHook accepts a post-commit hook and parses the payload
// in order to trigger a build.
//
// GET /api/hook
//
func PostHook(c *gin.Context) {
remote := ToRemote(c)
store := ToDatastore(c)
hook, err := remote.Hook(c.Request)
if err != nil {
c.Fail(400, err)
return
}
if hook == nil {
c.Writer.WriteHeader(200)
return
}
if hook.Repo == nil {
c.Writer.WriteHeader(400)
return
}
// a build may be skipped if the text [CI SKIP]
// is found inside the commit message
if hook.Commit != nil && strings.Contains(hook.Commit.Message, "[CI SKIP]") {
c.Writer.WriteHeader(204)
return
}
repo, err := store.GetRepo(hook.Repo.FullName)
if err != nil {
c.Fail(404, err)
return
}
if repo.Disabled || repo.User == nil || (repo.DisablePR && hook.PullRequest != nil) {
c.Writer.WriteHeader(204)
return
}
user, err := store.GetUser(repo.User.Login)
if err != nil {
c.Fail(500, err)
return
}
build := &common.Build{}
build.State = common.StatePending
build.Commit = hook.Commit
build.PullRequest = hook.PullRequest
// featch the .drone.yml file from the database
_, err = remote.Script(user, repo, build)
if err != nil {
c.Fail(404, err)
return
}
err = store.InsertBuild(repo.FullName, build)
if err != nil {
c.Fail(500, err)
return
}
// w := worker.Work{
// User: user,
// Repo: repo,
// Build: build,
// }
// verify the branches can be built vs skipped
// s, _ := script.ParseBuild(string(yml))
// if len(hook.PullRequest) == 0 && !s.MatchBranch(hook.Branch) {
// w.WriteHeader(http.StatusOK)
// return
// }
c.JSON(200, build)
}

181
server/login.go Normal file
View file

@ -0,0 +1,181 @@
package server
import (
"fmt"
"log"
"strings"
"github.com/gin-gonic/gin"
"github.com/drone/drone/common"
"github.com/drone/drone/common/gravatar"
"github.com/drone/drone/common/httputil"
"github.com/drone/drone/common/oauth2"
)
// GetLogin accepts a request to authorize the user and to
// return a valid OAuth2 access token. The access token is
// returned as url segment #access_token
//
// GET /authorize
//
func GetLogin(c *gin.Context) {
settings := ToSettings(c)
session := ToSession(c)
store := ToDatastore(c)
// when dealing with redirects we may need
// to adjust the content type. I cannot, however,
// rememver why, so need to revisit this line.
c.Writer.Header().Del("Content-Type")
// depending on the configuration a user may
// authenticate with OAuth1, OAuth2 or Basic
// Auth (username and password). This will delegate
// authorization accordingly.
switch {
case settings.Service.OAuth == nil:
getLoginBasic(c)
case settings.Service.OAuth.RequestToken != "":
getLoginOauth1(c)
default:
getLoginOauth2(c)
}
// exit if authorization fails
// TODO(bradrydzewski) return an error message instead
if c.Writer.Status() != 200 {
return
}
// get the user from the database
login := ToUser(c)
u, err := store.GetUser(login.Login)
if err != nil {
// if self-registration is disabled we should
// return a notAuthorized error. the only exception
// is if no users exist yet in the system we'll proceed.
if !settings.Service.Open {
count, err := store.GetUserCount()
if err != nil || count != 0 {
c.String(400, "Unable to create account. Registration is closed")
return
}
}
// create the user account
u = &common.User{}
u.Login = login.Login
u.Token = login.Token
u.Secret = login.Secret
u.Name = login.Name
u.Email = login.Email
u.Gravatar = gravatar.Generate(u.Email)
// insert the user into the database
if err := store.InsertUser(u); err != nil {
log.Println(err)
c.Fail(400, err)
return
}
// // if this is the first user, they
// // should be an admin.
//if u.ID == 1 {
if u.Login == "bradrydzewski" {
u.Admin = true
}
}
// update the user meta data and authorization
// data and cache in the datastore.
u.Token = login.Token
u.Secret = login.Secret
u.Name = login.Name
u.Email = login.Email
u.Gravatar = gravatar.Generate(u.Email)
if err := store.UpdateUser(u); err != nil {
log.Println(err)
c.Fail(400, err)
return
}
token, err := session.GenerateToken(c.Request, u)
if err != nil {
log.Println(err)
c.Fail(400, err)
return
}
c.Redirect(303, "/#access_token="+token)
}
// getLoginOauth2 is the default authorization implementation
// using the oauth2 protocol.
func getLoginOauth2(c *gin.Context) {
var settings = ToSettings(c)
var remote = ToRemote(c)
var config = &oauth2.Config{
ClientId: settings.Service.OAuth.Client,
ClientSecret: settings.Service.OAuth.Secret,
Scope: strings.Join(settings.Service.OAuth.Scope, ","),
AuthURL: settings.Service.OAuth.Authorize,
TokenURL: settings.Service.OAuth.AccessToken,
RedirectURL: fmt.Sprintf("%s/authorize", httputil.GetURL(c.Request)),
//settings.Server.Scheme, settings.Server.Hostname),
}
// get the OAuth code
var code = c.Request.FormValue("code")
//var state = c.Request.FormValue("state")
if len(code) == 0 {
c.Redirect(303, config.AuthCodeURL("random"))
return
}
// exhange for a token
var trans = &oauth2.Transport{Config: config}
var token, err = trans.Exchange(code)
if err != nil {
c.Fail(400, err)
return
}
// get user account
user, err := remote.Login(token.AccessToken, token.RefreshToken)
if err != nil {
c.Fail(404, err)
return
}
// add the user to the request
c.Set("user", user)
}
// getLoginOauth1 is able to authorize a user with the oauth1
// authentication protocol. This is used primarily with Bitbucket
// and Stash only, and one day I hope can be removed.
func getLoginOauth1(c *gin.Context) {
}
// getLoginBasic is able to authorize a user with a username and
// password. This can be used for systems that do not support oauth.
func getLoginBasic(c *gin.Context) {
var (
remote = ToRemote(c)
username = c.Request.FormValue("username")
password = c.Request.FormValue("username")
)
// get user account
user, err := remote.Login(username, password)
if err != nil {
c.Fail(404, err)
return
}
// add the user to the request
c.Set("user", user)
}

27
server/logs.go Normal file
View file

@ -0,0 +1,27 @@
package server
import (
"strconv"
"github.com/gin-gonic/gin"
)
// GetLogs accepts a request to retrieve logs from the
// datastore for the given repository, build and task
// number.
//
// GET /api/logs/:owner/:name/:number/:task
//
func GetLogs(c *gin.Context) {
ds := ToDatastore(c)
repo := ToRepo(c)
build, _ := strconv.Atoi(c.Params.ByName("number"))
task, _ := strconv.Atoi(c.Params.ByName("task"))
logs, err := ds.GetTaskLogs(repo.FullName, build, task)
if err != nil {
c.Fail(404, err)
} else {
c.Writer.Write(logs)
}
}

242
server/repos.go Normal file
View file

@ -0,0 +1,242 @@
package server
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/drone/drone/common"
"github.com/drone/drone/common/httputil"
"github.com/drone/drone/common/sshutil"
"github.com/drone/drone/remote"
)
// repoResp is a data structure used for sending
// repository data to the client, augmented with
// additional repository meta-data.
type repoResp struct {
*common.Repo
Perms *common.Perm `json:"permissions,omitempty"`
Watch *common.Subscriber `json:"subscription,omitempty"`
Params map[string]string `json:"params,omitempty"`
}
// repoReq is a data structure used for receiving
// repository data from the client to modify the
// attributes of an existing repository.
//
// note that attributes are pointers so that we can
// accept null values, effectively patching an existing
// repository object with only the supplied fields.
type repoReq struct {
Disabled *bool `json:"disabled"`
DisablePR *bool `json:"disable_prs"`
DisableTag *bool `json:"disable_tags"`
Trusted *bool `json:"privileged"`
Timeout *int64 `json:"timeout"`
// optional private parameters can only be
// supplied by the repository admin.
Params *map[string]string `json:"params"`
}
// GetRepo accepts a request to retrieve a commit
// from the datastore for the given repository, branch and
// commit hash.
//
// GET /api/repos/:owner/:name
//
func GetRepo(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
user := ToUser(c)
perm := ToPerm(c)
data := repoResp{repo, perm, nil, nil}
// if the user is an administrator of the project
// we should display the private parameter data.
if perm.Admin {
data.Params, _ = store.GetRepoParams(repo.FullName)
}
// if the user is authenticated, we should display
// if she is watching the current repository.
if user != nil {
data.Watch, _ = store.GetSubscriber(repo.FullName, user.Login)
}
c.JSON(200, data)
}
// PutRepo accapets a request to update the named repository
// in the datastore. It expects a JSON input and returns the
// updated repository in JSON format if successful.
//
// PUT /api/repos/:owner/:name
//
func PutRepo(c *gin.Context) {
store := ToDatastore(c)
perm := ToPerm(c)
u := ToUser(c)
r := ToRepo(c)
in := &repoReq{}
if !c.BindWith(in, binding.JSON) {
return
}
if in.Params != nil {
err := store.UpsertRepoParams(r.FullName, *in.Params)
if err != nil {
c.Fail(400, err)
return
}
}
if in.Disabled != nil {
r.Disabled = *in.Disabled
}
if in.DisablePR != nil {
r.DisablePR = *in.DisablePR
}
if in.DisableTag != nil {
r.DisableTag = *in.DisableTag
}
if in.Trusted != nil && u.Admin {
r.Trusted = *in.Trusted
}
if in.Timeout != nil && u.Admin {
r.Timeout = *in.Timeout
}
err := store.UpdateRepo(r)
if err != nil {
c.Fail(400, err)
return
}
data := repoResp{r, perm, nil, nil}
data.Params, _ = store.GetRepoParams(r.FullName)
data.Watch, _ = store.GetSubscriber(r.FullName, u.Login)
c.JSON(200, data)
}
// DeleteRepo accepts a request to delete the named
// repository.
//
// DEL /api/repos/:owner/:name
//
func DeleteRepo(c *gin.Context) {
ds := ToDatastore(c)
u := ToUser(c)
r := ToRepo(c)
link := fmt.Sprintf(
"%s/api/hook",
httputil.GetURL(c.Request),
)
remote := ToRemote(c)
err := remote.Deactivate(u, r, link)
if err != nil {
c.Fail(400, err)
}
err = ds.DeleteRepo(r)
if err != nil {
c.Fail(400, err)
}
c.Writer.WriteHeader(200)
}
// PostRepo accapets a request to activate the named repository
// in the datastore. It returns a 201 status created if successful
//
// POST /api/repos/:owner/:name
//
func PostRepo(c *gin.Context) {
user := ToUser(c)
store := ToDatastore(c)
owner := c.Params.ByName("owner")
name := c.Params.ByName("name")
link := fmt.Sprintf(
"%s/api/hook",
httputil.GetURL(c.Request),
)
// TODO(bradrydzewski) verify repo not exists
// get the repository and user permissions
// from the remote system.
remote := ToRemote(c)
r, err := remote.Repo(user, owner, name)
if err != nil {
c.Fail(400, err)
}
m, err := remote.Perm(user, owner, name)
if err != nil {
c.Fail(400, err)
return
}
if !m.Admin {
c.Fail(403, fmt.Errorf("must be repository admin"))
return
}
// set the repository owner to the
// currently authenticated user.
r.User = user
// generate an RSA key and add to the repo
key, err := sshutil.GeneratePrivateKey()
if err != nil {
c.Fail(400, err)
return
}
keypair := &common.Keypair{}
keypair.Public = sshutil.MarshalPublicKey(&key.PublicKey)
keypair.Private = sshutil.MarshalPrivateKey(key)
err = store.UpsertRepoKeys(r.FullName, keypair)
if err != nil {
c.Fail(500, err)
return
}
// store the repository and the users' permissions
// in the datastore.
err = store.InsertRepo(user, r)
if err != nil {
c.Fail(500, err)
return
}
err = store.InsertSubscriber(r.FullName, &common.Subscriber{Subscribed: true})
if err != nil {
c.Fail(500, err)
return
}
err = remote.Activate(user, r, keypair, link)
if err != nil {
c.Fail(500, err)
return
}
c.JSON(200, r)
}
// perms is a helper function that returns user permissions
// for a particular repository.
func perms(remote remote.Remote, u *common.User, r *common.Repo) *common.Perm {
switch {
case u == nil && r.Private:
return &common.Perm{}
case u == nil && r.Private == false:
return &common.Perm{Pull: true}
case u.Admin:
return &common.Perm{Pull: true, Push: true, Admin: true}
}
p, err := remote.Perm(u, r.Owner, r.Name)
if err != nil {
return &common.Perm{}
}
return p
}

241
server/server.go Normal file
View file

@ -0,0 +1,241 @@
package server
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/drone/drone/common"
"github.com/drone/drone/datastore"
"github.com/drone/drone/eventbus"
"github.com/drone/drone/remote"
"github.com/drone/drone/server/session"
"github.com/drone/drone/settings"
)
func SetBus(r eventbus.Bus) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("eventbus", r)
c.Next()
}
}
func ToBus(c *gin.Context) eventbus.Bus {
v, err := c.Get("eventbus")
if err != nil {
return nil
}
return v.(eventbus.Bus)
}
func ToRemote(c *gin.Context) remote.Remote {
v, err := c.Get("remote")
if err != nil {
return nil
}
return v.(remote.Remote)
}
func SetRemote(r remote.Remote) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("remote", r)
c.Next()
}
}
func ToSettings(c *gin.Context) *settings.Settings {
v, err := c.Get("settings")
if err != nil {
return nil
}
return v.(*settings.Settings)
}
func SetSettings(s *settings.Settings) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("settings", s)
c.Next()
}
}
func ToPerm(c *gin.Context) *common.Perm {
v, err := c.Get("perm")
if err != nil {
return nil
}
return v.(*common.Perm)
}
func ToUser(c *gin.Context) *common.User {
v, err := c.Get("user")
if err != nil {
return nil
}
return v.(*common.User)
}
func ToRepo(c *gin.Context) *common.Repo {
v, err := c.Get("repo")
if err != nil {
return nil
}
return v.(*common.Repo)
}
func ToDatastore(c *gin.Context) datastore.Datastore {
return c.MustGet("datastore").(datastore.Datastore)
}
func ToSession(c *gin.Context) session.Session {
return c.MustGet("session").(session.Session)
}
func SetDatastore(ds datastore.Datastore) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("datastore", ds)
c.Next()
}
}
func SetSession(s session.Session) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("session", s)
c.Next()
}
}
func SetUser(s session.Session) gin.HandlerFunc {
return func(c *gin.Context) {
ds := ToDatastore(c)
login := s.GetLogin(c.Request)
if len(login) == 0 {
c.Next()
return
}
u, err := ds.GetUser(login)
if err == nil {
c.Set("user", u)
}
}
}
func SetRepo() gin.HandlerFunc {
return func(c *gin.Context) {
ds := ToDatastore(c)
u := ToUser(c)
owner := c.Params.ByName("owner")
name := c.Params.ByName("name")
r, err := ds.GetRepo(owner + "/" + name)
switch {
case err != nil && u != nil:
c.Fail(401, err)
return
case err != nil && u == nil:
c.Fail(404, err)
return
}
c.Set("repo", r)
c.Next()
}
}
func SetPerm() gin.HandlerFunc {
return func(c *gin.Context) {
remote := ToRemote(c)
user := ToUser(c)
repo := ToRepo(c)
perm := perms(remote, user, repo)
c.Set("perm", perm)
c.Next()
}
}
func MustUser() gin.HandlerFunc {
return func(c *gin.Context) {
u := ToUser(c)
if u == nil {
c.AbortWithStatus(401)
} else {
c.Set("user", u)
c.Next()
}
}
}
func MustAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
u := ToUser(c)
if u == nil {
c.AbortWithStatus(401)
} else if !u.Admin {
c.AbortWithStatus(403)
} else {
c.Set("user", u)
c.Next()
}
}
}
func CheckPull() gin.HandlerFunc {
return func(c *gin.Context) {
u := ToUser(c)
m := ToPerm(c)
switch {
case u == nil && m == nil:
c.AbortWithStatus(401)
case u == nil && m.Pull == false:
c.AbortWithStatus(401)
case u != nil && m.Pull == false:
c.AbortWithStatus(404)
default:
c.Next()
}
}
}
func CheckPush() gin.HandlerFunc {
return func(c *gin.Context) {
switch c.Request.Method {
case "GET", "OPTIONS":
c.Next()
return
}
u := ToUser(c)
m := ToPerm(c)
switch {
case u == nil && m.Push == false:
c.AbortWithStatus(401)
case u != nil && m.Push == false:
c.AbortWithStatus(404)
default:
c.Next()
}
}
}
func SetHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Add("Access-Control-Allow-Origin", "*")
c.Writer.Header().Add("X-Frame-Options", "DENY")
c.Writer.Header().Add("X-Content-Type-Options", "nosniff")
c.Writer.Header().Add("X-XSS-Protection", "1; mode=block")
c.Writer.Header().Add("Cache-Control", "no-cache")
c.Writer.Header().Add("Cache-Control", "no-store")
c.Writer.Header().Add("Cache-Control", "max-age=0")
c.Writer.Header().Add("Cache-Control", "must-revalidate")
c.Writer.Header().Add("Cache-Control", "value")
c.Writer.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
c.Writer.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
if c.Request.TLS != nil {
c.Writer.Header().Add("Strict-Transport-Security", "max-age=31536000")
}
c.Next()
}
}

109
server/session/session.go Normal file
View file

@ -0,0 +1,109 @@
package session
import (
"fmt"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/drone/drone/common"
"github.com/drone/drone/common/httputil"
"github.com/drone/drone/settings"
"github.com/gorilla/securecookie"
)
type Session interface {
GenerateToken(*http.Request, *common.User) (string, error)
GetLogin(*http.Request) string
}
type session struct {
secret []byte
expire time.Duration
}
func New(s *settings.Session) Session {
secret := securecookie.GenerateRandomKey(32)
expire := time.Hour * 72
return &session{
secret: secret,
expire: expire,
}
}
// GenerateToken generates a JWT token for the user session
// that can be appended to the #access_token segment to
// facilitate client-based OAuth2.
func (s *session) GenerateToken(r *http.Request, user *common.User) (string, error) {
token := jwt.New(jwt.GetSigningMethod("HS256"))
token.Claims["user_id"] = user.Login
token.Claims["audience"] = httputil.GetURL(r)
token.Claims["expires"] = time.Now().UTC().Add(s.expire).Unix()
return token.SignedString(s.secret)
}
// GetLogin gets the currently authenticated user for the
// http.Request. The user details will be stored as either
// a simple API token or JWT bearer token.
func (s *session) GetLogin(r *http.Request) (_ string) {
token := getToken(r)
if len(token) == 0 {
return
}
claims := getClaims(token, s.secret)
if claims == nil || claims["user_id"] == nil {
return
}
userid, ok := claims["user_id"].(string)
if !ok {
return
}
// tokenid, ok := claims["token_id"].(string)
// if ok {
// _, err := datastore.GetToken(c, int64(tokenid))
// if err != nil {
// return nil
// }
// }
return userid
}
// getToken is a helper function that extracts the token
// from the http.Request.
func getToken(r *http.Request) string {
token := getTokenHeader(r)
if len(token) == 0 {
token = getTokenParam(r)
}
return token
}
// getTokenHeader parses the JWT token value from
// the http Authorization header.
func getTokenHeader(r *http.Request) string {
var tokenstr = r.Header.Get("Authorization")
fmt.Sscanf(tokenstr, "Bearer %s", &tokenstr)
return tokenstr
}
// getTokenParam parses the JWT token value from
// the http Request's query parameter.
func getTokenParam(r *http.Request) string {
return r.FormValue("access_token")
}
// getClaims is a helper function that extracts the token
// claims from the JWT token string.
func getClaims(token string, secret []byte) map[string]interface{} {
t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
return secret, nil
})
if err != nil || !t.Valid {
return nil
}
return t.Claims
}

39
server/static/index.html Normal file
View file

@ -0,0 +1,39 @@
<!doctype html>
<html ng-app="drone" lang="en">
<head>
<base href="/">
<meta charset="utf-8">
<meta name="author" content="Brad Rydzewski and the Drone Authors">
<meta name="viewport" content="width=device-width, user-scalable=no">
<link rel="shortcut icon" href="/favicon.ico"/>
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,300,700" rel="stylesheet" type="text/css" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/octicons/2.1.2/octicons.min.css" rel="stylesheet" type="text/css" />
</head>
<body ng-cloak>
<main role="main" ng-view></main>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.8/angular.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.8/angular-route.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.8/angular-resource.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui/0.4.0/angular-ui.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.6.0/moment.min.js"></script>
<!-- main javascript application -->
<script src="/static/scripts/drone.js"></script>
<script src="/static/scripts/controllers/repos.js"></script>
<script src="/static/scripts/controllers/builds.js"></script>
<script src="/static/scripts/controllers/users.js"></script>
<script src="/static/scripts/services/repos.js"></script>
<script src="/static/scripts/services/builds.js"></script>
<script src="/static/scripts/services/tasks.js"></script>
<script src="/static/scripts/services/users.js"></script>
<script src="/static/scripts/services/logs.js"></script>
<script src="/static/scripts/filters/filter.js"></script>
<script src="/static/scripts/filters/gravatar.js"></script>
<script src="/static/scripts/filters/time.js"></script>
</body>
</html>

View file

@ -0,0 +1,101 @@
(function () {
/**
* BuildsCtrl responsible for rendering the repo's
* recent build history.
*/
function BuildsCtrl($scope, $routeParams, builds, repos, users) {
var owner = $routeParams.owner;
var name = $routeParams.name;
var fullName = owner+'/'+name;
// Gets the currently authenticated user
users.getCached().then(function(payload){
$scope.user = payload.data;
});
// Gets a repository
repos.get(fullName).then(function(payload){
$scope.repo = payload.data;
}).catch(function(err){
$scope.error = err;
});
// Gets a list of builds
builds.list(fullName).then(function(payload){
$scope.builds = angular.isArray(payload.data) ? payload.data : [];
}).catch(function(err){
$scope.error = err;
});
$scope.watch = function(repo) {
repos.watch(repo.full_name).then(function(payload) {
$scope.repo.subscription = payload.data;
});
}
$scope.unwatch = function(repo) {
repos.unwatch(repo.full_name).then(function() {
delete $scope.repo.subscription;
});
}
}
/**
* BuildCtrl responsible for rendering a build.
*/
function BuildCtrl($scope, $routeParams, logs, tasks, builds, repos, users) {
var step = parseInt($routeParams.step) || 1;
var number = $routeParams.number;
var owner = $routeParams.owner;
var name = $routeParams.name;
var fullName = owner+'/'+name;
// Gets the currently authenticated user
users.getCached().then(function(payload){
$scope.user = payload.data;
});
// Gets a repository
repos.get(fullName).then(function(payload){
$scope.repo = payload.data;
}).catch(function(err){
$scope.error = err;
});
// Gets the build
builds.get(fullName, number).then(function(payload){
$scope.build = payload.data;
}).catch(function(err){
$scope.error = err;
});
// Gets a list of build steps
tasks.list(fullName, number).then(function(payload){
$scope.tasks = payload.data || [];
$scope.tasks.forEach(function(task) {
if (task.number === step) {
$scope.task = task;
}
});
}).catch(function(err){
$scope.error = err;
});
if (step) {
// Gets a list of build steps
logs.get(fullName, number, step).then(function(payload){
$scope.logs = payload.data;
}).catch(function(err){
$scope.error = err;
});
}
}
angular
.module('drone')
.controller('BuildCtrl', BuildCtrl)
.controller('BuildsCtrl', BuildsCtrl);
})();

View file

@ -0,0 +1,92 @@
(function () {
/**
* ReposCtrl responsible for rendering the user's
* repository home screen.
*/
function ReposCtrl($scope, $routeParams, repos, users) {
// Gets the currently authenticated user
users.getCached().then(function(payload){
$scope.user = payload.data;
});
// Gets a list of repos to display in the
// dropdown.
repos.list().then(function(payload){
$scope.repos = angular.isArray(payload.data) ? payload.data : [];
}).catch(function(err){
$scope.error = err;
});
}
/**
* RepoAddCtrl responsible for activaing a new
* repository.
*/
function RepoAddCtrl($scope, $location, repos, users) {
$scope.add = function(slug) {
repos.post(slug).then(function(payload) {
$location.path('/'+slug);
}).catch(function(err){
$scope.error = err;
});
}
}
/**
* RepoEditCtrl responsible for editing a repository.
*/
function RepoEditCtrl($scope, $location, $routeParams, repos, users) {
var owner = $routeParams.owner;
var name = $routeParams.name;
var fullName = owner+'/'+name;
// Gets the currently authenticated user
users.getCached().then(function(payload){
$scope.user = payload.data;
});
// Gets a repository
repos.get(fullName).then(function(payload){
$scope.repo = payload.data;
}).catch(function(err){
$scope.error = err;
});
$scope.save = function(repo) {
repos.update(repo).then(function(payload) {
$scope.repo = payload.data;
}).catch(function(err){
$scope.error = err;
});
}
$scope.delete = function(repo) {
repos.delete(repo).then(function(payload) {
$location.path('/');
}).catch(function(err){
$scope.error = err;
});
}
$scope.param={}
$scope.addParam = function(param) {
if (!$scope.repo.params) {
$scope.repo.params = {}
}
$scope.repo.params[param.key]=param.value;
$scope.param={}
}
$scope.deleteParam = function(key) {
delete $scope.repo.params[key];
}
}
angular
.module('drone')
.controller('ReposCtrl', ReposCtrl)
.controller('RepoAddCtrl', RepoAddCtrl)
.controller('RepoEditCtrl', RepoEditCtrl);
})();

View file

@ -0,0 +1,54 @@
(function () {
/**
* UserCtrl is responsible for managing user settings.
*/
function UserCtrl($scope, users) {
// Gets the currently authenticated user
users.getCurrent().then(function(payload){
$scope.user = payload.data;
});
}
/**
* UsersCtrl is responsible for managing user accounts.
* This part of the site is for administrators only.
*/
function UsersCtrl($scope, users) {
// Gets the currently authenticated user
users.getCached().then(function(payload){
$scope.user = payload.data;
});
users.list().then(function(payload){
$scope.users = payload.data;
});
$scope.login="";
$scope.add = function(login) {
users.post(login).then(function(payload){
$scope.users.push(payload.data);
$scope.login="";
});
}
$scope.toggle = function(user) {
user.admin = !user.admin;
users.put(user);
}
$scope.remove = function(user) {
users.delete(user).then(function(){
users.list().then(function(payload){
$scope.users = payload.data;
});
});
}
}
angular
.module('drone')
.controller('UserCtrl', UserCtrl)
.controller('UsersCtrl', UsersCtrl);
})();

View file

@ -0,0 +1,140 @@
'use strict';
(function () {
/**
* Creates the angular application.
*/
angular.module('drone', [
'ngRoute',
'ui.filters'
]);
/**
* Bootstraps the application and retrieves the
* token from the
*/
function Authorize() {
// First, parse the query string
var params = {}, queryString = location.hash.substring(1),
regex = /([^&=]+)=([^&]*)/g, m;
// Loop through and retrieve the token
while (m = regex.exec(queryString)) {
params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
// if the user has just received an auth token we
// should extract from the URL, save to local storage
// and then remove from the URL for good measure.
if (params.access_token) {
localStorage.setItem("access_token", params.access_token);
history.replaceState({}, document.title, location.pathname);
}
}
/**
* Defines the route configuration for the
* main application.
*/
function Config ($routeProvider, $httpProvider, $locationProvider) {
// Resolver that will attempt to load the currently
// authenticated user prior to loading the page.
var resolveUser = {
user: function(users) {
return users.getCached();
}
}
$routeProvider
.when('/', {
templateUrl: '/static/scripts/views/repos.html',
controller: 'ReposCtrl',
resolve: resolveUser
})
.when('/login', {
templateUrl: '/static/scripts/views/login.html',
})
.when('/profile', {
templateUrl: '/static/scripts/views/user.html',
controller: 'UserCtrl',
resolve: resolveUser
})
.when('/users', {
templateUrl: '/static/scripts/views/users.html',
controller: 'UsersCtrl',
resolve: resolveUser
})
.when('/new', {
templateUrl: '/static/scripts/views/repos_add.html',
controller: 'RepoAddCtrl',
resolve: resolveUser
})
.when('/:owner/:name', {
templateUrl: '/static/scripts/views/builds.html',
controller: 'BuildsCtrl',
resolve: resolveUser
})
.when('/:owner/:name/edit', {
templateUrl: '/static/scripts/views/repos_edit.html',
controller: 'RepoEditCtrl',
resolve: resolveUser
})
.when('/:owner/:name/:number', {
templateUrl: '/static/scripts/views/build.html',
controller: 'BuildCtrl',
resolve: resolveUser
})
.when('/:owner/:name/:number/:step', {
templateUrl: '/static/scripts/views/build.html',
controller: 'BuildCtrl',
resolve: resolveUser
});
// Enables html5 mode
$locationProvider.html5Mode(true)
// Appends the Bearer token to authorize every
// outbound http request.
$httpProvider.defaults.headers.common.Authorization = 'Bearer '+localStorage.getItem('access_token');
// Intercepts every oubput http response and redirects
// the user to the logic screen if the request was rejected.
$httpProvider.interceptors.push(function($q, $location) {
return {
'responseError': function(rejection) {
if (rejection.status === 401) {// && rejection.config.url != "/api/user") {
$location.path('/login');
}
if (rejection.status === 0) {
// this happens when the app is down or
// the browser loses internet connectivity.
}
return $q.reject(rejection);
}
};
});
}
// /**
// *
// */
// function RouteChange($rootScope, stdout, projs) {
// $rootScope.$on('$routeChangeStart', function (event, next) {
// projs.unsubscribe();
// stdout.unsubscribe();
// });
// //$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
// // document.title = current.$$route.title + ' · drone.io';
// //});
// }
angular
.module('drone')
.config(Authorize)
.config(Config);
// .run(RouteChange);
})();

View file

@ -0,0 +1,61 @@
'use strict';
(function () {
/**
* author is a helper function that return the builds
* commit or pull request author.
*/
function author() {
return function(build) {
if (!build) { return ""; }
if (!build.head_commit && !build.pull_request) { return ""; }
if (build.head_commit) { return build.head_commit.author.login || ""; }
return build.pull_request.source.author.login;
}
}
/**
* sha is a helper function that return the builds sha.
*/
function sha() {
return function(build) {
if (!build) { return ""; }
if (!build.head_commit && !build.pull_request) { return ""; }
if (build.head_commit) { return build.head_commit.sha || ""; }
return build.pull_request.source.sha;
}
}
/**
* ref is a helper function that return the builds sha.
*/
function ref() {
return function(build) {
if (!build) { return ""; }
if (!build.head_commit && !build.pull_request) { return ""; }
if (build.head_commit) { return build.head_commit.ref || ""; }
return build.pull_request.source.ref;
}
}
/**
* message is a helper function that return the builds message.
*/
function message() {
return function(build) {
if (!build) { return ""; }
if (!build.head_commit && !build.pull_request) { return ""; }
if (build.head_commit) { return build.head_commit.message || ""; }
return build.pull_request.title || "";
}
}
angular
.module('drone')
.filter('author', author)
.filter('message', message)
.filter('sha', sha)
.filter('ref', ref);
})();

View file

@ -0,0 +1,32 @@
'use strict';
(function () {
/**
* gravatar is a helper function that return the user's gravatar
* image URL given an email hash.
*/
function gravatar() {
return function(hash) {
if (hash === undefined) { return ""; }
return "https://secure.gravatar.com/avatar/"+hash+"?s=48&d=mm";
}
}
/**
* gravatarLarge is a helper function that return the user's gravatar
* image URL given an email hash.
*/
function gravatarLarge() {
return function(hash) {
if (hash === undefined) { return ""; }
return "https://secure.gravatar.com/avatar/"+hash+"?s=128&d=mm";
}
}
angular
.module('drone')
.filter('gravatar', gravatar)
.filter('gravatarLarge', gravatarLarge)
})();

View file

@ -0,0 +1,45 @@
'use strict';
(function () {
/**
* fromNow is a helper function that returns a human readable
* string for the elapsed time between the given unix date and the
* current time (ex. 10 minutes ago).
*/
function fromNow() {
return function(date) {
if (!date) {
return;
}
return moment(new Date(date*1000)).fromNow();
}
}
/**
* toDuration is a helper function that returns a human readable
* string for the given duration in seconds (ex. 1 hour and 20 minutes).
*/
function toDuration() {
return function(seconds) {
return moment.duration(seconds, "seconds").humanize();
}
}
/**
* toDate is a helper function that returns a human readable
* string gor the given unix date.
*/
function toDate() {
return function(date) {
return moment(new Date(date*1000)).format('ll');
}
}
angular
.module('drone')
.filter('fromNow', fromNow)
.filter('toDate', toDate)
.filter('toDuration', toDuration)
})();

View file

@ -0,0 +1,34 @@
'use strict';
(function () {
/**
* The BuildsService provides access to build
* data using REST API calls.
*/
function BuildService($http, $window) {
/**
* Gets a list of builds.
*
* @param {string} Name of the repository.
*/
this.list = function(repoName) {
return $http.get('/api/builds/'+repoName);
};
/**
* Gets a build.
*
* @param {string} Name of the repository.
* @param {number} Number of the build.
*/
this.get = function(repoName, buildNumber) {
return $http.get('/api/builds/'+repoName+'/'+buildNumber);
};
}
angular
.module('drone')
.service('builds', BuildService);
})();

View file

@ -0,0 +1,26 @@
'use strict';
(function () {
/**
* The LogService provides access to build
* log data using REST API calls.
*/
function LogService($http, $window) {
/**
* Gets a task logs.
*
* @param {string} Name of the repository.
* @param {number} Number of the build.
* @param {number} Number of the task.
*/
this.get = function(repoName, number, step) {
return $http.get('/api/logs/'+repoName+'/'+number+'/'+step);
};
}
angular
.module('drone')
.service('logs', LogService);
})();

View file

@ -0,0 +1,80 @@
'use strict';
(function () {
/**
* The RepoService provides access to repository
* data using REST API calls.
*/
function RepoService($http, $window) {
var callback,
websocket,
token = localStorage.getItem('access_token');
/**
* Gets a list of all repositories.
*/
this.list = function() {
return $http.get('/api/user/repos');
};
/**
* Gets a repository by name.
*
* @param {string} Name of the repository.
*/
this.get = function(repoName) {
return $http.get('/api/repos/'+repoName);
};
/**
* Creates a new repository.
*
* @param {object} JSON representation of a repository.
*/
this.post = function(repoName) {
return $http.post('/api/repos/' + repoName);
};
/**
* Updates an existing repository.
*
* @param {object} JSON representation of a repository.
*/
this.update = function(repo) {
return $http.put('/api/repos/'+repo.full_name, repo);
};
/**
* Deletes a repository.
*
* @param {string} Name of the repository.
*/
this.delete = function(repoName) {
return $http.delete('/api/repos/'+repoName);
};
/**
* Watch a repository.
*
* @param {string} Name of the repository.
*/
this.watch = function(repoName) {
return $http.post('/api/subscribers/'+repoName);
};
/**
* Unwatch a repository.
*
* @param {string} Name of the repository.
*/
this.unwatch = function(repoName) {
return $http.delete('/api/subscribers/'+repoName);
};
}
angular
.module('drone')
.service('repos', RepoService);
})();

View file

@ -0,0 +1,47 @@
'use strict';
(function () {
/**
* The TaskService provides access to build
* task data using REST API calls.
*/
function TaskService($http, $window) {
/**
* Gets a list of builds.
*
* @param {string} Name of the repository.
* @param {number} Number of the build.
*/
this.list = function(repoName, number) {
return $http.get('/api/tasks/'+repoName+'/'+number);
};
/**
* Gets a task.
*
* @param {string} Name of the repository.
* @param {number} Number of the build.
* @param {number} Number of the task.
*/
this.get = function(repoName, number, step) {
return $http.get('/api/tasks/'+repoName+'/'+name+'/'+step);
};
/**
* Gets a task.
*
* @param {string} Name of the repository.
* @param {number} Number of the build.
* @param {number} Number of the task.
*/
this.get = function(repoName, number, step) {
return $http.get('/api/tasks/'+repoName+'/'+name+'/'+step);
};
}
angular
.module('drone')
.service('tasks', TaskService);
})();

View file

@ -0,0 +1,86 @@
'use strict';
(function () {
/**
* Cached user object.
*/
var _user;
/**
* The UserService provides access to useer
* data using REST API calls.
*/
function UserService($http, $q) {
/**
* Gets a list of all users.
*/
this.list = function() {
return $http.get('/api/users');
};
/**
* Gets a user by login.
*/
this.get = function(login) {
return $http.get('/api/users/'+login);
};
/**
* Gets the currently authenticated user.
*/
this.getCurrent = function() {
return $http.get('/api/user');
};
/**
* Updates an existing user
*/
this.post = function(user) {
return $http.post('/api/users/'+user);
};
/**
* Updates an existing user
*/
this.put = function(user) {
return $http.put('/api/users/'+user.login, user);
};
/**
* Deletes a user.
*/
this.delete = function(user) {
return $http.delete('/api/users/'+user.login);
};
/**
* Gets the currently authenticated user from
* the local cache. If not exists, it will fetch
* from the server.
*/
this.getCached = function() {
var defer = $q.defer();
// if the user is already authenticated
if (_user) {
defer.resolve(_user);
return defer.promise;
}
// else fetch the currently authenticated
// user using the REST API.
this.getCurrent().then(function(payload){
_user=payload;
defer.resolve(_user);
});
return defer.promise;
}
}
angular
.module('drone')
.service('users', UserService);
})();

View file

@ -0,0 +1,80 @@
<h1>{{ repo.full_name }}/{{ build.number }}</h1>
<a href="/{{ repo.full_name }}">Back</a>
<dl>
<dt>Build State</dt>
<dd>{{ build.state }}</dd>
<dt>Started</dt>
<dd>{{ build.started_at | fromNow }}</dd>
<dt>Duration</dt>
<dd>{{ build.duration | toDuration }}</dd>
<dt>Type</dt>
<dd>{{ build.head_commit ? "push" : "pull request" }}</dd>
<dt>Ref</dt>
<dd>{{ build | ref }}</dd>
<dt>Sha</dt>
<dd>{{ build | sha }}</dd>
<dt>Author</dt>
<dd>{{ build | author }}</dd>
<dt>Message</dt>
<dd>{{ build | message }}</dd>
</dl>
<hr>
<dl>
<dt>Task State</dt>
<dd>{{ task.state }}</dd>
<dt>Started</dt>
<dd>{{ task.started_at | fromNow }}</dd>
<dt>Finished</dt>
<dd>{{ task.finished_at | fromNow }}</dd>
<dt>Duration</dt>
<dd>{{ task.duration | toDuration }}</dd>
<dt>Exit Code</dt>
<dd>{{ task.exit_code }}</dd>
<dt>Matrix</dt>
<dd>{{ task.environment }}</dd>
</dl>
<hr>
<pre>{{ logs }}</pre>
<table border="1">
<thead>
<tr>
<th>Number</th>
<th>Status</th>
<th>Started</th>
<th>Finished</th>
<th>Duration</th>
<th>Exit Code</th>
<th>Matrix</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="task in tasks">
<td><a ng-href="{{ repo.full_name }}/{{ build.number }}/{{ task.number }}">{{ task.number }}</a></td>
<td>{{ task.state }}</td>
<td>{{ task.started_at | fromNow }}</td>
<td>{{ task.finished_at | fromNow }}</td>
<td>{{ task.duration | toDuration }}</td>
<td>{{ task.exit_code }}</td>
<td>{{ task.environment }}</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,36 @@
<h1>{{ repo.full_name }}</h1>
<a href="/">Back</a>
<a ng-href="/{{ repo.full_name }}/edit">Settings</a>
<button ng-click="watch(repo)" ng-if="!repo.subscription || !repo.subscription.subscribed">Watch</button>
<button ng-click="unwatch(repo)" ng-if="repo.subscription && repo.subscription.subscribed">Unwatch</button>
<table border="1">
<thead>
<tr>
<th>Number</th>
<th>Status</th>
<th>Started</th>
<th>Duration</th>
<th>Type</th>
<th>Ref</th>
<th>Commit</th>
<th>Author</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="build in builds">
<td><a ng-href="/{{ repo.full_name }}/{{ build.number }}">{{ build.number }}</a></td>
<td>{{ build.state }}</td>
<td>{{ build.started_at | fromNow }}</td>
<td>{{ build.duration | toDuration }}</td>
<td>{{ build.head_commit ? "push" : "pull request" }}</td>
<td>{{ build | ref }}</td>
<td>{{ build | sha }}</td>
<td>{{ build | author }}</td>
<td>{{ build | message }}</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,3 @@
<h1>Login</h1>
<a href="/authorize" target="_self">Login</a>

View file

@ -0,0 +1,30 @@
<h1>Dashboard</h1>
<a href="/new">New</a>
<a href="/profile">Settings</a>
<a href="/users" ng-if="user.admin">User Management</a>
<table border="1">
<thead>
<tr>
<th>Repo</th>
<th>Status</th>
<th>Number</th>
<th>Started</th>
<th>Duration</th>
<th>Branch</th>
<th>Commit</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="repo in repos">
<td><a ng-href="{{ repo.full_name }}">{{ repo.full_name }}</a></td>
<td>{{ repo.last_build.state }}</td>
<td><a ng-href="{{ repo.full_name }}/{{ repo.last_build.number }}">{{ repo.last_build.number }}</a></td>
<td>{{ repo.last_build.started_at | fromNow }}</td>
<td>{{ repo.last_build.duration | toDuration }}</td>
<td>{{ repo.last_build.head_commit.ref || repo.last_build.pull_request.source.ref }}</td>
<td>{{ repo.last_build.head_commit.sha || repo.last_build.pull_request.source.ref }}</td>
</tr>
</tbody>
</table>

View file

@ -0,0 +1,8 @@
<h1>Add Repository</h1>
<a href="/">Back</a>
<form>
<input type="text" placeholder="octocat/Hello-World" ng-model="slug" />
<button ng-click="add(slug)">Add</button>
</form>

View file

@ -0,0 +1,42 @@
<h1>{{ repo.full_name }} / Edit</h1>
<a ng-href="/{{ repo.full_name }}">Back</a>
<form>
<div>
<label>Disable Pushes</label>
<input type="checkbox" ng-model="repo.disabled" />
</div>
<div>
<label>Disable PRs</label>
<input type="checkbox" ng-model="repo.disabled_prs" />
</div>
<div>
<label>Disable Tags</label>
<input type="checkbox" ng-model="repo.disabled_tags" />
</div>
<div>
<label>Trusted</label>
<input type="checkbox" ng-model="repo.trusted" />
</div>
<div>
<label>Timeout</label>
<input type="number" ng-model="repo.timeout" />
</div>
<ul>
<li ng-repeat="(key, value) in repo.params">
<label>{{ key }}</label>
<input type="text" ng-model="repo.params[key]"/>
<button ng-click="deleteParam(key)">remove</button>
</li>
<li>
<input type="text" ng-model="param.key" />
<input type="text" ng-model="param.value" />
<button ng-click="addParam(param)">add</button>
</li>
</ul>
<button ng-click="save(repo)">Save</button>
<button ng-click="delete(repo.full_name)">Delete</button>
</form>

View file

@ -0,0 +1,26 @@
<h1>{{ user.login }}</h1>
<a href="/">Back</a>
<dl>
<dt>Login</dt>
<dd>{{ user.login }}</dd>
<dt>Full Name</dt>
<dd>{{ user.name }}</dd>
<dt>Created</dt>
<dd>{{ user.created_at | fromNow }}</dd>
<dt>Updated</dt>
<dd>{{ user.update_at | fromNow }}</dd>
<dt>Email</dt>
<dd>{{ user.email }}</dd>
<dt>Site Admin</dt>
<dd>{{ user.admin }}</dd>
<dt>Gravatar</dt>
<dd><img ng-src="{{ user.gravatar_id | gravatar }}"</dd>
</dl>

View file

@ -0,0 +1,37 @@
<h1>Users</h1>
<a href="/">Back</a>
<table border="1">
<thead>
<tr>
<th>Login</th>
<th>Full Name</th>
<th>Created</th>
<th>Updated</th>
<th>Email</th>
<th>Site Admin</th>
<th>Gravatar</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="user in users">
<td>{{ user.login }}</td>
<td>{{ user.name }}</td>
<td>{{ user.created_at | fromNow }}</td>
<td>{{ user.updated_at | fromNow }}</td>
<td>{{ user.email }}</td>
<td>{{ !!user.admin }}</td>
<td><img ng-src="{{ user.gravatar_id | gravatar }}" /></td>
<td><button ng-click="toggle(user)">toggle admin</button></td>
<td><button ng-click="remove(user)">delete</button></td>
</tr>
</tbody>
</table>
<form>
<input type="text" ng-model="login" />
<button ng-click="add(login)">Add</button>
</form>

75
server/status.go Normal file
View file

@ -0,0 +1,75 @@
package server
import (
"strconv"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/drone/drone/common"
)
// GetStatus accepts a request to retrieve a build status
// from the datastore for the given repository and
// build number.
//
// GET /api/status/:owner/:name/:number/:context
//
func GetStatus(c *gin.Context) {
ds := ToDatastore(c)
repo := ToRepo(c)
num, _ := strconv.Atoi(c.Params.ByName("number"))
ctx := c.Params.ByName("context")
status, err := ds.GetBuildStatus(repo.FullName, num, ctx)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, status)
}
}
// PostStatus accepts a request to create a new build
// status. The created user status is returned in JSON
// format if successful.
//
// POST /api/status/:owner/:name/:number
//
func PostStatus(c *gin.Context) {
ds := ToDatastore(c)
repo := ToRepo(c)
num, err := strconv.Atoi(c.Params.ByName("number"))
if err != nil {
c.Fail(400, err)
return
}
in := &common.Status{}
if !c.BindWith(in, binding.JSON) {
c.AbortWithStatus(400)
return
}
if err := ds.InsertBuildStatus(repo.Name, num, in); err != nil {
c.Fail(400, err)
} else {
c.JSON(201, in)
}
}
// GetStatusList accepts a request to retrieve a list of
// all build status from the datastore for the given repository
// and build number.
//
// GET /api/status/:owner/:name/:number/:context
//
func GetStatusList(c *gin.Context) {
ds := ToDatastore(c)
repo := ToRepo(c)
num, _ := strconv.Atoi(c.Params.ByName("number"))
list, err := ds.GetBuildStatusList(repo.FullName, num)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, list)
}
}

84
server/subscribe.go Normal file
View file

@ -0,0 +1,84 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/drone/drone/common"
)
// GetSubscriber accepts a request to retrieve a repository
// subscriber from the datastore for the given repository by
// user Login.
//
// GET /api/subscribers/:owner/:name/:login
//
func GetSubscriber(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
login := c.Params.ByName("login")
subsc, err := store.GetSubscriber(repo.FullName, login)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, subsc)
}
}
// GetSubscribers accepts a request to retrieve a repository
// watchers from the datastore for the given repository.
//
// GET /api/subscribers/:owner/:name
//
func GetSubscribers(c *gin.Context) {
// store := ToDatastore(c)
// repo := ToRepo(c)
// subs, err := store.GetSubscribers(repo.FullName)
// if err != nil {
// c.Fail(404, err)
// } else {
// c.JSON(200, subs)
// }
c.Writer.WriteHeader(501)
}
// Unubscribe accapets a request to unsubscribe the
// currently authenticated user to the repository.
//
// DEL /api/subscribers/:owner/:name
//
func Unsubscribe(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
user := ToUser(c)
sub, err := store.GetSubscriber(repo.FullName, user.Login)
if err != nil {
c.Fail(404, err)
}
err = store.DeleteSubscriber(repo.FullName, sub)
if err != nil {
c.Fail(400, err)
} else {
c.Writer.WriteHeader(200)
}
}
// Subscribe accapets a request to subscribe the
// currently authenticated user to the repository.
//
// POST /api/subscriber/:owner/:name
//
func Subscribe(c *gin.Context) {
store := ToDatastore(c)
repo := ToRepo(c)
user := ToUser(c)
subscriber := &common.Subscriber{
Login: user.Login,
Subscribed: true,
}
err := store.InsertSubscriber(repo.FullName, subscriber)
if err != nil {
c.Fail(400, err)
} else {
c.JSON(200, subscriber)
}
}

46
server/tasks.go Normal file
View file

@ -0,0 +1,46 @@
package server
import (
"strconv"
"github.com/gin-gonic/gin"
)
// GetTask accepts a request to retrieve a build task
// from the datastore for the given repository and
// build number.
//
// GET /api/tasks/:owner/:name/:number/:task
//
func GetTask(c *gin.Context) {
ds := ToDatastore(c)
repo := ToRepo(c)
b, _ := strconv.Atoi(c.Params.ByName("number"))
t, _ := strconv.Atoi(c.Params.ByName("task"))
task, err := ds.GetTask(repo.FullName, b, t)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, task)
}
}
// GetTasks accepts a request to retrieve a list of
// build tasks from the datastore for the given repository
// and build number.
//
// GET /api/tasks/:owner/:name/:number
//
func GetTasks(c *gin.Context) {
ds := ToDatastore(c)
repo := ToRepo(c)
num, _ := strconv.Atoi(c.Params.ByName("number"))
tasks, err := ds.GetTaskList(repo.FullName, num)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, tasks)
}
}

59
server/user.go Normal file
View file

@ -0,0 +1,59 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/drone/drone/common"
"github.com/drone/drone/common/gravatar"
)
// GetUserCurr accepts a request to retrieve the
// currently authenticated user from the datastore
// and return in JSON format.
//
// GET /api/user
//
func GetUserCurr(c *gin.Context) {
c.JSON(200, ToUser(c))
}
// PutUserCurr accepts a request to update the currently
// authenticated User profile.
//
// PUT /api/user
//
func PutUserCurr(c *gin.Context) {
ds := ToDatastore(c)
me := ToUser(c)
in := &common.User{}
if !c.BindWith(in, binding.JSON) {
return
}
me.Email = in.Email
me.Gravatar = gravatar.Generate(in.Email)
err := ds.UpdateUser(me)
if err != nil {
c.Fail(400, err)
} else {
c.JSON(200, me)
}
}
// GetUserRepos accepts a request to get the currently
// authenticated user's repository list from the datastore,
// encoded and returned in JSON format.
//
// GET /api/user/repos
//
func GetUserRepos(c *gin.Context) {
ds := ToDatastore(c)
me := ToUser(c)
repos, err := ds.GetUserRepos(me.Login)
if err != nil {
c.Fail(400, err)
} else {
c.JSON(200, &repos)
}
}

125
server/users.go Normal file
View file

@ -0,0 +1,125 @@
package server
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/drone/drone/common"
"github.com/drone/drone/common/gravatar"
)
// GetUsers accepts a request to retrieve all users
// from the datastore and return encoded in JSON format.
//
// GET /api/users
//
func GetUsers(c *gin.Context) {
ds := ToDatastore(c)
users, err := ds.GetUserList()
if err != nil {
c.Fail(400, err)
} else {
c.JSON(200, users)
}
}
// PostUser accepts a request to create a new user in the
// system. The created user account is returned in JSON
// format if successful.
//
// POST /api/users
//
func PostUser(c *gin.Context) {
ds := ToDatastore(c)
name := c.Params.ByName("name")
user := &common.User{Login: name, Name: name}
if err := ds.InsertUser(user); err != nil {
c.Fail(400, err)
} else {
c.JSON(201, user)
}
}
// GetUser accepts a request to retrieve a user by hostname
// and login from the datastore and return encoded in JSON
// format.
//
// GET /api/users/:name
//
func GetUser(c *gin.Context) {
ds := ToDatastore(c)
name := c.Params.ByName("name")
user, err := ds.GetUser(name)
if err != nil {
c.Fail(404, err)
} else {
c.JSON(200, user)
}
}
// PutUser accepts a request to update an existing user in
// the system. The modified user account is returned in JSON
// format if successful.
//
// PUT /api/users/:name
//
func PutUser(c *gin.Context) {
ds := ToDatastore(c)
me := ToUser(c)
name := c.Params.ByName("name")
user, err := ds.GetUser(name)
if err != nil {
c.Fail(404, err)
return
}
in := &common.User{}
if !c.BindWith(in, binding.JSON) {
return
}
user.Email = in.Email
user.Gravatar = gravatar.Generate(user.Email)
// an administrator must not be able to
// downgrade her own account.
if me.Login != user.Login {
user.Admin = in.Admin
}
err = ds.UpdateUser(user)
if err != nil {
c.Fail(400, err)
} else {
c.JSON(200, user)
}
}
// DeleteUser accepts a request to delete the specified
// user account from the system. A successful request will
// respond with an OK 200 status.
//
// DELETE /api/users/:name
//
func DeleteUser(c *gin.Context) {
ds := ToDatastore(c)
me := ToUser(c)
name := c.Params.ByName("name")
user, err := ds.GetUser(name)
if err != nil {
c.Fail(404, err)
return
}
// an administrator must not be able to
// delete her own account.
if user.Login == me.Login {
c.Writer.WriteHeader(403)
return
}
if err := ds.DeleteUser(user); err != nil {
c.Fail(400, err)
} else {
c.Writer.WriteHeader(204)
}
}

92
server/ws.go Normal file
View file

@ -0,0 +1,92 @@
package server
import (
"time"
"github.com/drone/drone/eventbus"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
const (
// Time allowed to write the message to the client.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the client.
pongWait = 60 * time.Second
// Send pings to client with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// GetEvents will upgrade the connection to a Websocket and will stream
// event updates to the browser.
func GetEvents(c *gin.Context) {
bus := ToBus(c)
user := ToUser(c)
remote := ToRemote(c)
// upgrade the websocket
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
c.Fail(400, err)
return
}
ticker := time.NewTicker(pingPeriod)
eventc := make(chan *eventbus.Event, 1)
bus.Subscribe(eventc)
defer func() {
bus.Unsubscribe(eventc)
ticker.Stop()
ws.Close()
close(eventc)
}()
go func() {
for {
select {
case event := <-eventc:
if event == nil {
return // why would this ever happen?
}
perms := perms(remote, user, event.Repo)
if perms != nil && perms.Pull {
ws.WriteJSON(event)
}
case <-ticker.C:
ws.SetWriteDeadline(time.Now().Add(writeWait))
err := ws.WriteMessage(websocket.PingMessage, []byte{})
if err != nil {
ws.Close()
return
}
}
}
}()
readWebsocket(ws)
}
// readWebsocket will block while reading the websocket data
func readWebsocket(ws *websocket.Conn) {
defer ws.Close()
ws.SetReadLimit(512)
ws.SetReadDeadline(time.Now().Add(pongWait))
ws.SetPongHandler(func(string) error {
ws.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
_, _, err := ws.ReadMessage()
if err != nil {
break
}
}
}