added new handlers and workers

This commit is contained in:
Brad Rydzewski 2014-09-28 18:36:24 -07:00
parent 8993a71df2
commit 21f9aec808
45 changed files with 1522 additions and 3330 deletions

View file

@ -21,7 +21,7 @@ install:
install -t /usr/local/bin debian/drone/usr/local/bin/droned install -t /usr/local/bin debian/drone/usr/local/bin/droned
run: run:
@go run server/main.go @go run server/main.go --config=$$HOME/.drone/config.toml
clean: clean:
find . -name "*.out" -delete find . -name "*.out" -delete

View file

@ -0,0 +1,5 @@
package capability
const (
Registration = "REGISTRATION"
)

View file

@ -1,252 +0,0 @@
package database
import (
"database/sql"
"time"
"github.com/drone/drone/shared/model"
"github.com/russross/meddler"
)
type CommitManager interface {
// Find finds the commit by ID.
Find(id int64) (*model.Commit, error)
// FindSha finds the commit for the branch and sha.
FindSha(repo int64, branch, sha string) (*model.Commit, error)
// FindLatest finds the most recent commit for the branch.
FindLatest(repo int64, branch string) (*model.Commit, error)
// FindOutput finds the commit's output.
FindOutput(commit int64) ([]byte, error)
// List finds recent commits for the repository
List(repo int64) ([]*model.Commit, error)
// ListBranch finds recent commits for the repository and branch.
ListBranch(repo int64, branch string) ([]*model.Commit, error)
// ListBranches finds most recent commit for each branch.
//ListBranches(repo int64) ([]*model.Commit, error)
// ListUser finds most recent commits for a user.
ListUser(repo int64) ([]*model.CommitRepo, error)
// Insert persists the commit to the datastore.
Insert(commit *model.Commit) error
// Update persists changes to the commit to the datastore.
Update(commit *model.Commit) error
// UpdateOutput persists a commit's stdout to the datastore.
UpdateOutput(commit *model.Commit, out []byte) error
// Delete removes the commit from the datastore.
Delete(commit *model.Commit) error
// CancelAll will update the status of all Started or Pending
// builds to a status of Killed (cancelled).
CancelAll() error
}
// commitManager manages a list of commits in a SQL database.
type commitManager struct {
*sql.DB
}
// NewCommitManager initiales a new CommitManager intended to
// manage and persist commits.
func NewCommitManager(db *sql.DB) CommitManager {
return &commitManager{db}
}
// SQL query to retrieve the latest Commits for each branch.
const listBranchesQuery = `
SELECT *
FROM commits
WHERE commit_id IN (
SELECT MAX(commit_id)
FROM commits
WHERE repo_id=?
AND commit_status NOT IN ('Started', 'Pending')
GROUP BY commit_branch)
ORDER BY commit_branch ASC
`
// SQL query to retrieve the latest Commits for a specific branch.
const listBranchQuery = `
SELECT *
FROM commits
WHERE repo_id=?
AND commit_branch=?
ORDER BY commit_id DESC
LIMIT 20
`
// SQL query to retrieve the latest Commits for a user's repositories.
//const listUserCommitsQuery = `
//SELECT r.repo_remote, r.repo_host, r.repo_owner, r.repo_name, c.*
//FROM commits c, repos r, perms p
//WHERE c.repo_id=r.repo_id
//AND r.repo_id=p.repo_id
//AND p.user_id=?
//AND c.commit_status NOT IN ('Started', 'Pending')
//ORDER BY commit_id DESC
//LIMIT 20
//`
const listUserCommitsQuery = `
SELECT r.repo_remote, r.repo_host, r.repo_owner, r.repo_name, c.*
FROM commits c, repos r
WHERE c.repo_id=r.repo_id
AND c.commit_id IN (
SELECT max(c.commit_id)
FROM commits c, repos r, perms p
WHERE c.repo_id=r.repo_id
AND r.repo_id=p.repo_id
AND p.user_id=?
AND c.commit_id
AND c.commit_status NOT IN ('Started', 'Pending')
GROUP BY r.repo_id
) ORDER BY c.commit_created DESC LIMIT 5;
`
// SQL query to retrieve the latest Commits across all branches.
const listCommitsQuery = `
SELECT *
FROM commits
WHERE repo_id=?
ORDER BY commit_id DESC
LIMIT 20
`
// SQL query to retrieve a Commit by branch and sha.
const findCommitQuery = `
SELECT *
FROM commits
WHERE repo_id=?
AND commit_branch=?
AND commit_sha=?
LIMIT 1
`
// SQL query to retrieve the most recent Commit for a branch.
const findLatestCommitQuery = `
SELECT *
FROM commits
WHERE commit_id IN (
SELECT MAX(commit_id)
FROM commits
WHERE repo_id=?
AND commit_branch=?)
`
// SQL query to retrieve a Commit's stdout.
const findOutputQuery = `
SELECT output_raw
FROM output
WHERE commit_id = ?
`
// SQL statement to insert a Commit's stdout.
const insertOutputStmt = `
INSERT INTO output (commit_id, output_raw) values (?,?);
`
// SQL statement to update a Commit's stdout.
const updateOutputStmt = `
UPDATE output SET output_raw = ? WHERE commit_id = ?;
`
// SQL statement to delete a Commit by ID.
const deleteCommitStmt = `
DELETE FROM commits WHERE commit_id = ?;
`
// SQL statement to cancel all running Commits.
const cancelCommitStmt = `
UPDATE commits SET
commit_status = ?,
commit_started = ?,
commit_finished = ?
WHERE commit_status IN ('Started', 'Pending');
`
func (db *commitManager) Find(id int64) (*model.Commit, error) {
dst := model.Commit{}
err := meddler.Load(db, "commits", &dst, id)
return &dst, err
}
func (db *commitManager) FindSha(repo int64, branch, sha string) (*model.Commit, error) {
dst := model.Commit{}
err := meddler.QueryRow(db, &dst, findCommitQuery, repo, branch, sha)
return &dst, err
}
func (db *commitManager) FindLatest(repo int64, branch string) (*model.Commit, error) {
dst := model.Commit{}
err := meddler.QueryRow(db, &dst, findLatestCommitQuery, repo, branch)
return &dst, err
}
func (db *commitManager) FindOutput(commit int64) ([]byte, error) {
var dst string
err := db.QueryRow(findOutputQuery, commit).Scan(&dst)
return []byte(dst), err
}
func (db *commitManager) List(repo int64) ([]*model.Commit, error) {
var dst []*model.Commit
err := meddler.QueryAll(db, &dst, listCommitsQuery, repo)
return dst, err
}
func (db *commitManager) ListBranch(repo int64, branch string) ([]*model.Commit, error) {
var dst []*model.Commit
err := meddler.QueryAll(db, &dst, listBranchQuery, repo, branch)
return dst, err
}
func (db *commitManager) ListBranches(repo int64) ([]*model.Commit, error) {
var dst []*model.Commit
err := meddler.QueryAll(db, &dst, listBranchesQuery, repo)
return dst, err
}
func (db *commitManager) ListUser(user int64) ([]*model.CommitRepo, error) {
var dst []*model.CommitRepo
err := meddler.QueryAll(db, &dst, listUserCommitsQuery, user)
return dst, err
}
func (db *commitManager) Insert(commit *model.Commit) error {
commit.Created = time.Now().Unix()
commit.Updated = time.Now().Unix()
return meddler.Insert(db, "commits", commit)
}
func (db *commitManager) Update(commit *model.Commit) error {
commit.Updated = time.Now().Unix()
return meddler.Update(db, "commits", commit)
}
func (db *commitManager) UpdateOutput(commit *model.Commit, out []byte) error {
_, err := db.Exec(insertOutputStmt, commit.ID, out)
if err != nil {
return nil
}
_, err = db.Exec(updateOutputStmt, out, commit.ID)
return err
}
func (db *commitManager) Delete(commit *model.Commit) error {
_, err := db.Exec(deleteCommitStmt, commit.ID)
return err
}
func (db *commitManager) CancelAll() error {
_, err := db.Exec(cancelCommitStmt, model.StatusKilled, time.Now().Unix(), time.Now().Unix())
return err
}

View file

@ -1,282 +0,0 @@
package database
import (
"database/sql"
"testing"
"time"
"github.com/drone/drone/shared/model"
)
func TestCommitFind(t *testing.T) {
setup()
defer teardown()
commits := NewCommitManager(db)
commit, err := commits.Find(3)
if err != nil {
t.Errorf("Want Commit from ID, got %s", err)
}
testCommit(t, commit)
}
func TestCommitFindSha(t *testing.T) {
setup()
defer teardown()
commits := NewCommitManager(db)
commit, err := commits.FindSha(2, "master", "7253f6545caed41fb8f5a6fcdb3abc0b81fa9dbf")
if err != nil {
t.Errorf("Want Commit from SHA, got %s", err)
}
testCommit(t, commit)
}
func TestCommitFindLatest(t *testing.T) {
setup()
defer teardown()
commits := NewCommitManager(db)
commit, err := commits.FindLatest(2, "master")
if err != nil {
t.Errorf("Want Latest Commit, got %s", err)
}
testCommit(t, commit)
}
func TestCommitFindOutput(t *testing.T) {
setup()
defer teardown()
commits := NewCommitManager(db)
out, err := commits.FindOutput(1)
if err != nil {
t.Errorf("Want Commit stdout, got %s", err)
}
var want, got = "sample console output", string(out)
if want != got {
t.Errorf("Want stdout %v, got %v", want, got)
}
}
func TestCommitList(t *testing.T) {
setup()
defer teardown()
commits := NewCommitManager(db)
list, err := commits.List(2)
if err != nil {
t.Errorf("Want List from RepoID, got %s", err)
}
var got, want = len(list), 3
if got != want {
t.Errorf("Want List size %v, got %v", want, got)
}
testCommit(t, list[0])
}
func TestCommitListBranch(t *testing.T) {
setup()
defer teardown()
commits := NewCommitManager(db)
list, err := commits.ListBranch(2, "master")
if err != nil {
t.Errorf("Want List from RepoID, got %s", err)
}
var got, want = len(list), 2
if got != want {
t.Errorf("Want List size %v, got %v", want, got)
}
testCommit(t, list[0])
}
func TestCommitListBranches(t *testing.T) {
setup()
defer teardown()
commits := NewCommitManager(db)
list, err := commits.ListBranches(2)
if err != nil {
t.Errorf("Want Branch List from RepoID, got %s", err)
}
var got, want = len(list), 2
if got != want {
t.Errorf("Want List size %v, got %v", want, got)
}
testCommit(t, list[1])
}
func TestCommitInsert(t *testing.T) {
setup()
defer teardown()
commit := model.Commit{RepoID: 3, Branch: "foo", Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac"}
commits := NewCommitManager(db)
if err := commits.Insert(&commit); err != nil {
t.Errorf("Want Commit created, got %s", err)
}
// verify that it is ok to add same sha for different branch
var err = commits.Insert(&model.Commit{RepoID: 3, Branch: "bar", Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac"})
if err != nil {
t.Errorf("Want Commit created, got %s", err)
}
// verify unique remote + remote id constraint
err = commits.Insert(&model.Commit{RepoID: 3, Branch: "bar", Sha: "85f8c029b902ed9400bc600bac301a0aadb144ac"})
if err == nil {
t.Error("Want unique constraint violated")
}
}
func TestCommitUpdate(t *testing.T) {
setup()
defer teardown()
commits := NewCommitManager(db)
commit, err := commits.Find(5)
if err != nil {
t.Errorf("Want Commit from ID, got %s", err)
}
// update the commit's access token
commit.Status = "Success"
commit.Finished = time.Now().Unix()
commit.Duration = 999
if err := commits.Update(commit); err != nil {
t.Errorf("Want Commit updated, got %s", err)
}
updated, _ := commits.Find(5)
var got, want = updated.Status, "Success"
if got != want {
t.Errorf("Want updated Status %v, got %v", want, got)
}
var gotInt64, wantInt64 = updated.ID, commit.ID
if gotInt64 != wantInt64 {
t.Errorf("Want commit ID %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = updated.Duration, commit.Duration
if gotInt64 != wantInt64 {
t.Errorf("Want updated Duration %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = updated.Finished, commit.Finished
if gotInt64 != wantInt64 {
t.Errorf("Want updated Finished %v, got %v", wantInt64, gotInt64)
}
}
func TestCommitDelete(t *testing.T) {
setup()
defer teardown()
commits := NewCommitManager(db)
commit, err := commits.Find(1)
if err != nil {
t.Errorf("Want Commit from ID, got %s", err)
}
// delete the commit
if err := commits.Delete(commit); err != nil {
t.Errorf("Want Commit deleted, got %s", err)
}
// check to see if the deleted commit is actually gone
if _, err := commits.Find(1); err != sql.ErrNoRows {
t.Errorf("Want ErrNoRows, got %s", err)
}
}
// testCommit is a helper function that compares the commit
// to an expected set of fixed field values.
func testCommit(t *testing.T, commit *model.Commit) {
var got, want = commit.Status, "Success"
if got != want {
t.Errorf("Want Status %v, got %v", want, got)
}
got, want = commit.Sha, "7253f6545caed41fb8f5a6fcdb3abc0b81fa9dbf"
if got != want {
t.Errorf("Want Sha %v, got %v", want, got)
}
got, want = commit.Branch, "master"
if got != want {
t.Errorf("Want Branch %v, got %v", want, got)
}
got, want = commit.PullRequest, "5"
if got != want {
t.Errorf("Want PullRequest %v, got %v", want, got)
}
got, want = commit.Author, "drcooper@caltech.edu"
if got != want {
t.Errorf("Want Author %v, got %v", want, got)
}
got, want = commit.Gravatar, "ab23a88a3ed77ecdfeb894c0eaf2817a"
if got != want {
t.Errorf("Want Gravatar %v, got %v", want, got)
}
got, want = commit.Timestamp, "Wed Apr 23 01:02:38 2014 -0700"
if got != want {
t.Errorf("Want Timestamp %v, got %v", want, got)
}
got, want = commit.Message, "a commit message"
if got != want {
t.Errorf("Want Message %v, got %v", want, got)
}
var gotInt64, wantInt64 = commit.ID, int64(3)
if gotInt64 != wantInt64 {
t.Errorf("Want ID %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = commit.RepoID, int64(2)
if gotInt64 != wantInt64 {
t.Errorf("Want RepoID %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = commit.Created, int64(1398065343)
if gotInt64 != wantInt64 {
t.Errorf("Want Created %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = commit.Updated, int64(1398065344)
if gotInt64 != wantInt64 {
t.Errorf("Want Updated %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = commit.Started, int64(1398065345)
if gotInt64 != wantInt64 {
t.Errorf("Want Started %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = commit.Finished, int64(1398069999)
if gotInt64 != wantInt64 {
t.Errorf("Want Finished %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = commit.Duration, int64(854)
if gotInt64 != wantInt64 {
t.Errorf("Want Duration %v, got %v", wantInt64, gotInt64)
}
}

View file

@ -1,161 +0,0 @@
package database
import (
"database/sql"
"time"
"github.com/drone/drone/shared/model"
"github.com/russross/meddler"
)
type PermManager interface {
// Grant will grant the user read, write and admin persmissions
// to the specified repository.
Grant(u *model.User, r *model.Repo, read, write, admin bool) error
// Revoke will revoke all user permissions to the specified repository.
Revoke(u *model.User, r *model.Repo) error
// Find returns the user's permission to access the specified repository.
Find(u *model.User, r *model.Repo) *model.Perm
// Read returns true if the specified user has read
// access to the repository.
Read(u *model.User, r *model.Repo) (bool, error)
// Write returns true if the specified user has write
// access to the repository.
//Write(u *model.User, r *model.Repo) (bool, error)
// Admin returns true if the specified user is an
// administrator of the repository.
Admin(u *model.User, r *model.Repo) (bool, error)
// Member returns true if the specified user is a
// collaborator on the repository.
//Member(u *model.User, r *model.Repo) (bool, error)
}
// permManager manages user permissions to access repositories.
type permManager struct {
*sql.DB
}
// SQL query to retrieve a user's permission to
// access a repository.
const findPermQuery = `
SELECT *
FROM perms
WHERE user_id=?
AND repo_id=?
LIMIT 1
`
// SQL statement to delete a permission.
const deletePermStmt = `
DELETE FROM perms WHERE user_id=? AND repo_id=?
`
// NewManager initiales a new PermManager intended to
// manage user permission and access control.
func NewPermManager(db *sql.DB) PermManager {
return &permManager{db}
}
// Grant will grant the user read, write and admin persmissions
// to the specified repository.
func (db *permManager) Grant(u *model.User, r *model.Repo, read, write, admin bool) error {
// attempt to get existing permissions from the database
perm, err := db.find(u, r)
if err != nil && err != sql.ErrNoRows {
return err
}
// if this is a new permission set the user ID,
// repository ID and created timestamp.
if perm.ID == 0 {
perm.UserID = u.ID
perm.RepoID = r.ID
perm.Created = time.Now().Unix()
}
// set all the permission values
perm.Read = read
perm.Write = write
perm.Admin = admin
perm.Updated = time.Now().Unix()
// update the database
return meddler.Save(db, "perms", perm)
}
// Revoke will revoke all user permissions to the specified repository.
func (db *permManager) Revoke(u *model.User, r *model.Repo) error {
_, err := db.Exec(deletePermStmt, u.ID, r.ID)
return err
}
func (db *permManager) Find(u *model.User, r *model.Repo) *model.Perm {
// if the user is a gues they should only be granted
// read access to public repositories.
switch {
case u == nil && r.Private:
return &model.Perm{
Guest: true,
Read: false,
Write: false,
Admin: false}
case u == nil && !r.Private:
return &model.Perm{
Guest: true,
Read: true,
Write: false,
Admin: false}
}
// if the user is authenticated we'll retireive the
// permission details from the database.
perm, err := db.find(u, r)
if err != nil && perm.ID != 0 {
return perm
}
switch {
// if the user is a system admin grant super access.
case u.Admin == true:
perm.Read = true
perm.Write = true
perm.Admin = true
perm.Guest = true
// if the repo is public, grant read access only.
case r.Private == false:
perm.Read = true
perm.Guest = true
}
return perm
}
func (db *permManager) Read(u *model.User, r *model.Repo) (bool, error) {
return db.Find(u, r).Read, nil
}
func (db *permManager) Write(u *model.User, r *model.Repo) (bool, error) {
return db.Find(u, r).Write, nil
}
func (db *permManager) Admin(u *model.User, r *model.Repo) (bool, error) {
return db.Find(u, r).Admin, nil
}
func (db *permManager) Member(u *model.User, r *model.Repo) (bool, error) {
perm := db.Find(u, r)
return perm.Read && !perm.Guest, nil
}
func (db *permManager) find(u *model.User, r *model.Repo) (*model.Perm, error) {
var dst = model.Perm{}
var err = meddler.QueryRow(db, &dst, findPermQuery, u.ID, r.ID)
return &dst, err
}

View file

@ -1,310 +0,0 @@
package database
import (
"database/sql"
"testing"
"github.com/drone/drone/shared/model"
)
func Test_find(t *testing.T) {
setup()
defer teardown()
manager := NewPermManager(db).(*permManager)
perm, err := manager.find(&model.User{ID: 101}, &model.Repo{ID: 200})
if err != nil {
t.Errorf("Want permission, got %s", err)
}
var got, want = perm.ID, int64(1)
if got != want {
t.Errorf("Want ID %d, got %d", got, want)
}
got, want = perm.UserID, int64(101)
if got != want {
t.Errorf("Want Created %d, got %d", got, want)
}
got, want = perm.RepoID, int64(200)
if got != want {
t.Errorf("Want Created %d, got %d", got, want)
}
got, want = perm.Created, int64(1398065343)
if got != want {
t.Errorf("Want Created %d, got %d", got, want)
}
got, want = perm.Updated, int64(1398065344)
if got != want {
t.Errorf("Want Updated %d, got %d", got, want)
}
var gotBool, wantBool = perm.Read, true
if gotBool != wantBool {
t.Errorf("Want Read %v, got %v", gotBool, wantBool)
}
gotBool, wantBool = perm.Write, true
if gotBool != wantBool {
t.Errorf("Want Read %v, got %v", gotBool, wantBool)
}
gotBool, wantBool = perm.Admin, true
if gotBool != wantBool {
t.Errorf("Want Read %v, got %v", gotBool, wantBool)
}
// test that we get the appropriate error message when
// no permissions are found in the database.
_, err = manager.find(&model.User{ID: 102}, &model.Repo{ID: 201})
if err != sql.ErrNoRows {
t.Errorf("Want ErrNoRows, got %s", err)
}
}
func TestPermFind(t *testing.T) {
setup()
defer teardown()
manager := NewPermManager(db).(*permManager)
u := model.User{ID: 101, Admin: false}
r := model.Repo{ID: 201, Private: false}
// public repos should always be accessible
if perm := manager.Find(&u, &r); !perm.Read {
t.Errorf("Public repos should always be READ accessible")
}
// public repos should always be accessible, even to guest users
if perm := manager.Find(nil, &r); !perm.Read || perm.Write || perm.Admin {
t.Errorf("Public repos should always be READ accessible, even to nil users")
}
// private repos should not be accessible to nil users
r.Private = true
if perm := manager.Find(nil, &r); perm.Read || perm.Write || perm.Admin {
t.Errorf("Private repos should not be READ accessible to nil users")
}
// private repos should not be accessible to users without a row in the perm table.
r.Private = true
if perm := manager.Find(&u, &r); perm.Read || perm.Write || perm.Admin {
t.Errorf("Private repos should not be READ accessible to users without a row in the perm table.")
}
// private repos should be accessible to admins
r.Private = true
u.Admin = true
if perm := manager.Find(&u, &r); !perm.Read || !perm.Write || !perm.Admin {
t.Errorf("Private repos should be READ accessible to admins")
}
// private repos should be accessible to users with rows in the perm table.
r.ID = 200
r.Private = true
u.Admin = false
if perm := manager.Find(&u, &r); !perm.Read {
t.Errorf("Private repos should be READ accessible to users with rows in the perm table.")
}
}
func TestPermRead(t *testing.T) {
setup()
defer teardown()
var manager = NewPermManager(db)
// dummy admin and repo
u := model.User{ID: 101, Admin: false}
r := model.Repo{ID: 201, Private: false}
// public repos should always be accessible
if read, err := manager.Read(&u, &r); !read || err != nil {
t.Errorf("Public repos should always be READ accessible")
}
// public repos should always be accessible, even to guest users
if read, err := manager.Read(nil, &r); !read || err != nil {
t.Errorf("Public repos should always be READ accessible, even to nil users")
}
// private repos should not be accessible to nil users
r.Private = true
if read, err := manager.Read(nil, &r); read || err != nil {
t.Errorf("Private repos should not be READ accessible to nil users")
}
// private repos should not be accessible to users without a row in the perm table.
r.Private = true
if read, _ := manager.Read(&u, &r); read {
t.Errorf("Private repos should not be READ accessible to users without a row in the perm table.")
}
// private repos should be accessible to admins
r.Private = true
u.Admin = true
if read, err := manager.Read(&u, &r); !read || err != nil {
t.Errorf("Private repos should be READ accessible to admins")
}
// private repos should be accessible to users with rows in the perm table.
r.ID = 200
r.Private = true
u.Admin = false
if read, err := manager.Read(&u, &r); !read || err != nil {
t.Errorf("Private repos should be READ accessible to users with rows in the perm table.")
}
}
func TestPermWrite(t *testing.T) {
setup()
defer teardown()
var manager = NewPermManager(db)
// dummy admin and repo
u := model.User{ID: 101, Admin: false}
r := model.Repo{ID: 201, Private: false}
// repos should not be accessible to nil users
r.Private = true
if write, err := manager.Write(nil, &r); write || err != nil {
t.Errorf("Repos should not be WRITE accessible to nil users")
}
// repos should not be accessible to users without a row in the perm table.
if write, _ := manager.Write(&u, &r); write {
t.Errorf("Repos should not be WRITE accessible to users without a row in the perm table.")
}
// repos should be accessible to admins
u.Admin = true
if write, err := manager.Write(&u, &r); !write || err != nil {
t.Errorf("Repos should be WRITE accessible to admins")
}
// repos should be accessible to users with rows in the perm table.
r.ID = 200
u.Admin = false
if write, err := manager.Write(&u, &r); !write || err != nil {
t.Errorf("Repos should be WRITE accessible to users with rows in the perm table.")
}
// repos should not be accessible to users with a row in the perm table, but write=false
u.ID = 103
u.Admin = false
if write, err := manager.Write(&u, &r); write || err != nil {
t.Errorf("Repos should not be WRITE accessible to users with perm.Write=false.")
}
}
func TestPermAdmin(t *testing.T) {
setup()
defer teardown()
var manager = NewPermManager(db)
// dummy admin and repo
u := model.User{ID: 101, Admin: false}
r := model.Repo{ID: 201, Private: false}
// repos should not be accessible to nil users
r.Private = true
if admin, err := manager.Admin(nil, &r); admin || err != nil {
t.Errorf("Repos should not be ADMIN accessible to nil users")
}
// repos should not be accessible to users without a row in the perm table.
if admin, _ := manager.Admin(&u, &r); admin {
t.Errorf("Repos should not be ADMIN accessible to users without a row in the perm table.")
}
// repos should be accessible to admins
u.Admin = true
if admin, err := manager.Admin(&u, &r); !admin || err != nil {
t.Errorf("Repos should be ADMIN accessible to admins")
}
// repos should be accessible to users with rows in the perm table.
r.ID = 200
u.Admin = false
if admin, err := manager.Admin(&u, &r); !admin || err != nil {
t.Errorf("Repos should be ADMIN accessible to users with rows in the perm table.")
}
// repos should not be accessible to users with a row in the perm table, but admin=false
u.ID = 103
u.Admin = false
if admin, err := manager.Admin(&u, &r); admin || err != nil {
t.Errorf("Repos should not be ADMIN accessible to users with perm.Admin=false.")
}
}
func TestPermRevoke(t *testing.T) {
setup()
defer teardown()
// dummy admin and repo
u := model.User{ID: 101}
r := model.Repo{ID: 200}
manager := NewPermManager(db).(*permManager)
admin, err := manager.Admin(&u, &r)
if !admin || err != nil {
t.Errorf("Want Admin permission, got Admin %v, error %s", admin, err)
}
// revoke permissions
if err := manager.Revoke(&u, &r); err != nil {
t.Errorf("Want revoked permissions, got %s", err)
}
perm, err := manager.find(&u, &r)
if perm.Admin == true || err != sql.ErrNoRows {
t.Errorf("Expected revoked permission, got Admin %v, error %v", perm.Admin, err)
}
}
func TestPermGrant(t *testing.T) {
setup()
defer teardown()
// dummy admin and repo
u := model.User{ID: 104}
r := model.Repo{ID: 200}
manager := NewPermManager(db).(*permManager)
if err := manager.Grant(&u, &r, true, true, true); err != nil {
t.Errorf("Want permissions granted, got %s", err)
}
// add new permissions
perm, err := manager.find(&u, &r)
if err != nil {
t.Errorf("Want permission, got %s", err)
} else if perm.Read != true {
t.Errorf("Want Read permission True, got %v", perm.Read)
} else if perm.Write != true {
t.Errorf("Want Write permission True, got %v", perm.Write)
} else if perm.Admin != true {
t.Errorf("Want Admin permission True, got %v", perm.Admin)
}
// update permissions
if err := manager.Grant(&u, &r, false, false, false); err != nil {
t.Errorf("Want permissions granted, got %s", err)
}
// add new permissions
perm, err = manager.find(&u, &r)
if err != nil {
t.Errorf("Want permission updated, got %s", err)
} else if perm.Read != false {
t.Errorf("Want Read permission False, got %v", perm.Read)
} else if perm.Write != false {
t.Errorf("Want Write permission False, got %v", perm.Write)
} else if perm.Admin != false {
t.Errorf("Want Admin permission False, got %v", perm.Admin)
}
}

View file

@ -1,78 +0,0 @@
package database
import (
"database/sql"
"time"
"github.com/drone/drone/shared/model"
"github.com/russross/meddler"
)
type RepoManager interface {
// Find retrieves the Repo by ID.
Find(id int64) (*model.Repo, error)
// FindName retrieves the Repo by the remote, owner and name.
FindName(remote, owner, name string) (*model.Repo, error)
// Insert persists a new Repo to the datastore.
Insert(repo *model.Repo) error
// Insert persists a modified Repo to the datastore.
Update(repo *model.Repo) error
// Delete removes a Repo from the datastore.
Delete(repo *model.Repo) error
// List retrieves all repositories from the datastore.
List(user int64) ([]*model.Repo, error)
// List retrieves all public repositories from the datastore.
//ListPublic(user int64) ([]*Repo, error)
}
func NewRepoManager(db *sql.DB) RepoManager {
return &repoManager{db}
}
type repoManager struct {
*sql.DB
}
func (db *repoManager) Find(id int64) (*model.Repo, error) {
const query = "select * from repos where repo_id = ?"
var repo = model.Repo{}
var err = meddler.QueryRow(db, &repo, query, id)
return &repo, err
}
func (db *repoManager) FindName(remote, owner, name string) (*model.Repo, error) {
const query = "select * from repos where repo_host = ? and repo_owner = ? and repo_name = ?"
var repo = model.Repo{}
var err = meddler.QueryRow(db, &repo, query, remote, owner, name)
return &repo, err
}
func (db *repoManager) List(user int64) ([]*model.Repo, error) {
const query = "select * from repos where repo_id IN (select repo_id from perms where user_id = ?)"
var repos []*model.Repo
err := meddler.QueryAll(db, &repos, query, user)
return repos, err
}
func (db *repoManager) Insert(repo *model.Repo) error {
repo.Created = time.Now().Unix()
repo.Updated = time.Now().Unix()
return meddler.Insert(db, "repos", repo)
}
func (db *repoManager) Update(repo *model.Repo) error {
repo.Updated = time.Now().Unix()
return meddler.Update(db, "repos", repo)
}
func (db *repoManager) Delete(repo *model.Repo) error {
const stmt = "delete from repos where repo_id = ?"
_, err := db.Exec(stmt, repo.ID)
return err
}

View file

@ -1,226 +0,0 @@
package database
import (
"database/sql"
"testing"
"github.com/drone/drone/shared/model"
)
func TestRepoFind(t *testing.T) {
setup()
defer teardown()
repos := NewRepoManager(db)
repo, err := repos.Find(1)
if err != nil {
t.Errorf("Want Repo from ID, got %s", err)
}
testRepo(t, repo)
}
func TestRepoFindName(t *testing.T) {
setup()
defer teardown()
repos := NewRepoManager(db)
user, err := repos.FindName("github.com", "lhofstadter", "lenwoloppali")
if err != nil {
t.Errorf("Want Repo by Name, got %s", err)
}
testRepo(t, user)
}
func TestRepoList(t *testing.T) {
setup()
defer teardown()
repos := NewRepoManager(db)
all, err := repos.List(1)
if err != nil {
t.Errorf("Want Repos, got %s", err)
}
var got, want = len(all), 2
if got != want {
t.Errorf("Want %v Repos, got %v", want, got)
}
testRepo(t, all[0])
}
func TestRepoInsert(t *testing.T) {
setup()
defer teardown()
repo, _ := model.NewRepo("github.com", "mrwolowitz", "lenwoloppali")
repos := NewRepoManager(db)
if err := repos.Insert(repo); err != nil {
t.Errorf("Want Repo created, got %s", err)
}
// verify unique remote + owner + name login constraint
var err = repos.Insert(&model.Repo{Host: repo.Host, Owner: repo.Owner, Name: repo.Name})
if err == nil {
t.Error("Want unique constraint violated")
}
}
func TestRepoUpdate(t *testing.T) {
setup()
defer teardown()
repos := NewRepoManager(db)
repo, err := repos.Find(1)
if err != nil {
t.Errorf("Want Repo from ID, got %s", err)
}
// update the repo's access token
repo.Active = false
repo.Private = false
repo.Privileged = false
repo.PostCommit = false
repo.PullRequest = false
if err := repos.Update(repo); err != nil {
t.Errorf("Want Repo updated, got %s", err)
}
updated, _ := repos.Find(1)
var got, want = updated.Active, repo.Active
if got != want {
t.Errorf("Want updated Active %v, got %v", want, got)
}
got, want = updated.Private, repo.Private
if got != want {
t.Errorf("Want updated Private %v, got %v", want, got)
}
got, want = updated.Privileged, repo.Privileged
if got != want {
t.Errorf("Want updated Privileged %v, got %v", want, got)
}
got, want = updated.PostCommit, repo.PostCommit
if got != want {
t.Errorf("Want updated PostCommit %v, got %v", want, got)
}
got, want = updated.PullRequest, repo.PullRequest
if got != want {
t.Errorf("Want updated PullRequest %v, got %v", want, got)
}
}
func TestRepoDelete(t *testing.T) {
setup()
defer teardown()
repos := NewRepoManager(db)
repo, err := repos.Find(1)
if err != nil {
t.Errorf("Want Repo from ID, got %s", err)
}
// delete the repo
if err := repos.Delete(repo); err != nil {
t.Errorf("Want Repo deleted, got %s", err)
}
// check to see if the deleted repo is actually gone
if _, err := repos.Find(1); err != sql.ErrNoRows {
t.Errorf("Want ErrNoRows, got %s", err)
}
}
// testRepo is a helper function that compares the repo
// to an expected set of fixed field values.
func testRepo(t *testing.T, repo *model.Repo) {
var got, want = repo.Remote, "github.com"
if got != want {
t.Errorf("Want Remote %v, got %v", want, got)
}
got, want = repo.Host, "github.com"
if got != want {
t.Errorf("Want Host %v, got %v", want, got)
}
got, want = repo.Owner, "lhofstadter"
if got != want {
t.Errorf("Want Owner %v, got %v", want, got)
}
got, want = repo.Name, "lenwoloppali"
if got != want {
t.Errorf("Want Name %v, got %v", want, got)
}
got, want = repo.CloneURL, "git://github.com/lhofstadter/lenwoloppali.git"
if got != want {
t.Errorf("Want URL %v, got %v", want, got)
}
got, want = repo.PublicKey, "publickey"
if got != want {
t.Errorf("Want PublicKey %v, got %v", want, got)
}
got, want = repo.PrivateKey, "privatekey"
if got != want {
t.Errorf("Want PrivateKey %v, got %v", want, got)
}
got, want = repo.Params, "params"
if got != want {
t.Errorf("Want Params %v, got %v", want, got)
}
var gotBool, wantBool = repo.Active, true
if gotBool != wantBool {
t.Errorf("Want Active %v, got %v", wantBool, gotBool)
}
gotBool, wantBool = repo.Private, true
if gotBool != wantBool {
t.Errorf("Want Private %v, got %v", wantBool, gotBool)
}
gotBool, wantBool = repo.Privileged, true
if gotBool != wantBool {
t.Errorf("Want Privileged %v, got %v", wantBool, gotBool)
}
gotBool, wantBool = repo.PostCommit, true
if gotBool != wantBool {
t.Errorf("Want PostCommit %v, got %v", wantBool, gotBool)
}
gotBool, wantBool = repo.PullRequest, true
if gotBool != wantBool {
t.Errorf("Want PullRequest %v, got %v", wantBool, gotBool)
}
var gotInt64, wantInt64 = repo.ID, int64(1)
if gotInt64 != wantInt64 {
t.Errorf("Want ID %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = repo.Created, int64(1398065343)
if gotInt64 != wantInt64 {
t.Errorf("Want Created %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = repo.Updated, int64(1398065344)
if gotInt64 != wantInt64 {
t.Errorf("Want Updated %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = repo.Timeout, int64(900)
if gotInt64 != wantInt64 {
t.Errorf("Want Timeout %v, got %v", wantInt64, gotInt64)
}
}

View file

@ -1,128 +0,0 @@
package schema
import (
"database/sql"
"log"
)
// statements to setup our database
var stmts = []string{`
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT
,user_remote VARCHAR(255)
,user_login VARCHAR(255)
,user_access VARCHAR(255)
,user_secret VARCHAR(255)
,user_name VARCHAR(255)
,user_email VARCHAR(255)
,user_gravatar VARCHAR(255)
,user_token VARCHAR(255)
,user_admin BOOLEAN
,user_active BOOLEAN
,user_syncing BOOLEAN
,user_created INTEGER
,user_updated INTEGER
,user_synced INTEGER
,UNIQUE(user_token)
,UNIQUE(user_remote, user_login)
);`, `
CREATE TABLE IF NOT EXISTS perms (
perm_id INTEGER PRIMARY KEY AUTOINCREMENT
,user_id INTEGER
,repo_id INTEGER
,perm_read BOOLEAN
,perm_write BOOLEAN
,perm_admin BOOLEAN
,perm_created INTEGER
,perm_updated INTEGER
,UNIQUE (repo_id, user_id)
);`, `
CREATE TABLE IF NOT EXISTS repos (
repo_id INTEGER PRIMARY KEY AUTOINCREMENT
,user_id INTEGER
,repo_remote VARCHAR(255)
,repo_host VARCHAR(255)
,repo_owner VARCHAR(255)
,repo_name VARCHAR(255)
,repo_url VARCHAR(1024)
,repo_clone_url VARCHAR(255)
,repo_git_url VARCHAR(255)
,repo_ssh_url VARCHAR(255)
,repo_active BOOLEAN
,repo_private BOOLEAN
,repo_privileged BOOLEAN
,repo_post_commit BOOLEAN
,repo_pull_request BOOLEAN
,repo_public_key VARCHAR(4000)
,repo_private_key VARCHAR(4000)
,repo_params VARCHAR(4000)
,repo_timeout INTEGER
,repo_created INTEGER
,repo_updated INTEGER
,UNIQUE(repo_host, repo_owner, repo_name)
);`, `
CREATE TABLE IF NOT EXISTS commits (
commit_id INTEGER PRIMARY KEY AUTOINCREMENT
,repo_id INTEGER
,commit_status VARCHAR(255)
,commit_started INTEGER
,commit_finished INTEGER
,commit_duration INTEGER
,commit_sha VARCHAR(255)
,commit_branch VARCHAR(255)
,commit_pr VARCHAR(255)
,commit_author VARCHAR(255)
,commit_gravatar VARCHAR(255)
,commit_timestamp VARCHAR(255)
,commit_message VARCHAR(255)
,commit_yaml VARCHAR(4000)
,commit_created INTEGER
,commit_updated INTEGER
,UNIQUE(commit_sha, commit_branch, repo_id)
);`, `
CREATE TABLE IF NOT EXISTS output (
output_id INTEGER PRIMARY KEY AUTOINCREMENT
,commit_id INTEGER
,output_raw BLOB
,UNIQUE(commit_id)
);`, `
CREATE TABLE IF NOT EXISTS remotes (
remote_id INTEGER PRIMARY KEY AUTOINCREMENT
,remote_type VARCHAR(255)
,remote_host VARCHAR(255)
,remote_url VARCHAR(255)
,remote_api VARCHAR(255)
,remote_client VARCHAR(255)
,remote_secret VARCHAR(255)
,remote_open BOOLEAN
,UNIQUE(remote_host)
,UNIQUE(remote_type)
);`, `
CREATE TABLE IF NOT EXISTS servers (
server_id INTEGER PRIMARY KEY AUTOINCREMENT
,server_name VARCHAR(255)
,server_host VARCHAR(255)
,server_user VARCHAR(255)
,server_pass VARCHAR(255)
,server_cert VARCHAR(4000)
,UNIQUE(server_name)
);`, `
CREATE TABLE IF NOT EXISTS smtp (
smtp_id INTEGER PRIMARY KEY AUTOINCREMENT
,smtp_from VARCHAR(255)
,smtp_host VARCHAR(255)
,smtp_port VARCHAR(255)
,smtp_user VARCHAR(255)
,smtp_pass VARCHAR(255)
);`,
}
func Load(db *sql.DB) {
// execute all setup commands
for _, stmt := range stmts {
if _, err := db.Exec(stmt); err != nil {
// exit on failure since this should never happen
log.Fatalf("Error generating database schema. %s\n%s", err, stmt)
}
}
}

View file

@ -1,57 +0,0 @@
package testdata
import (
"database/sql"
"fmt"
)
var stmts = []string{
// insert user entries
"insert into users values (1, 'github.com', 'smellypooper', 'f0b461ca586c27872b43a0685cbc2847', '976f22a5eef7caacb7e678d6c52f49b1', 'Dr. Cooper', 'drcooper@caltech.edu', 'b9015b0857e16ac4d94a0ffd9a0b79c8', 'e42080dddf012c718e476da161d21ad5', 1, 1, 0, 1398065343, 1398065344, 1398065345);",
"insert into users values (2, 'github.com', 'lhofstadter', 'e4105c3059ac4c466594932dc9a4ffb2', '2257216903d9cd0d3d24772132febf52', 'Dr. Hofstadter', 'leanard@caltech.edu', '23dde632fdece6880f4ff03bb20f05d7', 'a5ad0d75f317f0b0a5dfdb68e5a3079e', 1, 1, 0, 1398065343, 1398065344, 1398065345);",
"insert into users values (3, 'gitlab.com', 'browndynamite', '4821477cc26a0c8c80c6c9b568d98e32', '1dd52c37cf5c63fe5abfd047b5b74a31', 'Dr. Koothrappali', 'rajesh@caltech.edu', 'f9133051f480b7ea88848b9f0a079dae', '7a50ede04637d4a8fce532c7d511226b', 1, 1, 0, 1398065343, 1398065344, 1398065345);",
"insert into users values (4, 'github.com', 'mrwolowitz', '1f6a80bde960e6913bf9b7e61eadd068', '74c40472494ba7f9f6c3ae061ff799ed', 'Mr. Wolowitz', 'wolowitz@caltech.edu', 'ea250570c794d84dc583421bb717be82', '3bd7e7d7411b2978e45919c9ad419984', 1, 1, 0, 1398065343, 1398065344, 1398065345);",
// insert repository entries
"insert into repos values (1, 0, 'github.com', 'github.com', 'lhofstadter', 'lenwoloppali', '', 'git://github.com/lhofstadter/lenwoloppali.git', '', '', 1, 1, 1, 1, 1, 'publickey', 'privatekey', 'params', 900, 1398065343, 1398065344);",
"insert into repos values (2, 0, 'github.com', 'github.com', 'browndynamite', 'lenwoloppali', '', 'git://github.com/browndynamite/lenwoloppali.git', '', '', 1, 1, 1, 1, 1, 'publickey', 'privatekey', 'params', 900, 1398065343, 1398065344);",
"insert into repos values (3, 0, 'gitlab.com', 'gitlab.com', 'browndynamite', 'lenwoloppali', '', 'git://gitlab.com/browndynamite/lenwoloppali.git', '', '', 1, 1, 1, 1, 1, 'publickey', 'privatekey', 'params', 900, 1398065343, 1398065344);",
// insert user + repository permission entries
"insert into perms values (1, 101, 200, 1, 1, 1, 1398065343, 1398065344);",
"insert into perms values (2, 102, 200, 1, 1, 0, 1398065343, 1398065344);",
"insert into perms values (3, 103, 200, 1, 0, 0, 1398065343, 1398065344);",
"insert into perms values (4, 1, 1, 1, 1, 1, 1398065343, 1398065344);",
"insert into perms values (5, 1, 2, 1, 1, 0, 1398065343, 1398065344);",
// insert commit entries
"insert into commits values (1, 2, 'Success', 1398065345, 1398069999, 854, '4e81eca185897c2d0cf81f5bc12623550c2ef952', 'dev', '3', 'drcooper@caltech.edu', 'ab23a88a3ed77ecdfeb894c0eaf2817a', 'Wed Apr 23 01:00:00 2014 -0700', 'a commit message', '', 1398065343, 1398065344);",
"insert into commits values (2, 2, 'Success', 1398065345, 1398069999, 854, '4e81eca185897c2d0cf81f5bc12623550c2ef952', 'master', '4', 'drcooper@caltech.edu', 'ab23a88a3ed77ecdfeb894c0eaf2817a', 'Wed Apr 23 01:01:00 2014 -0700', 'a commit message', '', 1398065343, 1398065344);",
"insert into commits values (3, 2, 'Success', 1398065345, 1398069999, 854, '7253f6545caed41fb8f5a6fcdb3abc0b81fa9dbf', 'master', '5', 'drcooper@caltech.edu', 'ab23a88a3ed77ecdfeb894c0eaf2817a', 'Wed Apr 23 01:02:38 2014 -0700', 'a commit message', '', 1398065343, 1398065344);",
"insert into commits values (4, 1, 'Success', 1398065345, 1398069999, 854, 'd12c9e5a11982f71796ad909c93551b16fba053e', 'dev', '', 'drcooper@caltech.edu', 'ab23a88a3ed77ecdfeb894c0eaf2817a', 'Wed Apr 23 02:00:00 2014 -0700', 'a commit message', '', 1398065343, 1398065344);",
"insert into commits values (5, 1, 'Started', 1398065345, 0, 0, '85f8c029b902ed9400bc600bac301a0aadb144ac', 'master', '', 'drcooper@caltech.edu', 'ab23a88a3ed77ecdfeb894c0eaf2817a', 'Wed Apr 23 03:00:00 2014 -0700', 'a commit message', '', 1398065343, 1398065344);",
// insert commit console output
"insert into output values (1, 1, 'sample console output');",
"insert into output values (2, 2, 'sample console output.....');",
// insert server entries
"insert into servers values (1, 'docker1', 'tcp://127.0.0.1:4243', 'root', 'pa55word', '/path/to/cert.key');",
"insert into servers values (2, 'docker2', 'tcp://127.0.0.1:4243', 'root', 'pa55word', '/path/to/cert.key');",
// insert remote entries
"insert into remotes values (1, 'enterprise.github.com', 'github.drone.io', 'https://github.drone.io', 'https://github.drone.io/v3/api', 'f0b461ca586c27872b43a0685cbc2847', '976f22a5eef7caacb7e678d6c52f49b1', '1');",
"insert into remotes values (2, 'github.com', 'github.com', 'https://github.io', 'https://api.github.com', 'a0b461ca586c27872b43a0685cbc2847', 'a76f22a5eef7caacb7e678d6c52f49b1', '0');",
}
// Load will populate the database with a fixed dataset for
// unit testing purposes.
func Load(db *sql.DB) {
// loop through insert statements and execute
for _, stmt := range stmts {
_, err := db.Exec(stmt)
if err != nil {
fmt.Printf("Error executing Query %s\n %s\n", stmt, err)
}
}
}

View file

@ -1,32 +0,0 @@
package testdatabase
import (
"database/sql"
"os"
// database drivers that may be tested
_ "github.com/mattn/go-sqlite3"
)
var (
driver = env("TEST_DB_DRIVER", "sqlite3")
source = env("TEST_DB_SOURCE", ":memory:")
)
// Open opens a new database connection using a test
// database environment, specified using the `$TEST_DB_DRIVER`
// and `$TEST_DB_SOURCE` environment variables.
func Open() (*sql.DB, error) {
return sql.Open(driver, source)
}
// helper function that retrieves the environment variable
// if exists, else returns a default value.
func env(name, def string) string {
value := os.Getenv(name)
if len(value) == 0 {
value = def
}
return value
}

View file

@ -1,127 +0,0 @@
package database
import (
"database/sql"
"time"
"github.com/drone/drone/shared/model"
"github.com/russross/meddler"
)
type UserManager interface {
// Find finds the User by ID.
Find(id int64) (*model.User, error)
// FindLogin finds the User by remote login.
FindLogin(remote, login string) (*model.User, error)
// FindToken finds the User by token.
FindToken(token string) (*model.User, error)
// List finds all registered users of the system.
List() ([]*model.User, error)
// Insert persists the User to the datastore.
Insert(user *model.User) error
// Update persists changes to the User to the datastore.
Update(user *model.User) error
// Delete removes the User from the datastore.
Delete(user *model.User) error
// Exist returns true if Users exist in the system.
Exist() bool
}
// userManager manages a list of users in a SQL database.
type userManager struct {
*sql.DB
}
// SQL query to retrieve a User by remote login.
const findUserLoginQuery = `
SELECT *
FROM users
WHERE user_remote=?
AND user_login=?
LIMIT 1
`
// SQL query to retrieve a User by remote login.
const findUserTokenQuery = `
SELECT *
FROM users
WHERE user_token=?
LIMIT 1
`
// SQL query to retrieve a list of all users.
const listUserQuery = `
SELECT *
FROM users
ORDER BY user_name ASC
`
// SQL statement to delete a User by ID.
const deleteUserStmt = `
DELETE FROM users WHERE user_id=?
`
// SQL statement to check if users exist.
const confirmUserStmt = `
select 1 from users limit 1
`
// NewUserManager initiales a new UserManager intended to
// manage and persist commits.
func NewUserManager(db *sql.DB) UserManager {
return &userManager{db}
}
func (db *userManager) Find(id int64) (*model.User, error) {
dst := model.User{}
err := meddler.Load(db, "users", &dst, id)
return &dst, err
}
func (db *userManager) FindLogin(remote, login string) (*model.User, error) {
dst := model.User{}
err := meddler.QueryRow(db, &dst, findUserLoginQuery, remote, login)
return &dst, err
}
func (db *userManager) FindToken(token string) (*model.User, error) {
dst := model.User{}
err := meddler.QueryRow(db, &dst, findUserTokenQuery, token)
return &dst, err
}
func (db *userManager) List() ([]*model.User, error) {
var dst []*model.User
err := meddler.QueryAll(db, &dst, listUserQuery)
return dst, err
}
func (db *userManager) Insert(user *model.User) error {
user.Created = time.Now().Unix()
user.Updated = time.Now().Unix()
return meddler.Insert(db, "users", user)
}
func (db *userManager) Update(user *model.User) error {
user.Updated = time.Now().Unix()
return meddler.Update(db, "users", user)
}
func (db *userManager) Delete(user *model.User) error {
_, err := db.Exec(deleteUserStmt, user.ID)
return err
}
func (db *userManager) Exist() bool {
row := db.QueryRow(confirmUserStmt)
var result int
row.Scan(&result)
return result == 1
}

View file

@ -1,235 +0,0 @@
package database
import (
"database/sql"
"testing"
"github.com/drone/drone/server/database/schema"
"github.com/drone/drone/server/database/testdata"
"github.com/drone/drone/server/database/testdatabase"
"github.com/drone/drone/shared/model"
)
// in-memory database instance for unit testing
var db *sql.DB
// setup the test database and test fixtures
func setup() {
db, _ = testdatabase.Open()
schema.Load(db)
testdata.Load(db)
}
// teardown the test database
func teardown() {
db.Close()
}
func TestUserFind(t *testing.T) {
setup()
defer teardown()
users := NewUserManager(db)
user, err := users.Find(1)
if err != nil {
t.Errorf("Want User from ID, got %s", err)
}
testUser(t, user)
}
func TestUserFindLogin(t *testing.T) {
setup()
defer teardown()
users := NewUserManager(db)
user, err := users.FindLogin("github.com", "smellypooper")
if err != nil {
t.Errorf("Want User from Login, got %s", err)
}
testUser(t, user)
}
func TestUserFindToken(t *testing.T) {
setup()
defer teardown()
users := NewUserManager(db)
user, err := users.FindToken("e42080dddf012c718e476da161d21ad5")
if err != nil {
t.Errorf("Want User from Token, got %s", err)
}
testUser(t, user)
}
func TestUserList(t *testing.T) {
setup()
defer teardown()
users := NewUserManager(db)
all, err := users.List()
if err != nil {
t.Errorf("Want Users, got %s", err)
}
var got, want = len(all), 4
if got != want {
t.Errorf("Want %v Users, got %v", want, got)
}
testUser(t, all[0])
}
func TestUserInsert(t *testing.T) {
setup()
defer teardown()
user := model.NewUser("github.com", "winkle", "winkle@caltech.edu")
users := NewUserManager(db)
if err := users.Insert(user); err != nil {
t.Errorf("Want User created, got %s", err)
}
var got, want = user.ID, int64(5)
if want != got {
t.Errorf("Want User ID %v, got %v", want, got)
}
// verify unique remote + remote login constraint
var err = users.Insert(&model.User{Remote: user.Remote, Login: user.Login, Token: "f71eb4a81a2cca56035dd7f6f2942e41"})
if err == nil {
t.Error("Want Token unique constraint violated")
}
// verify unique token constraint
err = users.Insert(&model.User{Remote: "gitlab.com", Login: user.Login, Token: user.Token})
if err == nil {
t.Error("Want Token unique constraint violated")
}
}
func TestUserUpdate(t *testing.T) {
setup()
defer teardown()
users := NewUserManager(db)
user, err := users.Find(4)
if err != nil {
t.Errorf("Want User from ID, got %s", err)
}
// update the user's access token
user.Access = "fc47f37716fa04e9dfa9ac7eb22b5718"
user.Secret = "d1c65427c978f2c9ad4baed72628dba0"
if err := users.Update(user); err != nil {
t.Errorf("Want User updated, got %s", err)
}
updated, _ := users.Find(4)
var got, want = updated.Access, user.Access
if got != want {
t.Errorf("Want updated Access %s, got %s", want, got)
}
got, want = updated.Secret, user.Secret
if got != want {
t.Errorf("Want updated Secret %s, got %s", want, got)
}
}
func TestUserDelete(t *testing.T) {
setup()
defer teardown()
users := NewUserManager(db)
user, err := users.Find(1)
if err != nil {
t.Errorf("Want User from ID, got %s", err)
}
// delete the user
if err := users.Delete(user); err != nil {
t.Errorf("Want User deleted, got %s", err)
}
// check to see if the deleted user is actually gone
if _, err := users.Find(1); err != sql.ErrNoRows {
t.Errorf("Want ErrNoRows, got %s", err)
}
}
// testUser is a helper function that compares the user
// to an expected set of fixed field values.
func testUser(t *testing.T, user *model.User) {
var got, want = user.Login, "smellypooper"
if got != want {
t.Errorf("Want Token %v, got %v", want, got)
}
got, want = user.Remote, "github.com"
if got != want {
t.Errorf("Want Token %v, got %v", want, got)
}
got, want = user.Access, "f0b461ca586c27872b43a0685cbc2847"
if got != want {
t.Errorf("Want Access Token %v, got %v", want, got)
}
got, want = user.Secret, "976f22a5eef7caacb7e678d6c52f49b1"
if got != want {
t.Errorf("Want Token Secret %v, got %v", want, got)
}
got, want = user.Name, "Dr. Cooper"
if got != want {
t.Errorf("Want Name %v, got %v", want, got)
}
got, want = user.Email, "drcooper@caltech.edu"
if got != want {
t.Errorf("Want Email %v, got %v", want, got)
}
got, want = user.Gravatar, "b9015b0857e16ac4d94a0ffd9a0b79c8"
if got != want {
t.Errorf("Want Gravatar %v, got %v", want, got)
}
got, want = user.Token, "e42080dddf012c718e476da161d21ad5"
if got != want {
t.Errorf("Want Token %v, got %v", want, got)
}
var gotBool, wantBool = user.Active, true
if gotBool != wantBool {
t.Errorf("Want Active %v, got %v", wantBool, gotBool)
}
gotBool, wantBool = user.Admin, true
if gotBool != wantBool {
t.Errorf("Want Admin %v, got %v", wantBool, gotBool)
}
var gotInt64, wantInt64 = user.ID, int64(1)
if gotInt64 != wantInt64 {
t.Errorf("Want ID %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = user.Created, int64(1398065343)
if gotInt64 != wantInt64 {
t.Errorf("Want Created %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = user.Updated, int64(1398065344)
if gotInt64 != wantInt64 {
t.Errorf("Want Updated %v, got %v", wantInt64, gotInt64)
}
gotInt64, wantInt64 = user.Synced, int64(1398065345)
if gotInt64 != wantInt64 {
t.Errorf("Want Synced %v, got %v", wantInt64, gotInt64)
}
}

View file

@ -72,8 +72,8 @@ func mustConnectTest() *sql.DB {
return db return db
} }
// New returns a new DataStore // New returns a new Datastore
func New(db *sql.DB) datastore.Datastore { func NewDatastore(db *sql.DB) datastore.Datastore {
return struct { return struct {
*Userstore *Userstore
*Permstore *Permstore

View file

@ -32,7 +32,7 @@ func GetPerm(c context.Context, user *model.User, repo *model.Repo) (*model.Perm
Read: false, Read: false,
Write: false, Write: false,
Admin: false}, nil Admin: false}, nil
case user == nil && !reop.Private: case user == nil && !repo.Private:
return &model.Perm{ return &model.Perm{
Guest: true, Guest: true,
Read: true, Read: true,

View file

@ -3,12 +3,12 @@ package handler
import ( import (
"encoding/xml" "encoding/xml"
"net/http" "net/http"
"time"
"github.com/drone/drone/server/database" "github.com/drone/drone/server/datastore"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/model" "github.com/drone/drone/shared/model"
"github.com/gorilla/pat" "github.com/goji/context"
"github.com/zenazn/goji/web"
) )
// badges that indicate the current build status for a repository // badges that indicate the current build status for a repository
@ -21,56 +21,40 @@ var (
badgeNone = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="75" height="18"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient><rect rx="4" width="75" height="18" fill="#555"/><rect rx="4" x="37" width="38" height="18" fill="#9f9f9f"/><path fill="#9f9f9f" d="M37 0h4v18h-4z"/><rect rx="4" width="75" height="18" 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="13" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="12">build</text><text x="55" y="13" fill="#010101" fill-opacity=".3">none</text><text x="55" y="12">none</text></g></svg>`) badgeNone = []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="75" height="18"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient><rect rx="4" width="75" height="18" fill="#555"/><rect rx="4" x="37" width="38" height="18" fill="#9f9f9f"/><path fill="#9f9f9f" d="M37 0h4v18h-4z"/><rect rx="4" width="75" height="18" 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="13" fill="#010101" fill-opacity=".3">build</text><text x="19.5" y="12">build</text><text x="55" y="13" fill="#010101" fill-opacity=".3">none</text><text x="55" y="12">none</text></g></svg>`)
) )
type BadgeHandler struct { // GetBadge accepts a request to retrieve the named
commits database.CommitManager // repo and branhes latest build details from the datastore
repos database.RepoManager // and return an SVG badges representing the build results.
} //
// GET /api/badge/:host/:owner/:name/status.svg
//
func GetBadge(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
var (
host = c.URLParams["host"]
owner = c.URLParams["owner"]
name = c.URLParams["name"]
branch = c.URLParams["branch"]
)
func NewBadgeHandler(repos database.RepoManager, commits database.CommitManager) *BadgeHandler { repo, err := datastore.GetRepoName(ctx, host, owner, name)
return &BadgeHandler{commits, repos}
}
// GetStatus gets the build status badge.
// GET /v1/badge/:host/:owner/:name/status.svg
func (h *BadgeHandler) GetStatus(w http.ResponseWriter, r *http.Request) error {
host, owner, name := parseRepo(r)
branch := r.FormValue("branch")
// github has insanely aggressive caching so we'll set almost
// every parameter possible to try to prevent caching.
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Add("Cache-Control", "no-cache")
w.Header().Add("Cache-Control", "no-store")
w.Header().Add("Cache-Control", "max-age=0")
w.Header().Add("Cache-Control", "must-revalidate")
w.Header().Add("Cache-Control", "value")
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
// get the repository from the database
arepo, err := h.repos.FindName(host, owner, name)
if err != nil { if err != nil {
w.Write(badgeNone) w.Write(badgeNone)
return nil return
} }
// if no branch, use the default
if len(branch) == 0 { if len(branch) == 0 {
branch = model.DefaultBranch branch = model.DefaultBranch
} }
commit, _ := datastore.GetCommitLast(ctx, repo, branch)
// get the latest commit
c, _ := h.commits.FindLatest(arepo.ID, branch)
// if no commit was found then display // if no commit was found then display
// the 'none' badge // the 'none' badge, instead of throwing
if c == nil { // an error response
if commit == nil {
w.Write(badgeNone) w.Write(badgeNone)
return nil return
} }
// determine which badge to load switch commit.Status {
switch c.Status {
case model.StatusSuccess: case model.StatusSuccess:
w.Write(badgeSuccess) w.Write(badgeSuccess)
case model.StatusFailure: case model.StatusFailure:
@ -82,40 +66,33 @@ func (h *BadgeHandler) GetStatus(w http.ResponseWriter, r *http.Request) error {
default: default:
w.Write(badgeNone) w.Write(badgeNone)
} }
return nil
} }
// GetCoverage gets the build status badge. // GetCC accepts a request to retrieve the latest build
// GET /v1/badges/:host/:owner/:name/coverage.svg // status for the given repository from the datastore and
func (h *BadgeHandler) GetCoverage(w http.ResponseWriter, r *http.Request) error { // in CCTray XML format.
return notImplemented{} //
} // GET /api/badge/:host/:owner/:name/cc.xml
//
func (h *BadgeHandler) GetCC(w http.ResponseWriter, r *http.Request) error { func GetCC(c web.C, w http.ResponseWriter, r *http.Request) {
host, owner, name := parseRepo(r) var ctx = context.FromC(c)
var (
// get the repository from the database host = c.URLParams["host"]
repo, err := h.repos.FindName(host, owner, name) owner = c.URLParams["owner"]
name = c.URLParams["name"]
)
repo, err := datastore.GetRepoName(ctx, host, owner, name)
if err != nil { if err != nil {
return notFound{err} w.Write(badgeNone)
return
} }
commits, err := datastore.GetCommitList(ctx, repo)
// get the latest commits for the repo
commits, err := h.commits.List(repo.ID)
if err != nil || len(commits) == 0 { if err != nil || len(commits) == 0 {
return notFound{} w.WriteHeader(http.StatusNotFound)
return
} }
commit := commits[0]
// generate the URL for the repository var link = httputil.GetURL(r) + "/" + repo.Host + "/" + repo.Owner + "/" + repo.Name
url := httputil.GetURL(r) + "/" + repo.Host + "/" + repo.Owner + "/" + repo.Name var cc = model.NewCC(repo, commits[0], link)
proj := model.NewCC(repo, commit, url) xml.NewEncoder(w).Encode(cc)
return xml.NewEncoder(w).Encode(proj)
}
func (h *BadgeHandler) Register(r *pat.Router) {
r.Get("/v1/badge/{host}/{owner}/{name}/coverage.svg", errorHandler(h.GetCoverage))
r.Get("/v1/badge/{host}/{owner}/{name}/status.svg", errorHandler(h.GetStatus))
r.Get("/v1/badge/{host}/{owner}/{name}/cc.xml", errorHandler(h.GetCC))
} }

View file

@ -4,139 +4,55 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/drone/drone/server/database" "github.com/drone/drone/server/datastore"
"github.com/drone/drone/server/session" "github.com/goji/context"
"github.com/drone/drone/shared/httputil" "github.com/zenazn/goji/web"
"github.com/drone/drone/shared/model"
"github.com/gorilla/pat"
) )
type CommitHandler struct { // GetCommitList accepts a request to retrieve a list
users database.UserManager // of recent commits by Repo, and retur in JSON format.
perms database.PermManager //
repos database.RepoManager // GET /api/repos/:host/:owner/:name/commits
commits database.CommitManager //
sess session.Session func GetCommitList(c web.C, w http.ResponseWriter, r *http.Request) {
queue chan *model.Request var ctx = context.FromC(c)
} var repo = ToRepo(c)
func NewCommitHandler(users database.UserManager, repos database.RepoManager, commits database.CommitManager, perms database.PermManager, sess session.Session, queue chan *model.Request) *CommitHandler { commits, err := datastore.GetCommitList(ctx, repo)
return &CommitHandler{users, perms, repos, commits, sess, queue}
}
// GetFeed gets recent commits for the repository and branch
// GET /v1/repos/{host}/{owner}/{name}/branches/{branch}/commits
func (h *CommitHandler) GetFeed(w http.ResponseWriter, r *http.Request) error {
var host, owner, name = parseRepo(r)
var branch = r.FormValue(":branch")
// get the user form the session.
user := h.sess.User(r)
// get the repository from the database.
repo, err := h.repos.FindName(host, owner, name)
switch {
case err != nil && user == nil:
return notAuthorized{}
case err != nil && user != nil:
return notFound{}
}
// user must have read access to the repository.
ok, _ := h.perms.Read(user, repo)
switch {
case ok == false && user == nil:
return notAuthorized{}
case ok == false && user != nil:
return notFound{}
}
commits, err := h.commits.ListBranch(repo.ID, branch)
if err != nil { if err != nil {
return notFound{err} w.WriteHeader(http.StatusNotFound)
return
} }
return json.NewEncoder(w).Encode(commits) json.NewEncoder(w).Encode(commits)
} }
// GetCommit gets the commit for the repository, branch and sha. // GetCommit accepts a request to retrieve a commit
// GET /v1/repos/{host}/{owner}/{name}/branches/{branch}/commits/{commit} // from the datastore for the given repository, branch and
func (h *CommitHandler) GetCommit(w http.ResponseWriter, r *http.Request) error { // commit hash.
var host, owner, name = parseRepo(r) //
var branch = r.FormValue(":branch") // GET /api/repos/:host/:owner/:name/branches/:branch/commits/:commit
var sha = r.FormValue(":commit") //
func GetCommit(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
var (
branch = c.URLParams["branch"]
hash = c.URLParams["commit"]
repo = ToRepo(c)
)
// get the user form the session. commit, err := datastore.GetCommitSha(ctx, repo, branch, hash)
user := h.sess.User(r)
// get the repository from the database.
repo, err := h.repos.FindName(host, owner, name)
switch {
case err != nil && user == nil:
return notAuthorized{}
case err != nil && user != nil:
return notFound{}
}
// user must have read access to the repository.
ok, _ := h.perms.Read(user, repo)
switch {
case ok == false && user == nil:
return notAuthorized{}
case ok == false && user != nil:
return notFound{}
}
commit, err := h.commits.FindSha(repo.ID, branch, sha)
if err != nil { if err != nil {
return notFound{err} w.WriteHeader(http.StatusNotFound)
return
} }
return json.NewEncoder(w).Encode(commit) json.NewEncoder(w).Encode(commit)
} }
// GetCommitOutput gets the commit's stdout. func PostCommit(c web.C, w http.ResponseWriter, r *http.Request) {}
// GET /v1/repos/{host}/{owner}/{name}/branches/{branch}/commits/{commit}/console
func (h *CommitHandler) GetCommitOutput(w http.ResponseWriter, r *http.Request) error {
var host, owner, name = parseRepo(r)
var branch = r.FormValue(":branch")
var sha = r.FormValue(":commit")
// get the user form the session.
user := h.sess.User(r)
// get the repository from the database.
repo, err := h.repos.FindName(host, owner, name)
switch {
case err != nil && user == nil:
return notAuthorized{}
case err != nil && user != nil:
return notFound{}
}
// user must have read access to the repository.
ok, _ := h.perms.Read(user, repo)
switch {
case ok == false && user == nil:
return notAuthorized{}
case ok == false && user != nil:
return notFound{}
}
commit, err := h.commits.FindSha(repo.ID, branch, sha)
if err != nil {
return notFound{err}
}
output, err := h.commits.FindOutput(commit.ID)
if err != nil {
return notFound{err}
}
w.Write(output)
return nil
}
/*
// PostCommit gets the commit for the repository and schedules to re-build. // PostCommit gets the commit for the repository and schedules to re-build.
// GET /v1/repos/{host}/{owner}/{name}/branches/{branch}/commits/{commit} // GET /v1/repos/{host}/{owner}/{name}/branches/{branch}/commits/{commit}
func (h *CommitHandler) PostCommit(w http.ResponseWriter, r *http.Request) error { func (h *CommitHandler) PostCommit(w http.ResponseWriter, r *http.Request) error {
@ -201,10 +117,4 @@ func (h *CommitHandler) PostCommit(w http.ResponseWriter, r *http.Request) error
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
return nil return nil
} }
*/
func (h *CommitHandler) Register(r *pat.Router) {
r.Get("/v1/repos/{host}/{owner}/{name}/branches/{branch}/commits/{commit}/console", errorHandler(h.GetCommitOutput))
r.Get("/v1/repos/{host}/{owner}/{name}/branches/{branch}/commits/{commit}", errorHandler(h.GetCommit))
r.Post("/v1/repos/{host}/{owner}/{name}/branches/{branch}/commits/{commit}", errorHandler(h.PostCommit)).Queries("action", "rebuild")
r.Get("/v1/repos/{host}/{owner}/{name}/branches/{branch}/commits", errorHandler(h.GetFeed))
}

51
server/handler/context.go Normal file
View file

@ -0,0 +1,51 @@
package handler
import (
"github.com/drone/drone/shared/model"
"github.com/zenazn/goji/web"
)
// ToUser returns the User from the current
// request context. If the User does not exist
// a nil value is returned.
func ToUser(c web.C) *model.User {
var v = c.Env["user"]
if v == nil {
return nil
}
u, ok := v.(*model.User)
if !ok {
return nil
}
return u
}
// ToRepo returns the Repo from the current
// request context. If the Repo does not exist
// a nil value is returned.
func ToRepo(c web.C) *model.Repo {
var v = c.Env["repo"]
if v == nil {
return nil
}
r, ok := v.(*model.Repo)
if !ok {
return nil
}
return r
}
// ToRole returns the Role from the current
// request context. If the Role does not exist
// a nil value is returned.
func ToRole(c web.C) *model.Perm {
var v = c.Env["role"]
if v == nil {
return nil
}
p, ok := v.(*model.Perm)
if !ok {
return nil
}
return p
}

View file

@ -1,59 +0,0 @@
package handler
import (
"log"
"net/http"
)
// badRequest is handled by setting the status code in the reply to StatusBadRequest.
type badRequest struct{ error }
// notFound is handled by setting the status code in the reply to StatusNotFound.
type notFound struct{ error }
// notAuthorized is handled by setting the status code in the reply to StatusNotAuthorized.
type notAuthorized struct{ error }
// notImplemented is handled by setting the status code in the reply to StatusNotImplemented.
type notImplemented struct{ error }
// forbidden is handled by setting the status code in the reply to StatusForbidden.
type forbidden struct{ error }
// internalServerError is handled by setting the status code in the reply to StatusInternalServerError.
type internalServerError struct{ error }
// errorHandler wraps a function returning an error by handling the error and returning a http.Handler.
// If the error is of the one of the types defined above, it is handled as described for every type.
// If the error is of another type, it is considered as an internal error and its message is logged.
func errorHandler(f func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// serve the request
err := f(w, r)
if err == nil {
return
}
// log the url for debugging purposes
log.Println(r.Method, r.URL.Path)
switch err.(type) {
case badRequest:
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
case notFound:
http.Error(w, "Not Found", http.StatusNotFound)
case notAuthorized:
http.Error(w, "Not Authorized", http.StatusUnauthorized)
case notImplemented:
http.Error(w, "Not Implemented", http.StatusForbidden)
case forbidden:
http.Error(w, "Forbidden", http.StatusForbidden)
case internalServerError:
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
default:
log.Println(err)
http.Error(w, "oops", http.StatusInternalServerError)
}
}
}

View file

@ -1,121 +1,3 @@
package handler package handler
import ( // PostHook
"net/http"
"strings"
"github.com/drone/drone/plugin/remote"
"github.com/drone/drone/server/database"
"github.com/drone/drone/shared/build/script"
"github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/model"
"github.com/gorilla/pat"
)
type HookHandler struct {
users database.UserManager
repos database.RepoManager
commits database.CommitManager
queue chan *model.Request
}
func NewHookHandler(users database.UserManager, repos database.RepoManager, commits database.CommitManager, queue chan *model.Request) *HookHandler {
return &HookHandler{users, repos, commits, queue}
}
// PostHook receives a post-commit hook from GitHub, Bitbucket, etc
// GET /hook/:host
func (h *HookHandler) PostHook(w http.ResponseWriter, r *http.Request) error {
var host = r.FormValue(":host")
var remote = remote.Lookup(host)
if remote == nil {
return notFound{}
}
// parse the hook payload
hook, err := remote.ParseHook(r)
if err != nil {
return badRequest{err}
}
// in some cases we have neither a hook nor error. An example
// would be GitHub sending a ping request to the URL, in which
// case we'll just exit quiely with an 'OK'
if hook == nil || strings.Contains(hook.Message, "[CI SKIP]") {
w.WriteHeader(http.StatusOK)
return nil
}
// fetch the repository from the database
repo, err := h.repos.FindName(remote.GetHost(), hook.Owner, hook.Repo)
if err != nil {
return notFound{}
}
if repo.Active == false ||
(repo.PostCommit == false && len(hook.PullRequest) == 0) ||
(repo.PullRequest == false && len(hook.PullRequest) != 0) {
w.WriteHeader(http.StatusOK)
return nil
}
// fetch the user from the database that owns this repo
user, err := h.users.Find(repo.UserID)
if err != nil {
return notFound{}
}
// featch the .drone.yml file from the database
yml, err := remote.GetScript(user, repo, hook)
if err != nil {
return badRequest{err}
}
// verify the commit hooks branch matches the list of approved
// branches (unless it is a pull request). Note that we don't really
// care if parsing the yaml fails here.
s, _ := script.ParseBuild(string(yml), map[string]string{})
if len(hook.PullRequest) == 0 && !s.MatchBranch(hook.Branch) {
w.WriteHeader(http.StatusOK)
return nil
}
c := model.Commit{
RepoID: repo.ID,
Status: model.StatusEnqueue,
Sha: hook.Sha,
Branch: hook.Branch,
PullRequest: hook.PullRequest,
Timestamp: hook.Timestamp,
Message: hook.Message,
Config: string(yml)}
c.SetAuthor(hook.Author)
// inser the commit into the database
if err := h.commits.Insert(&c); err != nil {
return badRequest{err}
}
//fmt.Printf("%s", yml)
owner, err := h.users.Find(repo.UserID)
if err != nil {
return badRequest{err}
}
// drop the items on the queue
go func() {
h.queue <- &model.Request{
User: owner,
Host: httputil.GetURL(r),
Repo: repo,
Commit: &c,
}
}()
w.WriteHeader(http.StatusOK)
return nil
}
func (h *HookHandler) Register(r *pat.Router) {
r.Post("/v1/hook/{host}", errorHandler(h.PostHook))
r.Put("/v1/hook/{host}", errorHandler(h.PostHook))
}

View file

@ -6,52 +6,53 @@ import (
"time" "time"
"github.com/drone/drone/plugin/remote" "github.com/drone/drone/plugin/remote"
"github.com/drone/drone/server/database" "github.com/drone/drone/server/capability"
"github.com/drone/drone/server/datastore"
"github.com/drone/drone/server/session" "github.com/drone/drone/server/session"
"github.com/drone/drone/shared/model" "github.com/drone/drone/shared/model"
"github.com/gorilla/pat" "github.com/goji/context"
"github.com/zenazn/goji/web"
) )
type LoginHandler struct { // GetLogin accepts a request to authorize the user and to
users database.UserManager // return a valid OAuth2 access token. The access token is
repos database.RepoManager // returned as url segment #access_token
perms database.PermManager //
sess session.Session // GET /login/:host
open bool //
} func GetLogin(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
func NewLoginHandler(users database.UserManager, repos database.RepoManager, perms database.PermManager, sess session.Session, open bool) *LoginHandler { var host = c.URLParams["host"]
return &LoginHandler{users, repos, perms, sess, open}
}
// GetLogin gets the login to the 3rd party remote system.
// GET /login/:host
func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error {
var host = r.FormValue(":host")
var redirect = "/" var redirect = "/"
var remote = remote.Lookup(host) var remote = remote.Lookup(host)
if remote == nil { if remote == nil {
return notFound{} w.WriteHeader(http.StatusNotFound)
return
} }
// authenticate the user // authenticate the user
login, err := remote.Authorize(w, r) login, err := remote.Authorize(w, r)
if err != nil { if err != nil {
return badRequest{err} w.WriteHeader(http.StatusBadRequest)
return
} else if login == nil { } else if login == nil {
// in this case we probably just redirected // in this case we probably just redirected
// the user, so we can exit with no error // the user, so we can exit with no error
return nil return
} }
// get the user from the database // get the user from the database
u, err := h.users.FindLogin(host, login.Login) u, err := datastore.GetUserLogin(ctx, host, login.Login)
if err != nil { if err != nil {
// if self-registration is disabled we should // if self-registration is disabled we should
// return a notAuthorized error. the only exception // return a notAuthorized error. the only exception
// is if no users exist yet in the system we'll proceed. // is if no users exist yet in the system we'll proceed.
if h.open == false && h.users.Exist() { if capability.Enabled(ctx, capability.Registration) == false {
return notAuthorized{} users, err := datastore.GetUserList(ctx)
if err != nil || len(users) != 0 {
w.WriteHeader(http.StatusForbidden)
return
}
} }
// create the user account // create the user account
@ -60,8 +61,9 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error {
u.SetEmail(login.Email) u.SetEmail(login.Email)
// insert the user into the database // insert the user into the database
if err := h.users.Insert(u); err != nil { if err := datastore.PostUser(ctx, u); err != nil {
return badRequest{err} w.WriteHeader(http.StatusBadRequest)
return
} }
// if this is the first user, they // if this is the first user, they
@ -78,8 +80,9 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error {
u.Name = login.Name u.Name = login.Name
u.SetEmail(login.Email) u.SetEmail(login.Email)
u.Syncing = true //u.IsStale() // todo (badrydzewski) should not always sync u.Syncing = true //u.IsStale() // todo (badrydzewski) should not always sync
if err := h.users.Update(u); err != nil { if err := datastore.PutUser(ctx, u); err != nil {
return badRequest{err} w.WriteHeader(http.StatusBadRequest)
return
} }
// look at the last synchronized date to determine if // look at the last synchronized date to determine if
@ -109,10 +112,10 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error {
// insert all repositories // insert all repositories
for _, repo := range repos { for _, repo := range repos {
var role = repo.Role var role = repo.Role
if err := h.repos.Insert(repo); err != nil { if err := datastore.PostRepo(ctx, repo); err != nil {
// typically we see a failure because the repository already exists // typically we see a failure because the repository already exists
// in which case, we can retrieve the existing record to get the ID. // in which case, we can retrieve the existing record to get the ID.
repo, err = h.repos.FindName(repo.Host, repo.Owner, repo.Name) repo, err = datastore.GetRepoName(ctx, repo.Host, repo.Owner, repo.Name)
if err != nil { if err != nil {
log.Println("Error adding repo.", u.Login, repo.Name, err) log.Println("Error adding repo.", u.Login, repo.Name, err)
continue continue
@ -120,7 +123,14 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error {
} }
// add user permissions // add user permissions
if err := h.perms.Grant(u, repo, role.Read, role.Write, role.Admin); err != nil { perm := model.Perm{
UserID: u.ID,
RepoID: repo.ID,
Read: role.Read,
Write: role.Write,
Admin: role.Admin,
}
if err := datastore.PostPerm(ctx, &perm); err != nil {
log.Println("Error adding permissions.", u.Login, repo.Name, err) log.Println("Error adding permissions.", u.Login, repo.Name, err)
continue continue
} }
@ -130,31 +140,19 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error {
u.Synced = time.Now().UTC().Unix() u.Synced = time.Now().UTC().Unix()
u.Syncing = false u.Syncing = false
if err := h.users.Update(u); err != nil { if err := datastore.PutUser(ctx, u); err != nil {
log.Println("Error syncing user account, updating sync date", u.Login, err) log.Println("Error syncing user account, updating sync date", u.Login, err)
return return
} }
}() }()
} }
// (re)-create the user session token, err := session.GenerateToken(ctx, r, u)
h.sess.SetUser(w, r, u) if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
redirect = redirect + "#access_token=" + token
// redirect the user to their dashboard
http.Redirect(w, r, redirect, http.StatusSeeOther) http.Redirect(w, r, redirect, http.StatusSeeOther)
return nil
}
// GetLogout terminates the current user session
// GET /logout
func (h *LoginHandler) GetLogout(w http.ResponseWriter, r *http.Request) error {
h.sess.Clear(w, r)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return nil
}
func (h *LoginHandler) Register(r *pat.Router) {
r.Get("/login/{host}", errorHandler(h.GetLogin))
r.Post("/login/{host}", errorHandler(h.GetLogin))
r.Get("/logout", errorHandler(h.GetLogout))
} }

36
server/handler/output.go Normal file
View file

@ -0,0 +1,36 @@
package handler
import (
"io"
"net/http"
"path/filepath"
"github.com/drone/drone/server/blobstore"
"github.com/goji/context"
"github.com/zenazn/goji/web"
)
// GetOutput gets the commit's stdout.
//
// GET /api/repos/:host/:owner/:name/branches/:branch/commits/:commit/console
//
func GetOutput(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
var (
host = c.URLParams["host"]
owner = c.URLParams["owner"]
name = c.URLParams["name"]
branch = c.URLParams["branch"]
hash = c.URLParams["commit"]
)
path := filepath.Join(host, owner, name, branch, hash)
rc, err := blobstore.GetReader(ctx, path)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
defer rc.Close()
io.Copy(w, rc)
}

View file

@ -6,96 +6,73 @@ import (
"net/http" "net/http"
"github.com/drone/drone/plugin/remote" "github.com/drone/drone/plugin/remote"
"github.com/drone/drone/server/database" "github.com/drone/drone/server/datastore"
"github.com/drone/drone/server/session"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/model" "github.com/drone/drone/shared/model"
"github.com/drone/drone/shared/sshutil" "github.com/drone/drone/shared/sshutil"
"github.com/gorilla/pat" "github.com/goji/context"
"github.com/zenazn/goji/web"
) )
type RepoHandler struct { // GetRepo accepts a request to retrieve a commit
commits database.CommitManager // from the datastore for the given repository, branch and
perms database.PermManager // commit hash.
repos database.RepoManager //
sess session.Session // GET /api/repos/:host/:owner/:name
} //
func GetRepo(c web.C, w http.ResponseWriter, r *http.Request) {
var (
admin = r.FormValue("admin")
role = ToRole(c)
repo = ToRepo(c)
)
func NewRepoHandler(repos database.RepoManager, commits database.CommitManager, // if the user is not requesting (or cannot access)
perms database.PermManager, sess session.Session) *RepoHandler { // admin data then we just return the repo as-is
return &RepoHandler{commits, perms, repos, sess} if len(admin) == 0 || role.Admin == false {
} json.NewEncoder(w).Encode(repo)
return
// GetRepo gets the named repository.
// GET /v1/repos/:host/:owner/:name
func (h *RepoHandler) GetRepo(w http.ResponseWriter, r *http.Request) error {
var host, owner, name = parseRepo(r)
var admin = r.FormValue("admin")
// get the user form the session.
user := h.sess.User(r)
// get the repository from the database.
repo, err := h.repos.FindName(host, owner, name)
switch {
case err != nil && user == nil:
return notAuthorized{}
case err != nil && user != nil:
return notFound{}
} }
// user must have read access to the repository. // else we should return restricted fields
repo.Role = h.perms.Find(user, repo) json.NewEncoder(w).Encode(struct {
switch {
case repo.Role.Read == false && user == nil:
return notAuthorized{}
case repo.Role.Read == false && user != nil:
return notFound{}
}
// if the user is not requesting admin data we can
// return exactly what we have.
if len(admin) == 0 {
return json.NewEncoder(w).Encode(repo)
}
// ammend the response to include data that otherwise
// would be excluded from json serialization, assuming
// the user is actually an admin of the repo.
if ok, _ := h.perms.Admin(user, repo); !ok {
return notFound{err}
}
return json.NewEncoder(w).Encode(struct {
*model.Repo *model.Repo
PublicKey string `json:"public_key"` PublicKey string `json:"public_key"`
Params string `json:"params"` Params string `json:"params"`
}{repo, repo.PublicKey, repo.Params}) }{repo, repo.PublicKey, repo.Params})
} }
// PostRepo activates the named repository. // DelRepo accepts a request to inactivate the named
// POST /v1/repos/:host/:owner/:name // repository. This will disable all builds in the system
func (h *RepoHandler) PostRepo(w http.ResponseWriter, r *http.Request) error { // for this repository.
var host, owner, name = parseRepo(r) //
// DEL /api/repos/:host/:owner/:name
//
func DelRepo(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
var repo = ToRepo(c)
// get the user form the session. // disable everything
user := h.sess.User(r) repo.Active = false
if user == nil { repo.PullRequest = false
return notAuthorized{} repo.PostCommit = false
}
// get the repo from the database if err := datastore.PutRepo(ctx, repo); err != nil {
repo, err := h.repos.FindName(host, owner, name) w.WriteHeader(http.StatusInternalServerError)
switch { return
case err != nil && user == nil:
return notAuthorized{}
case err != nil && user != nil:
return notFound{}
} }
w.WriteHeader(http.StatusNoContent)
}
// user must have admin access to the repository. // PostRepo accapets a request to activate the named repository
if ok, _ := h.perms.Admin(user, repo); !ok { // in the datastore. It returns a 201 status created if successful
return notFound{err} //
} // POST /api/repos/:host/:owner/:name
//
func PostRepo(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
var repo = ToRepo(c)
var user = ToUser(c)
// update the repo active flag and fields // update the repo active flag and fields
repo.Active = true repo.Active = true
@ -104,62 +81,47 @@ func (h *RepoHandler) PostRepo(w http.ResponseWriter, r *http.Request) error {
repo.UserID = user.ID repo.UserID = user.ID
repo.Timeout = 3600 // default to 1 hour repo.Timeout = 3600 // default to 1 hour
// generate the rsa key // generates the rsa key
key, err := sshutil.GeneratePrivateKey() key, err := sshutil.GeneratePrivateKey()
if err != nil { if err != nil {
return internalServerError{err} w.WriteHeader(http.StatusInternalServerError)
return
} }
// marshal the public and private key values
repo.PublicKey = sshutil.MarshalPublicKey(&key.PublicKey) repo.PublicKey = sshutil.MarshalPublicKey(&key.PublicKey)
repo.PrivateKey = sshutil.MarshalPrivateKey(key) repo.PrivateKey = sshutil.MarshalPrivateKey(key)
var remote = remote.Lookup(host) var remote = remote.Lookup(repo.Host)
if remote == nil { if remote == nil {
return notFound{} w.WriteHeader(http.StatusNotFound)
return
} }
// post commit hook url // setup the post-commit hook with the remote system and
hook := fmt.Sprintf("%s://%s/v1/hook/%s", httputil.GetScheme(r), httputil.GetHost(r), remote.GetKind()) // if necessary, register the public key
var hook = fmt.Sprintf("%s/v1/hook/%s", httputil.GetURL(r), repo.Remote)
// activate the repository in the remote system
if err := remote.Activate(user, repo, hook); err != nil { if err := remote.Activate(user, repo, hook); err != nil {
return badRequest{err} w.WriteHeader(http.StatusInternalServerError)
return
} }
// update the status in the database if err := datastore.PutRepo(ctx, repo); err != nil {
if err := h.repos.Update(repo); err != nil { w.WriteHeader(http.StatusInternalServerError)
return badRequest{err} return
} }
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
return json.NewEncoder(w).Encode(repo) json.NewEncoder(w).Encode(repo)
} }
// PutRepo updates the named repository. // PutRepo accapets a request to update the named repository
// PUT /v1/repos/:host/:owner/:name // in the datastore. It expects a JSON input and returns the
func (h *RepoHandler) PutRepo(w http.ResponseWriter, r *http.Request) error { // updated repository in JSON format if successful.
var host, owner, name = parseRepo(r) //
// PUT /api/repos/:host/:owner/:name
// get the user form the session. //
user := h.sess.User(r) func PutRepo(c web.C, w http.ResponseWriter, r *http.Request) {
if user == nil { var ctx = context.FromC(c)
return notAuthorized{} var repo = ToRepo(c)
} var user = ToUser(c)
// get the repo from the database
repo, err := h.repos.FindName(host, owner, name)
switch {
case err != nil && user == nil:
return notAuthorized{}
case err != nil && user != nil:
return notFound{}
}
// user must have admin access to the repository.
if ok, _ := h.perms.Admin(user, repo); !ok {
return notFound{err}
}
// unmarshal the repository from the payload // unmarshal the repository from the payload
defer r.Body.Close() defer r.Body.Close()
@ -173,28 +135,22 @@ func (h *RepoHandler) PutRepo(w http.ResponseWriter, r *http.Request) error {
PrivateKey *string `json:"private_key"` PrivateKey *string `json:"private_key"`
}{} }{}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil { if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
return badRequest{err} w.WriteHeader(http.StatusBadRequest)
return
} }
// update the private/secure parameters
if in.Params != nil { if in.Params != nil {
repo.Params = *in.Params repo.Params = *in.Params
} }
// update the post commit flag
if in.PostCommit != nil { if in.PostCommit != nil {
repo.PostCommit = *in.PostCommit repo.PostCommit = *in.PostCommit
} }
// update the pull request flag
if in.PullRequest != nil { if in.PullRequest != nil {
repo.PullRequest = *in.PullRequest repo.PullRequest = *in.PullRequest
} }
// update the privileged flag. This can only be updated by
// the system administrator
if in.Privileged != nil && user.Admin { if in.Privileged != nil && user.Admin {
repo.Privileged = *in.Privileged repo.Privileged = *in.Privileged
} }
// update the timeout. This can only be updated by
// the system administrator
if in.Timeout != nil && user.Admin { if in.Timeout != nil && user.Admin {
repo.Timeout = *in.Timeout repo.Timeout = *in.Timeout
} }
@ -202,93 +158,9 @@ func (h *RepoHandler) PutRepo(w http.ResponseWriter, r *http.Request) error {
repo.PublicKey = *in.PublicKey repo.PublicKey = *in.PublicKey
repo.PrivateKey = *in.PrivateKey repo.PrivateKey = *in.PrivateKey
} }
if err := datastore.PutRepo(ctx, repo); err != nil {
// update the repository w.WriteHeader(http.StatusInternalServerError)
if err := h.repos.Update(repo); err != nil { return
return badRequest{err}
} }
json.NewEncoder(w).Encode(repo)
return json.NewEncoder(w).Encode(repo)
}
// DeleteRepo deletes the named repository.
// DEL /v1/repos/:host/:owner/:name
func (h *RepoHandler) DeleteRepo(w http.ResponseWriter, r *http.Request) error {
var host, owner, name = parseRepo(r)
// get the user form the session.
user := h.sess.User(r)
if user == nil {
return notAuthorized{}
}
// get the repo from the database
repo, err := h.repos.FindName(host, owner, name)
switch {
case err != nil && user == nil:
return notAuthorized{}
case err != nil && user != nil:
return notFound{}
}
// user must have admin access to the repository.
if ok, _ := h.perms.Admin(user, repo); !ok {
return notFound{err}
}
// update the repo active flag and fields.
repo.Active = false
repo.PullRequest = false
repo.PostCommit = false
// insert the new repository
if err := h.repos.Update(repo); err != nil {
return badRequest{err}
}
w.WriteHeader(http.StatusNoContent)
return nil
}
// GetFeed gets the most recent commits across all branches
// GET /v1/repos/{host}/{owner}/{name}/feed
func (h *RepoHandler) GetFeed(w http.ResponseWriter, r *http.Request) error {
var host, owner, name = parseRepo(r)
// get the user form the session.
user := h.sess.User(r)
// get the repository from the database.
repo, err := h.repos.FindName(host, owner, name)
switch {
case err != nil && user == nil:
return notAuthorized{}
case err != nil && user != nil:
return notFound{}
}
// user must have read access to the repository.
ok, _ := h.perms.Read(user, repo)
switch {
case ok == false && user == nil:
return notAuthorized{}
case ok == false && user != nil:
return notFound{}
}
// lists the most recent commits across all branches.
commits, err := h.commits.List(repo.ID)
if err != nil {
return notFound{err}
}
return json.NewEncoder(w).Encode(commits)
}
func (h *RepoHandler) Register(r *pat.Router) {
r.Get("/v1/repos/{host}/{owner}/{name}/feed", errorHandler(h.GetFeed))
r.Get("/v1/repos/{host}/{owner}/{name}", errorHandler(h.GetRepo))
r.Put("/v1/repos/{host}/{owner}/{name}", errorHandler(h.PutRepo))
r.Post("/v1/repos/{host}/{owner}/{name}", errorHandler(h.PostRepo))
r.Delete("/v1/repos/{host}/{owner}/{name}", errorHandler(h.DeleteRepo))
} }

View file

@ -4,112 +4,110 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/drone/drone/server/database" "github.com/drone/drone/server/datastore"
"github.com/drone/drone/server/session"
"github.com/drone/drone/shared/model" "github.com/drone/drone/shared/model"
"github.com/gorilla/pat" "github.com/goji/context"
"github.com/zenazn/goji/web"
) )
type UserHandler struct { // GetUserCurrent accepts a request to retrieve the
commits database.CommitManager // currently authenticated user from the datastore
repos database.RepoManager // and return in JSON format.
users database.UserManager //
sess session.Session // GET /api/user
} //
func GetUserCurrent(c web.C, w http.ResponseWriter, r *http.Request) {
func NewUserHandler(users database.UserManager, repos database.RepoManager, commits database.CommitManager, sess session.Session) *UserHandler { var user = ToUser(c)
return &UserHandler{commits, repos, users, sess} if user == nil {
} w.WriteHeader(http.StatusUnauthorized)
return
// GetUser gets the authenticated user.
// GET /api/user
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) error {
// get the user form the session
u := h.sess.User(r)
if u == nil {
return notAuthorized{}
} }
// Normally the Token would not be serialized to json. // return private data for the currently authenticated
// In this case it is appropriate because the user is // user, specifically, their auth token.
// requesting their own data, and will need to display
// the Token on the website.
data := struct { data := struct {
*model.User *model.User
Token string `json:"token"` Token string `json:"token"`
}{u, u.Token} }{user, user.Token}
return json.NewEncoder(w).Encode(&data) json.NewEncoder(w).Encode(&data)
} }
// PutUser updates the authenticated user. // PutUser accepts a request to update the currently
// PUT /api/user // authenticated User profile.
func (h *UserHandler) PutUser(w http.ResponseWriter, r *http.Request) error { //
// get the user form the session // PUT /api/user
u := h.sess.User(r) //
if u == nil { func PutUser(c web.C, w http.ResponseWriter, r *http.Request) {
return notAuthorized{} var ctx = context.FromC(c)
var user = ToUser(c)
if user == nil {
w.WriteHeader(http.StatusUnauthorized)
return
} }
// unmarshal the repository from the payload // unmarshal the repository from the payload
defer r.Body.Close() defer r.Body.Close()
in := model.User{} in := model.User{}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil { if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
return badRequest{err} w.WriteHeader(http.StatusBadRequest)
return
} }
// update the user email // update the user email
if len(in.Email) != 0 { if len(in.Email) != 0 {
u.SetEmail(in.Email) user.SetEmail(in.Email)
} }
// update the user full name // update the user full name
if len(in.Name) != 0 { if len(in.Name) != 0 {
u.Name = in.Name user.Name = in.Name
} }
// update the database // update the database
if err := h.users.Update(u); err != nil { if err := datastore.PutUser(ctx, user); err != nil {
return internalServerError{err} w.WriteHeader(http.StatusInternalServerError)
return
} }
return json.NewEncoder(w).Encode(u) json.NewEncoder(w).Encode(user)
} }
// GetRepos gets the authenticated user's repositories. // GetRepos accepts a request to get the currently
// GET /api/user/repos // authenticated user's repository list from the datastore,
func (h *UserHandler) GetRepos(w http.ResponseWriter, r *http.Request) error { // encoded and returned in JSON format.
// get the user from the session //
u := h.sess.User(r) // GET /api/user/repos
if u == nil { //
return notAuthorized{} func GetUserRepos(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
var user = ToUser(c)
if user == nil {
w.WriteHeader(http.StatusUnauthorized)
return
} }
repos, err := datastore.GetRepoList(ctx, user)
// get the user repositories
repos, err := h.repos.List(u.ID)
if err != nil { if err != nil {
return badRequest{err} w.WriteHeader(http.StatusNotFound)
return
} }
return json.NewEncoder(w).Encode(&repos) json.NewEncoder(w).Encode(&repos)
} }
// GetFeed gets the authenticated user's commit feed. // GetUserFeed accepts a request to get the user's latest
// GET /api/user/feed // build feed, across all repositories, from the datastore.
func (h *UserHandler) GetFeed(w http.ResponseWriter, r *http.Request) error { // The results are encoded and returned in JSON format.
// get the user from the session //
u := h.sess.User(r) // GET /api/user/feed
if u == nil { //
return notAuthorized{} func GetUserFeed(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
var user = ToUser(c)
if user == nil {
w.WriteHeader(http.StatusUnauthorized)
return
} }
repos, err := datastore.GetCommitListUser(ctx, user)
// get the user commits
commits, err := h.commits.ListUser(u.ID)
if err != nil { if err != nil {
return badRequest{err} w.WriteHeader(http.StatusNotFound)
return
} }
return json.NewEncoder(w).Encode(&commits) json.NewEncoder(w).Encode(&repos)
}
func (h *UserHandler) Register(r *pat.Router) {
r.Get("/v1/user/repos", errorHandler(h.GetRepos))
r.Get("/v1/user/feed", errorHandler(h.GetFeed))
r.Get("/v1/user", errorHandler(h.GetUser))
r.Put("/v1/user", errorHandler(h.PutUser))
} }

View file

@ -4,124 +4,127 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/drone/drone/server/database" "github.com/drone/drone/server/datastore"
"github.com/drone/drone/server/session"
"github.com/drone/drone/shared/model" "github.com/drone/drone/shared/model"
"github.com/gorilla/pat" "github.com/goji/context"
"github.com/zenazn/goji/web"
) )
type UsersHandler struct { // GetUsers accepts a request to retrieve all users
users database.UserManager // from the datastore and return encoded in JSON format.
sess session.Session
}
func NewUsersHandler(users database.UserManager, sess session.Session) *UsersHandler {
return &UsersHandler{users, sess}
}
// GetUsers gets all users.
// GET /api/users
func (h *UsersHandler) GetUsers(w http.ResponseWriter, r *http.Request) error {
// get the user form the session
user := h.sess.User(r)
switch {
case user == nil:
return notAuthorized{}
case user.Admin == false:
return forbidden{}
}
// get all users
users, err := h.users.List()
if err != nil {
return internalServerError{err}
}
return json.NewEncoder(w).Encode(users)
}
// GetUser gets a user by hostname and login.
// GET /api/users/:host/:login
func (h *UsersHandler) GetUser(w http.ResponseWriter, r *http.Request) error {
remote := r.FormValue(":host")
login := r.FormValue(":login")
// get the user form the session
user := h.sess.User(r)
switch {
case user == nil:
return notAuthorized{}
case user.Admin == false:
return forbidden{}
}
user, err := h.users.FindLogin(remote, login)
if err != nil {
return notFound{err}
}
return json.NewEncoder(w).Encode(user)
}
// PostUser registers a new user account.
// POST /api/users/:host/:login
func (h *UsersHandler) PostUser(w http.ResponseWriter, r *http.Request) error {
remote := r.FormValue(":host")
login := r.FormValue(":login")
// get the user form the session
user := h.sess.User(r)
switch {
case user == nil:
return notAuthorized{}
case user.Admin == false:
return forbidden{}
}
account := model.NewUser(remote, login, "")
if err := h.users.Insert(account); err != nil {
return badRequest{err}
}
return json.NewEncoder(w).Encode(account)
}
// DeleteUser gets a user by hostname and login and deletes
// from the system.
// //
// DELETE /api/users/:host/:login // GET /api/users
func (h *UsersHandler) DeleteUser(w http.ResponseWriter, r *http.Request) error { //
remote := r.FormValue(":host") func GetUserList(c web.C, w http.ResponseWriter, r *http.Request) {
login := r.FormValue(":login") var ctx = context.FromC(c)
var user = ToUser(c)
// get the user form the session
user := h.sess.User(r)
switch { switch {
case user == nil: case user == nil:
return notAuthorized{} w.WriteHeader(http.StatusUnauthorized)
return
case user.Admin == false: case user.Admin == false:
return forbidden{} w.WriteHeader(http.StatusForbidden)
return
} }
account, err := h.users.FindLogin(remote, login) users, err := datastore.GetUserList(ctx)
if err != nil { if err != nil {
return notFound{err} w.WriteHeader(http.StatusInternalServerError)
return
} }
json.NewEncoder(w).Encode(users)
}
// user cannot delete his / her own account // GetUser accepts a request to retrieve a user by hostname
// and login from the datastore and return encoded in JSON
// format.
//
// GET /api/users/:host/:login
//
func GetUser(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
var (
user = ToUser(c)
host = c.URLParams["host"]
login = c.URLParams["login"]
)
switch {
case user == nil:
w.WriteHeader(http.StatusUnauthorized)
return
case user.Admin == false:
w.WriteHeader(http.StatusForbidden)
return
}
user, err := datastore.GetUserLogin(ctx, host, login)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(user)
}
// 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/:host/:login
//
func PostUser(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
var (
user = ToUser(c)
host = c.URLParams["host"]
login = c.URLParams["login"]
)
switch {
case user == nil:
w.WriteHeader(http.StatusUnauthorized)
return
case user.Admin == false:
w.WriteHeader(http.StatusForbidden)
return
}
account := model.NewUser(host, login, "")
if err := datastore.PostUser(ctx, account); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
json.NewEncoder(w).Encode(account)
}
// 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/:host/:login
//
func DelUser(c web.C, w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(c)
var (
user = ToUser(c)
host = c.URLParams["host"]
login = c.URLParams["login"]
)
switch {
case user == nil:
w.WriteHeader(http.StatusUnauthorized)
return
case user.Admin == false:
w.WriteHeader(http.StatusForbidden)
return
}
account, err := datastore.GetUserLogin(ctx, host, login)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
if account.ID == user.ID { if account.ID == user.ID {
return badRequest{} w.WriteHeader(http.StatusBadRequest)
return
} }
if err := datastore.DelUser(ctx, account); err != nil {
if err := h.users.Delete(account); err != nil { w.WriteHeader(http.StatusInternalServerError)
return badRequest{err} return
} }
// return a 200 indicating deletion complete
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
return nil
}
func (h *UsersHandler) Register(r *pat.Router) {
r.Delete("/v1/users/{host}/{login}", errorHandler(h.DeleteUser))
r.Post("/v1/users/{host}/{login}", errorHandler(h.PostUser))
r.Get("/v1/users/{host}/{login}", errorHandler(h.GetUser))
r.Get("/v1/users", errorHandler(h.GetUsers))
} }

View file

@ -1,20 +0,0 @@
package handler
import (
"net/http"
)
func parseRepo(r *http.Request) (host string, owner string, name string) {
host = r.FormValue(":host")
owner = r.FormValue(":owner")
name = r.FormValue(":name")
return
}
func parseBranch(r *http.Request) (branch string) {
return r.FormValue(":branch")
}
func parseCommit(r *http.Request) (commit string) {
return r.FormValue(":commit")
}

View file

@ -1,204 +1 @@
package handler package handler
import (
"log"
"net/http"
"strconv"
"time"
"github.com/drone/drone/server/database"
"github.com/drone/drone/server/pubsub"
"github.com/drone/drone/server/session"
"github.com/drone/drone/shared/model"
"github.com/gorilla/pat"
"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,
}
type WsHandler struct {
pubsub *pubsub.PubSub
commits database.CommitManager
perms database.PermManager
repos database.RepoManager
sess session.Session
}
func NewWsHandler(repos database.RepoManager, commits database.CommitManager, perms database.PermManager, sess session.Session, pubsub *pubsub.PubSub) *WsHandler {
return &WsHandler{pubsub, commits, perms, repos, sess}
}
// WsUser will upgrade the connection to a Websocket and will stream
// all events to the browser pertinent to the authenticated user. If the user
// is not authenticated, only public events are streamed.
func (h *WsHandler) WsUser(w http.ResponseWriter, r *http.Request) error {
// get the user form the session
user := h.sess.UserCookie(r)
// upgrade the websocket
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return badRequest{err}
}
// register a channel for global events
channel := h.pubsub.Register("_global")
sub := channel.Subscribe()
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
sub.Close()
ws.Close()
}()
go func() {
for {
select {
case msg := <-sub.Read():
work, ok := msg.(*model.Request)
if !ok {
break
}
// user must have read access to the repository
// in order to pass this message along
if role := h.perms.Find(user, work.Repo); !role.Read {
break
}
ws.SetWriteDeadline(time.Now().Add(writeWait))
err := ws.WriteJSON(work)
if err != nil {
ws.Close()
return
}
case <-sub.CloseNotify():
ws.Close()
return
case <-ticker.C:
ws.SetWriteDeadline(time.Now().Add(writeWait))
err := ws.WriteMessage(websocket.PingMessage, []byte{})
if err != nil {
ws.Close()
return
}
}
}
}()
readWebsocket(ws)
return nil
}
// WsConsole will upgrade the connection to a Websocket and will stream
// the build output to the browser.
func (h *WsHandler) WsConsole(w http.ResponseWriter, r *http.Request) error {
var commitID, _ = strconv.Atoi(r.FormValue(":id"))
commit, err := h.commits.Find(int64(commitID))
if err != nil {
return notFound{err}
}
repo, err := h.repos.Find(commit.RepoID)
if err != nil {
return notFound{err}
}
user := h.sess.UserCookie(r)
if ok, _ := h.perms.Read(user, repo); !ok {
return notFound{err}
}
// find a channel that we can subscribe to
// and listen for stream updates.
channel := h.pubsub.Lookup(commit.ID)
if channel == nil {
return notFound{}
}
sub := channel.Subscribe()
defer sub.Close()
// upgrade the websocket
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return badRequest{err}
}
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
ws.Close()
}()
go func() {
for {
select {
case msg := <-sub.Read():
data, ok := msg.([]byte)
if !ok {
break
}
ws.SetWriteDeadline(time.Now().Add(writeWait))
err := ws.WriteMessage(websocket.TextMessage, data)
if err != nil {
log.Printf("websocket for commit %d closed. Err: %s\n", commitID, err)
ws.Close()
return
}
case <-sub.CloseNotify():
log.Printf("websocket for commit %d closed by client\n", commitID)
ws.Close()
return
case <-ticker.C:
ws.SetWriteDeadline(time.Now().Add(writeWait))
err := ws.WriteMessage(websocket.PingMessage, []byte{})
if err != nil {
log.Printf("websocket for commit %d closed. Err: %s\n", commitID, err)
ws.Close()
return
}
}
}
}()
readWebsocket(ws)
return nil
}
// 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
}
}
}
func (h *WsHandler) Register(r *pat.Router) {
r.Get("/ws/user", errorHandler(h.WsUser))
r.Get("/ws/stdout/{id}", errorHandler(h.WsConsole))
}

View file

@ -5,28 +5,32 @@ import (
"flag" "flag"
"fmt" "fmt"
"net/http" "net/http"
"runtime"
"strings" "strings"
"github.com/drone/config" "github.com/drone/config"
"github.com/drone/drone/server/database" //"github.com/drone/drone/server/database"
"github.com/drone/drone/server/database/schema"
"github.com/drone/drone/server/handler" "github.com/drone/drone/server/handler"
"github.com/drone/drone/server/pubsub" "github.com/drone/drone/server/middleware"
"github.com/drone/drone/server/session" //"github.com/drone/drone/server/pubsub"
"github.com/drone/drone/server/worker" //"github.com/drone/drone/server/session"
//"github.com/drone/drone/server/worker"
"github.com/drone/drone/shared/build/log" "github.com/drone/drone/shared/build/log"
"github.com/drone/drone/shared/model" //"github.com/drone/drone/shared/model"
"github.com/gorilla/pat" //"github.com/GeertJohan/go.rice"
//"github.com/justinas/nosurf"
"github.com/GeertJohan/go.rice"
_ "github.com/mattn/go-sqlite3"
"github.com/russross/meddler"
"code.google.com/p/go.net/context"
webcontext "github.com/goji/context"
"github.com/zenazn/goji"
"github.com/zenazn/goji/web"
_ "github.com/drone/drone/plugin/notify/email"
"github.com/drone/drone/plugin/remote/bitbucket" "github.com/drone/drone/plugin/remote/bitbucket"
"github.com/drone/drone/plugin/remote/github" "github.com/drone/drone/plugin/remote/github"
"github.com/drone/drone/plugin/remote/gitlab" "github.com/drone/drone/plugin/remote/gitlab"
"github.com/drone/drone/server/blobstore"
"github.com/drone/drone/server/datastore"
"github.com/drone/drone/server/datastore/database"
) )
var ( var (
@ -58,6 +62,8 @@ var (
open bool open bool
nodes StringArr nodes StringArr
db *sql.DB
) )
func main() { func main() {
@ -65,12 +71,8 @@ func main() {
flag.StringVar(&conf, "config", "", "") flag.StringVar(&conf, "config", "", "")
flag.StringVar(&prefix, "prefix", "DRONE_", "") flag.StringVar(&prefix, "prefix", "DRONE_", "")
flag.StringVar(&port, "port", ":8080", "")
flag.StringVar(&driver, "driver", "sqlite3", "") flag.StringVar(&driver, "driver", "sqlite3", "")
flag.StringVar(&datasource, "datasource", "drone.sqlite", "") flag.StringVar(&datasource, "datasource", "drone.sqlite", "")
flag.StringVar(&sslcert, "sslcert", "", "")
flag.StringVar(&sslkey, "sslkey", "", "")
flag.IntVar(&workers, "workers", runtime.NumCPU(), "")
flag.Parse() flag.Parse()
config.Var(&nodes, "worker-nodes") config.Var(&nodes, "worker-nodes")
@ -85,86 +87,91 @@ func main() {
github.Register() github.Register()
gitlab.Register() gitlab.Register()
// setup the database // setup the database and cancel all pending
meddler.Default = meddler.SQLite // commits in the system.
db, _ := sql.Open(driver, datasource) db = database.MustConnect(driver, datasource)
schema.Load(db) go database.NewCommitstore(db).KillCommits()
// setup the database managers goji.Get("/api/auth/:host", handler.GetLogin)
repos := database.NewRepoManager(db) goji.Get("/api/badge/:host/:owner/:name/status.svg", handler.GetBadge)
users := database.NewUserManager(db) goji.Get("/api/badge/:host/:owner/:name/cc.xml", handler.GetCC)
perms := database.NewPermManager(db) //goji.Get("/api/hook", handler.PostHook)
commits := database.NewCommitManager(db) //goji.Put("/api/hook", handler.PostHook)
//goji.Post("/api/hook", handler.PostHook)
// message broker repos := web.New()
pubsub := pubsub.NewPubSub() repos.Use(middleware.SetRepo)
repos.Use(middleware.RequireRepoRead)
repos.Use(middleware.RequireRepoAdmin)
repos.Get("/api/repos/:host/:owner/:name/branches/:branch/commits/:commit/console", handler.GetOutput)
repos.Get("/api/repos/:host/:owner/:name/branches/:branch/commits/:commit", handler.GetCommit)
repos.Post("/api/repos/:host/:owner/:name/branches/:branch/commits/:commit", handler.PostCommit)
repos.Get("/api/repos/:host/:owner/:name/commits", handler.GetCommitList)
repos.Get("/api/repos/:host/:owner/:name", handler.GetRepo)
repos.Put("/api/repos/:host/:owner/:name", handler.PutRepo)
repos.Post("/api/repos/:host/:owner/:name", handler.PostRepo)
repos.Delete("/api/repos/:host/:owner/:name", handler.DelRepo)
goji.Handle("/api/repos/:host/:owner/:name*", repos)
// cancel all previously running builds users := web.New()
go commits.CancelAll() users.Use(middleware.RequireUserAdmin)
users.Get("/api/users/:host/:login", handler.GetUser)
users.Post("/api/users/:host/:login", handler.PostUser)
users.Delete("/api/users/:host/:login", handler.DelUser)
users.Get("/api/users", handler.GetUserList)
goji.Handle("/api/users*", users)
queue := make(chan *model.Request) user := web.New()
workerc := make(chan chan *model.Request) user.Use(middleware.RequireUser)
worker.NewDispatch(queue, workerc).Start() user.Get("/api/user/feed", handler.GetUserFeed)
user.Get("/api/user/repos", handler.GetUserRepos)
user.Get("/api/user", handler.GetUserCurrent)
user.Put("/api/user", handler.PutUser)
goji.Handle("/api/user*", user)
// Add middleware and serve
goji.Use(ContextMiddleware)
goji.Use(middleware.SetHeaders)
goji.Use(middleware.SetUser)
goji.Serve()
// if no worker nodes are specified than start 2 workers // if no worker nodes are specified than start 2 workers
// using the default DOCKER_HOST // using the default DOCKER_HOST
if nodes == nil || len(nodes) == 0 { /*
worker.NewWorker(workerc, users, repos, commits, pubsub, &model.Server{}).Start() if nodes == nil || len(nodes) == 0 {
worker.NewWorker(workerc, users, repos, commits, pubsub, &model.Server{}).Start() worker.NewWorker(workerc, users, repos, commits, pubsub, &model.Server{}).Start()
} else { worker.NewWorker(workerc, users, repos, commits, pubsub, &model.Server{}).Start()
for _, node := range nodes { } else {
println(node) for _, node := range nodes {
worker.NewWorker(workerc, users, repos, commits, pubsub, &model.Server{Host: node}).Start() println(node)
worker.NewWorker(workerc, users, repos, commits, pubsub, &model.Server{Host: node}).Start()
}
} }
} */
// setup the session managers
sess := session.NewSession(users)
// setup the router and register routes
router := pat.New()
handler.NewUsersHandler(users, sess).Register(router)
handler.NewUserHandler(users, repos, commits, sess).Register(router)
handler.NewHookHandler(users, repos, commits, queue).Register(router)
handler.NewLoginHandler(users, repos, perms, sess, open).Register(router)
handler.NewCommitHandler(users, repos, commits, perms, sess, queue).Register(router)
handler.NewRepoHandler(repos, commits, perms, sess).Register(router)
handler.NewBadgeHandler(repos, commits).Register(router)
handler.NewWsHandler(repos, commits, perms, sess, pubsub).Register(router)
box := rice.MustFindBox("app/")
fserver := http.FileServer(box.HTTPBox())
index, _ := box.Bytes("index.html")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasPrefix(r.URL.Path, "/favicon.ico"),
strings.HasPrefix(r.URL.Path, "/scripts/"),
strings.HasPrefix(r.URL.Path, "/styles/"),
strings.HasPrefix(r.URL.Path, "/views/"):
// serve static conent
fserver.ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/logout"),
strings.HasPrefix(r.URL.Path, "/login/"),
strings.HasPrefix(r.URL.Path, "/v1/"),
strings.HasPrefix(r.URL.Path, "/ws/"):
// standard header variables that should be set, for good measure.
w.Header().Add("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
w.Header().Add("X-Frame-Options", "DENY")
w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-XSS-Protection", "1; mode=block")
// serve dynamic content
router.ServeHTTP(w, r)
default:
w.Write(index)
}
})
// start webserver using HTTPS or HTTP // start webserver using HTTPS or HTTP
if len(sslcert) != 0 { //if len(sslcert) != 0 {
panic(http.ListenAndServeTLS(port, sslcert, sslkey, nil)) // panic(http.ListenAndServeTLS(port, sslcert, sslkey, nil))
} else { //} else {
panic(http.ListenAndServe(port, nil)) //panic(http.ListenAndServe(port, nil))
//}
}
// ContextMiddleware creates a new go.net/context and
// injects into the current goji context.
func ContextMiddleware(c *web.C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var ctx = context.Background()
ctx = datastore.NewContext(ctx, database.NewDatastore(db))
ctx = blobstore.NewContext(ctx, database.NewBlobstore(db))
//ctx = pool.NewContext(ctx, workers)
//ctx = director.NewContext(ctx, worker)
// add the context to the goji web context
webcontext.Set(c, ctx)
h.ServeHTTP(w, r)
} }
return http.HandlerFunc(fn)
} }
type StringArr []string type StringArr []string

View file

@ -0,0 +1,69 @@
package middleware
import (
"github.com/drone/drone/shared/model"
"github.com/zenazn/goji/web"
)
// UserToC sets the User in the current
// web context.
func UserToC(c *web.C, user *model.User) {
c.Env["user"] = user
}
// RepoToC sets the User in the current
// web context.
func RepoToC(c *web.C, repo *model.Repo) {
c.Env["repo"] = repo
}
// RoleToC sets the User in the current
// web context.
func RoleToC(c *web.C, role *model.Perm) {
c.Env["role"] = role
}
// ToUser returns the User from the current
// request context. If the User does not exist
// a nil value is returned.
func ToUser(c *web.C) *model.User {
var v = c.Env["user"]
if v == nil {
return nil
}
u, ok := v.(*model.User)
if !ok {
return nil
}
return u
}
// ToRepo returns the Repo from the current
// request context. If the Repo does not exist
// a nil value is returned.
func ToRepo(c *web.C) *model.Repo {
var v = c.Env["repo"]
if v == nil {
return nil
}
r, ok := v.(*model.Repo)
if !ok {
return nil
}
return r
}
// ToRole returns the Role from the current
// request context. If the Role does not exist
// a nil value is returned.
func ToRole(c *web.C) *model.Perm {
var v = c.Env["role"]
if v == nil {
return nil
}
p, ok := v.(*model.Perm)
if !ok {
return nil
}
return p
}

View file

@ -0,0 +1,28 @@
package middleware
import (
"net/http"
"time"
"github.com/zenazn/goji/web"
)
// SetHeaders is a middleware function that applies
// default headers and caching rules to each request.
func SetHeaders(c *web.C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("X-Frame-Options", "DENY")
w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-XSS-Protection", "1; mode=block")
w.Header().Add("Cache-Control", "no-cache")
w.Header().Add("Cache-Control", "no-store")
w.Header().Add("Cache-Control", "max-age=0")
w.Header().Add("Cache-Control", "must-revalidate")
w.Header().Add("Cache-Control", "value")
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
w.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

103
server/middleware/repo.go Normal file
View file

@ -0,0 +1,103 @@
package middleware
import (
"fmt"
"net/http"
"github.com/drone/drone/server/datastore"
"github.com/goji/context"
"github.com/zenazn/goji/web"
)
// SetRepo is a middleware function that retrieves
// the repository and stores in the context.
func SetRepo(c *web.C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var (
ctx = context.FromC(*c)
host = c.URLParams["host"]
owner = c.URLParams["owner"]
name = c.URLParams["name"]
user = ToUser(c)
)
repo, err := datastore.GetRepoName(ctx, host, owner, name)
switch {
case err != nil && user == nil:
w.WriteHeader(http.StatusUnauthorized)
return
case err != nil && user != nil:
w.WriteHeader(http.StatusNotFound)
return
}
role, _ := datastore.GetPerm(ctx, user, repo)
RepoToC(c, repo)
RoleToC(c, role)
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// RequireRepoRead is a middleware function that verifies
// the user has read access to the repository.
func RequireRepoRead(c *web.C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var (
role = ToRole(c)
user = ToUser(c)
)
switch {
case role == nil:
w.WriteHeader(http.StatusInternalServerError)
case user == nil && role.Read == false:
w.WriteHeader(http.StatusUnauthorized)
return
case user == nil && role.Read == false:
w.WriteHeader(http.StatusUnauthorized)
return
case user != nil && role.Read == false:
w.WriteHeader(http.StatusNotFound)
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// RequireRepoAdmin is a middleware function that verifies
// the user has admin access to the repository.
func RequireRepoAdmin(c *web.C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var (
role = ToRole(c)
user = ToUser(c)
)
// Admin access is only rquired for POST, PUT, DELETE methods.
// If this is a GET request we can proceed immediately.
if r.Method == "GET" {
h.ServeHTTP(w, r)
return
}
switch {
case role == nil:
w.WriteHeader(http.StatusInternalServerError)
return
case user == nil && role.Admin == false:
w.WriteHeader(http.StatusUnauthorized)
return
case user != nil && role.Read == false && role.Admin == false:
w.WriteHeader(http.StatusNotFound)
return
case user != nil && role.Read == true && role.Admin == false:
w.WriteHeader(http.StatusForbidden)
return
default:
h.ServeHTTP(w, r)
return
}
}
return http.HandlerFunc(fn)
}

57
server/middleware/user.go Normal file
View file

@ -0,0 +1,57 @@
package middleware
import (
"net/http"
"github.com/drone/drone/server/session"
"github.com/goji/context"
"github.com/zenazn/goji/web"
)
// SetUser is a middleware function that retrieves
// the currently authenticated user from the request
// and stores in the context.
func SetUser(c *web.C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var ctx = context.FromC(*c)
var user = session.GetUser(ctx, r)
if user != nil && user.ID != 0 {
UserToC(c, user)
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// RequireUser is a middleware function that verifies
// there is a currently authenticated user stored in
// the context.
func RequireUser(c *web.C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if ToUser(c) == nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// RequireUserAdmin is a middleware function that verifies
// there is a currently authenticated user stored in
// the context with ADMIN privilege.
func RequireUserAdmin(c *web.C, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var user = ToUser(c)
switch {
case user == nil:
w.WriteHeader(http.StatusUnauthorized)
return
case user != nil && !user.Admin:
w.WriteHeader(http.StatusForbidden)
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}

View file

@ -2,97 +2,67 @@ package session
import ( import (
"net/http" "net/http"
"time"
"github.com/drone/drone/server/database" "code.google.com/p/go.net/context"
"github.com/dgrijalva/jwt-go"
"github.com/drone/drone/server/datastore"
"github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/httputil"
"github.com/drone/drone/shared/model" "github.com/drone/drone/shared/model"
"github.com/gorilla/securecookie" "github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
) )
// stores sessions using secure cookies. // secret key used to create jwt
var cookies = sessions.NewCookieStore( var secret = securecookie.GenerateRandomKey(32)
securecookie.GenerateRandomKey(64))
// stores sessions using secure cookies. // GetUser gets the currently authenticated user for the
var xsrftoken = string(securecookie.GenerateRandomKey(32)) // http.Request. The user details will be stored as either
// a simple API token or JWT bearer token.
type Session interface { func GetUser(c context.Context, r *http.Request) *model.User {
User(r *http.Request) *model.User var token = r.FormValue("access_token")
UserToken(r *http.Request) *model.User
UserCookie(r *http.Request) *model.User
SetUser(w http.ResponseWriter, r *http.Request, u *model.User)
Clear(w http.ResponseWriter, r *http.Request)
}
type session struct {
users database.UserManager
}
func NewSession(users database.UserManager) Session {
return &session{
users: users,
}
}
// User gets the currently authenticated user.
func (s *session) User(r *http.Request) *model.User {
switch { switch {
case r.FormValue("access_token") == "": case len(token) == 0:
return s.UserCookie(r) return nil
case r.FormValue("access_token") != "": case len(token) == 32:
return s.UserToken(r) return getUserToken(c, r)
default:
return getUserBearer(c, r)
} }
return nil
} }
// UserXsrf gets the currently authenticated user and // GenerateToken generates a JWT token for the user session
// validates the xsrf session token, if necessary. // that can be appended to the #access_token segment to
func (s *session) UserXsrf(r *http.Request) *model.User { // facilitate client-based OAuth2.
user := s.User(r) func GenerateToken(c context.Context, r *http.Request, user *model.User) (string, error) {
if user == nil || r.FormValue("access_token") != "" { token := jwt.New(jwt.GetSigningMethod("HS256"))
return user token.Claims["user_id"] = user.ID
} token.Claims["audience"] = httputil.GetURL(r)
if !httputil.CheckXsrf(r, xsrftoken, user.Login) { token.Claims["expires"] = time.Now().UTC().Add(time.Hour * 72).Unix()
return nil return token.SignedString(secret)
} }
// getUserToken gets the currently authenticated user for the given
// auth token.
func getUserToken(c context.Context, r *http.Request) *model.User {
var token = r.FormValue("access_token")
var user, _ = datastore.GetUserToken(c, token)
return user return user
} }
// UserToken gets the currently authenticated user for the given auth token. // getUserBearer gets the currently authenticated user for the given
func (s *session) UserToken(r *http.Request) *model.User { // bearer token (JWT)
token := r.FormValue("access_token") func getUserBearer(c context.Context, r *http.Request) *model.User {
user, _ := s.users.FindToken(token) var tokenstr = r.FormValue("access_token")
return user var token, err = jwt.Parse(tokenstr, func(t *jwt.Token) (interface{}, error) {
} return secret, nil
})
// UserCookie gets the currently authenticated user from the secure cookie session. if err != nil || token.Valid {
func (s *session) UserCookie(r *http.Request) *model.User {
sess, err := cookies.Get(r, "_sess")
if err != nil {
return nil return nil
} }
// get the uid from the session var userid, ok = token.Claims["user_id"].(int64)
value, ok := sess.Values["uid"]
if !ok { if !ok {
return nil return nil
} }
// get the user from the database var user, _ = datastore.GetUser(c, userid)
user, _ := s.users.Find(value.(int64))
return user return user
} }
// SetUser writes the specified username to the session.
func (s *session) SetUser(w http.ResponseWriter, r *http.Request, u *model.User) {
sess, _ := cookies.Get(r, "_sess")
sess.Values["uid"] = u.ID
sess.Save(r, w)
httputil.SetXsrf(w, r, xsrftoken, u.Login)
}
// Clear removes the user from the session.
func (s *session) Clear(w http.ResponseWriter, r *http.Request) {
sess, _ := cookies.Get(r, "_sess")
delete(sess.Values, "uid")
sess.Save(r, w)
}

31
server/worker/context.go Normal file
View file

@ -0,0 +1,31 @@
package worker
import (
"code.google.com/p/go.net/context"
)
const reqkey = "worker"
// NewContext returns a Context whose Value method returns the
// application's worker queue.
func NewContext(parent context.Context, worker Worker) context.Context {
return &wrapper{parent, worker}
}
type wrapper struct {
context.Context
worker Worker
}
// Value returns the named key from the context.
func (c *wrapper) Value(key interface{}) interface{} {
if key == reqkey {
return c.worker
}
return c.Context.Value(key)
}
// FromContext returns the worker queue associated with this context.
func FromContext(c context.Context) Worker {
return c.Value(reqkey).(Worker)
}

View file

@ -0,0 +1,12 @@
package director
import (
"code.google.com/p/go.net/context"
"github.com/drone/drone/server/worker"
)
// NewContext returns a Context whose Value method returns
// the director.
func NewContext(parent context.Context, w worker.Worker) context.Context {
return worker.NewContext(parent, w)
}

View file

@ -0,0 +1,117 @@
package director
import (
"sync"
"code.google.com/p/go.net/context"
"github.com/drone/drone/server/worker"
"github.com/drone/drone/server/worker/pool"
)
// Director manages workloads and delegates to workers.
type Director struct {
sync.Mutex
pending map[*worker.Work]bool
started map[*worker.Work]worker.Worker
}
func New() *Director {
return &Director{
pending: make(map[*worker.Work]bool),
started: make(map[*worker.Work]worker.Worker),
}
}
// Do processes the work request async.
func (d *Director) Do(c context.Context, work *worker.Work) {
defer func() {
recover()
}()
d.do(c, work)
}
// do is a blocking function that waits for an
// available worker to process work.
func (d *Director) do(c context.Context, work *worker.Work) {
d.markPending(work)
var pool = pool.FromContext(c)
var worker = <-pool.Reserve()
// var worker worker.Worker
//
// // waits for an available worker. This is a blocking
// // operation and will reject any nil workers to avoid
// // a potential panic.
// select {
// case worker = <-pool.Reserve():
// if worker != nil {
// break
// }
// }
d.markStarted(work, worker)
worker.Do(c, work)
d.markComplete(work)
pool.Release(worker)
}
// GetStarted returns a list of all jobs that
// are assigned and being worked on.
func (d *Director) GetStarted() []*worker.Work {
d.Lock()
defer d.Unlock()
started := []*worker.Work{}
for work, _ := range d.started {
started = append(started, work)
}
return started
}
// GetPending returns a list of all work that
// is pending assignment to a worker.
func (d *Director) GetPending() []*worker.Work {
d.Lock()
defer d.Unlock()
pending := []*worker.Work{}
for work, _ := range d.pending {
pending = append(pending, work)
}
return pending
}
// GetAssignments returns a list of assignments. The
// assignment type is a structure that stores the
// work being performed and the assigned worker.
func (d *Director) GetAssignemnts() []*worker.Assignment {
d.Lock()
defer d.Unlock()
assignments := []*worker.Assignment{}
for work, _worker := range d.started {
assignment := &worker.Assignment{work, _worker}
assignments = append(assignments, assignment)
}
return assignments
}
func (d *Director) markPending(work *worker.Work) {
d.Lock()
defer d.Unlock()
delete(d.started, work)
d.pending[work] = true
}
func (d *Director) markStarted(work *worker.Work, worker worker.Worker) {
d.Lock()
defer d.Unlock()
delete(d.pending, work)
d.started[work] = worker
}
func (d *Director) markComplete(work *worker.Work) {
d.Lock()
defer d.Unlock()
delete(d.pending, work)
delete(d.started, work)
}

View file

@ -0,0 +1,97 @@
package director
import (
"testing"
"code.google.com/p/go.net/context"
"github.com/drone/drone/server/worker"
"github.com/drone/drone/server/worker/pool"
"github.com/franela/goblin"
)
func TestDirector(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Director", func() {
g.It("Should mark work as pending", func() {
d := New()
d.markPending(&worker.Work{})
d.markPending(&worker.Work{})
g.Assert(len(d.GetPending())).Equal(2)
})
g.It("Should mark work as started", func() {
d := New()
w1 := worker.Work{}
w2 := worker.Work{}
d.markPending(&w1)
d.markPending(&w2)
g.Assert(len(d.GetPending())).Equal(2)
d.markStarted(&w1, &mockWorker{})
g.Assert(len(d.GetStarted())).Equal(1)
g.Assert(len(d.GetPending())).Equal(1)
d.markStarted(&w2, &mockWorker{})
g.Assert(len(d.GetStarted())).Equal(2)
g.Assert(len(d.GetPending())).Equal(0)
})
g.It("Should mark work as complete", func() {
d := New()
w1 := worker.Work{}
w2 := worker.Work{}
d.markStarted(&w1, &mockWorker{})
d.markStarted(&w2, &mockWorker{})
g.Assert(len(d.GetStarted())).Equal(2)
d.markComplete(&w1)
g.Assert(len(d.GetStarted())).Equal(1)
d.markComplete(&w2)
g.Assert(len(d.GetStarted())).Equal(0)
})
g.It("Should get work assignments", func() {
d := New()
w1 := worker.Work{}
w2 := worker.Work{}
d.markStarted(&w1, &mockWorker{})
d.markStarted(&w2, &mockWorker{})
g.Assert(len(d.GetAssignemnts())).Equal(2)
})
g.It("Should recover from a panic", func() {
d := New()
d.Do(nil, nil)
g.Assert(true).Equal(true)
})
g.It("Should distribute work to worker", func() {
work := &worker.Work{}
workr := &mockWorker{}
c := context.Background()
p := pool.New()
p.Allocate(workr)
c = pool.NewContext(c, p)
d := New()
d.do(c, work)
g.Assert(workr.work).Equal(work) // verify mock worker gets work
})
g.It("Should add director to context", func() {
d := New()
c := context.Background()
c = NewContext(c, d)
g.Assert(worker.FromContext(c)).Equal(d)
})
})
}
// fake worker for testing purpose only
type mockWorker struct {
name string
work *worker.Work
}
func (m *mockWorker) Do(c context.Context, w *worker.Work) {
m.work = w
}

View file

@ -1,50 +0,0 @@
package worker
import (
"github.com/drone/drone/shared/model"
)
// http://nesv.github.io/golang/2014/02/25/worker-queues-in-go.html
type Dispatch struct {
requests chan *model.Request
workers chan chan *model.Request
quit chan bool
}
func NewDispatch(requests chan *model.Request, workers chan chan *model.Request) *Dispatch {
return &Dispatch{
requests: requests,
workers: workers,
quit: make(chan bool),
}
}
// Start tells the dispatcher to start listening
// for work requests and dispatching to workers.
func (d *Dispatch) Start() {
go func() {
for {
select {
// pickup a request from the queue
case request := <-d.requests:
go func() {
// find an available worker and
// send the request to that worker
worker := <-d.workers
worker <- request
}()
// listen for a signal to exit
case <-d.quit:
return
}
}
}()
}
// Stop tells the dispatcher to stop listening for new
// work requests.
func (d *Dispatch) Stop() {
go func() { d.quit <- true }()
}

View file

@ -0,0 +1,146 @@
package docker
import (
"bytes"
"log"
"path/filepath"
"runtime/debug"
"time"
"code.google.com/p/go-uuid/uuid"
"code.google.com/p/go.net/context"
"github.com/drone/drone/plugin/notify"
"github.com/drone/drone/server/blobstore"
"github.com/drone/drone/server/datastore"
"github.com/drone/drone/server/worker"
"github.com/drone/drone/shared/build"
"github.com/drone/drone/shared/build/docker"
"github.com/drone/drone/shared/build/git"
"github.com/drone/drone/shared/build/repo"
"github.com/drone/drone/shared/build/script"
"github.com/drone/drone/shared/model"
)
const dockerKind = "docker"
type Docker struct {
UUID string `json:"uuid"`
Kind string `json:"type"`
Created int64 `json:"created"`
docker *docker.Client
}
func New() *Docker {
return &Docker{
UUID: uuid.New(),
Kind: dockerKind,
Created: time.Now().UTC().Unix(),
docker: docker.New(),
//docker.NewHost(w.server.Host)
}
}
func (d *Docker) Do(c context.Context, r *worker.Work) {
// ensure that we can recover from any panics to
// avoid bringing down the entire application.
defer func() {
if e := recover(); e != nil {
log.Printf("%s: %s", e, debug.Stack())
}
}()
// mark the build as Started and update the database
r.Commit.Status = model.StatusStarted
r.Commit.Started = time.Now().UTC().Unix()
datastore.PutCommit(c, r.Commit)
// notify all listeners that the build is started
//commitc := w.pubsub.Register("_global")
//commitc.Publish(r)
//stdoutc := w.pubsub.RegisterOpts(r.Commit.ID, pubsub.ConsoleOpts)
//defer stdoutc.Close()
// create a special buffer that will also
// write to a websocket channel
var buf bytes.Buffer //:= pubsub.NewBuffer(stdoutc)
// parse the parameters and build script. The script has already
// been parsed in the hook, so we can be confident it will succeed.
// that being said, we should clean this up
params, err := r.Repo.ParamMap()
if err != nil {
log.Printf("Error parsing PARAMS for %s/%s, Err: %s", r.Repo.Owner, r.Repo.Name, err.Error())
}
script, err := script.ParseBuild(r.Commit.Config, params)
if err != nil {
log.Printf("Error parsing YAML for %s/%s, Err: %s", r.Repo.Owner, r.Repo.Name, err.Error())
}
// append private parameters to the environment
// variable section of the .drone.yml file, iff
// this is not a pull request (for security purposes)
if params != nil && (r.Repo.Private || len(r.Commit.PullRequest) == 0) {
for k, v := range params {
script.Env = append(script.Env, k+"="+v)
}
}
path := r.Repo.Host + "/" + r.Repo.Owner + "/" + r.Repo.Name
repo := &repo.Repo{
Name: path,
Path: r.Repo.CloneURL,
Branch: r.Commit.Branch,
Commit: r.Commit.Sha,
PR: r.Commit.PullRequest,
Dir: filepath.Join("/var/cache/drone/src", git.GitPath(script.Git, path)),
Depth: git.GitDepth(script.Git),
}
// send all "started" notifications
if script.Notifications == nil {
script.Notifications = &notify.Notification{}
}
//script.Notifications.Send(r)
// create an instance of the Docker builder
builder := build.New(d.docker)
builder.Build = script
builder.Repo = repo
builder.Stdout = &buf
builder.Key = []byte(r.Repo.PrivateKey)
builder.Timeout = time.Duration(r.Repo.Timeout) * time.Second
builder.Privileged = r.Repo.Privileged
// run the build
err = builder.Run()
// update the build status based on the results
// from the build runner.
switch {
case err != nil:
r.Commit.Status = model.StatusError
log.Printf("Error building %s, Err: %s", r.Commit.Sha, err)
buf.WriteString(err.Error())
case builder.BuildState == nil:
r.Commit.Status = model.StatusFailure
case builder.BuildState.ExitCode != 0:
r.Commit.Status = model.StatusFailure
default:
r.Commit.Status = model.StatusSuccess
}
// calcualte the build finished and duration details and
// update the commit
r.Commit.Finished = time.Now().UTC().Unix()
r.Commit.Duration = (r.Commit.Finished - r.Commit.Started)
datastore.PutCommit(c, r.Commit)
blobstore.Put(c, filepath.Join(r.Repo.Host, r.Repo.Owner, r.Repo.Name, r.Commit.Branch, r.Commit.Sha), buf.Bytes())
// notify all listeners that the build is finished
//commitc.Publish(r)
// send all "finished" notifications
//script.Notifications.Send(r)
}

View file

@ -0,0 +1,31 @@
package pool
import (
"code.google.com/p/go.net/context"
)
const reqkey = "pool"
// NewContext returns a Context whose Value method returns the
// worker pool.
func NewContext(parent context.Context, pool *Pool) context.Context {
return &wrapper{parent, pool}
}
type wrapper struct {
context.Context
pool *Pool
}
// Value returns the named key from the context.
func (c *wrapper) Value(key interface{}) interface{} {
if key == reqkey {
return c.pool
}
return c.Context.Value(key)
}
// FromContext returns the pool assigned to the context.
func FromContext(c context.Context) *Pool {
return c.Value(reqkey).(*Pool)
}

View file

@ -0,0 +1,89 @@
package pool
import (
"sync"
"github.com/drone/drone/server/worker"
)
// TODO (bradrydzewski) ability to cancel work.
// TODO (bradrydzewski) ability to remove a worker.
type Pool struct {
sync.Mutex
workers map[worker.Worker]bool
workerc chan worker.Worker
}
func New() *Pool {
return &Pool{
workers: make(map[worker.Worker]bool),
workerc: make(chan worker.Worker, 999),
}
}
// Allocate allocates a worker to the pool to
// be available to accept work.
func (p *Pool) Allocate(w worker.Worker) bool {
if p.IsAllocated(w) {
return false
}
p.Lock()
p.workers[w] = true
p.Unlock()
p.workerc <- w
return true
}
// IsAllocated is a helper function that returns
// true if the worker is currently allocated to
// the Pool.
func (p *Pool) IsAllocated(w worker.Worker) bool {
p.Lock()
defer p.Unlock()
_, ok := p.workers[w]
return ok
}
// Deallocate removes the worker from the pool of
// available workers. If the worker is currently
// reserved and performing work it will finish,
// but no longer be given new work.
func (p *Pool) Deallocate(w worker.Worker) {
p.Lock()
defer p.Unlock()
delete(p.workers, w)
}
// List returns a list of all Workers currently
// allocated to the Pool.
func (p *Pool) List() []worker.Worker {
p.Lock()
defer p.Unlock()
var workers []worker.Worker
for w, _ := range p.workers {
workers = append(workers, w)
}
return workers
}
// Reserve reserves the next available worker to
// start doing work. Once work is complete, the
// worker should be released back to the pool.
func (p *Pool) Reserve() <-chan worker.Worker {
return p.workerc
}
// Release releases the worker back to the pool
// of available workers.
func (p *Pool) Release(w worker.Worker) bool {
if !p.IsAllocated(w) {
return false
}
p.workerc <- w
return true
}

View file

@ -0,0 +1,103 @@
package pool
import (
"testing"
"code.google.com/p/go.net/context"
"github.com/drone/drone/server/worker"
"github.com/franela/goblin"
)
func TestPool(t *testing.T) {
g := goblin.Goblin(t)
g.Describe("Pool", func() {
g.It("Should allocate workers", func() {
w := mockWorker{}
pool := New()
pool.Allocate(&w)
g.Assert(len(pool.workers)).Equal(1)
g.Assert(len(pool.workerc)).Equal(1)
g.Assert(pool.workers[&w]).Equal(true)
})
g.It("Should not re-allocate an allocated worker", func() {
w := mockWorker{}
pool := New()
g.Assert(pool.Allocate(&w)).Equal(true)
g.Assert(pool.Allocate(&w)).Equal(false)
})
g.It("Should reserve a worker", func() {
w := mockWorker{}
pool := New()
pool.Allocate(&w)
g.Assert(<-pool.Reserve()).Equal(&w)
})
g.It("Should release a worker", func() {
w := mockWorker{}
pool := New()
pool.Allocate(&w)
g.Assert(len(pool.workerc)).Equal(1)
g.Assert(<-pool.Reserve()).Equal(&w)
g.Assert(len(pool.workerc)).Equal(0)
pool.Release(&w)
g.Assert(len(pool.workerc)).Equal(1)
g.Assert(<-pool.Reserve()).Equal(&w)
g.Assert(len(pool.workerc)).Equal(0)
})
g.It("Should not release an unallocated worker", func() {
w := mockWorker{}
pool := New()
g.Assert(len(pool.workers)).Equal(0)
g.Assert(len(pool.workerc)).Equal(0)
pool.Release(&w)
g.Assert(len(pool.workers)).Equal(0)
g.Assert(len(pool.workerc)).Equal(0)
pool.Release(nil)
g.Assert(len(pool.workers)).Equal(0)
g.Assert(len(pool.workerc)).Equal(0)
})
g.It("Should list all allocated workers", func() {
w1 := mockWorker{}
w2 := mockWorker{}
pool := New()
pool.Allocate(&w1)
pool.Allocate(&w2)
g.Assert(len(pool.workers)).Equal(2)
g.Assert(len(pool.workerc)).Equal(2)
g.Assert(len(pool.List())).Equal(2)
})
g.It("Should remove a worker", func() {
w1 := mockWorker{}
w2 := mockWorker{}
pool := New()
pool.Allocate(&w1)
pool.Allocate(&w2)
g.Assert(len(pool.workers)).Equal(2)
pool.Deallocate(&w1)
pool.Deallocate(&w2)
g.Assert(len(pool.workers)).Equal(0)
g.Assert(len(pool.List())).Equal(0)
})
g.It("Should add / retrieve from context", func() {
c := context.Background()
p := New()
c = NewContext(c, p)
g.Assert(FromContext(c)).Equal(p)
})
})
}
// fake worker for testing purpose only
type mockWorker struct {
name string
}
func (*mockWorker) Do(c context.Context, w *worker.Work) {}

14
server/worker/work.go Normal file
View file

@ -0,0 +1,14 @@
package worker
import "github.com/drone/drone/shared/model"
type Work struct {
User *model.User
Repo *model.Repo
Commit *model.Commit
}
type Assignment struct {
Work *Work
Worker Worker
}

View file

@ -1,184 +1,15 @@
package worker package worker
import ( import (
"log" "code.google.com/p/go.net/context"
"path/filepath"
"time"
"github.com/drone/drone/plugin/notify"
"github.com/drone/drone/server/database"
"github.com/drone/drone/server/pubsub"
"github.com/drone/drone/shared/build"
"github.com/drone/drone/shared/build/docker"
"github.com/drone/drone/shared/build/git"
"github.com/drone/drone/shared/build/repo"
"github.com/drone/drone/shared/build/script"
"github.com/drone/drone/shared/model"
) )
type Worker interface { type Worker interface {
Start() // Start instructs the worker to start processing requests Do(context.Context, *Work)
Stop() // Stop instructions the worker to stop processing requests
} }
type worker struct { // Do retrieves a worker from the session and uses
users database.UserManager // it to get work done.
repos database.RepoManager func Do(c context.Context, w *Work) {
commits database.CommitManager FromContext(c).Do(c, w)
//config database.ConfigManager
pubsub *pubsub.PubSub
server *model.Server
request chan *model.Request
dispatch chan chan *model.Request
quit chan bool
}
func NewWorker(dispatch chan chan *model.Request, users database.UserManager, repos database.RepoManager, commits database.CommitManager /*config database.ConfigManager,*/, pubsub *pubsub.PubSub, server *model.Server) Worker {
return &worker{
users: users,
repos: repos,
commits: commits,
//config: config,
pubsub: pubsub,
server: server,
dispatch: dispatch,
request: make(chan *model.Request),
quit: make(chan bool),
}
}
// Start tells the worker to start listening and
// accepting new work requests.
func (w *worker) Start() {
go func() {
for {
// register our queue with the dispatch
// queue to start accepting work.
go func() { w.dispatch <- w.request }()
select {
case r := <-w.request:
// handle the request
r.Server = w.server
w.Execute(r)
case <-w.quit:
return
}
}
}()
}
// Stop tells the worker to stop listening for new
// work requests.
func (w *worker) Stop() {
go func() { w.quit <- true }()
}
// Execute executes the work Request, persists the
// results to the database, and sends event messages
// to the pubsub (for websocket updates on the website).
func (w *worker) Execute(r *model.Request) {
// mark the build as Started and update the database
r.Commit.Status = model.StatusStarted
r.Commit.Started = time.Now().UTC().Unix()
w.commits.Update(r.Commit)
// notify all listeners that the build is started
commitc := w.pubsub.Register("_global")
commitc.Publish(r)
stdoutc := w.pubsub.RegisterOpts(r.Commit.ID, pubsub.ConsoleOpts)
defer stdoutc.Close()
// create a special buffer that will also
// write to a websocket channel
buf := pubsub.NewBuffer(stdoutc)
// parse the parameters and build script. The script has already
// been parsed in the hook, so we can be confident it will succeed.
// that being said, we should clean this up
params, err := r.Repo.ParamMap()
if err != nil {
log.Printf("Error parsing PARAMS for %s/%s, Err: %s", r.Repo.Owner, r.Repo.Name, err.Error())
}
script, err := script.ParseBuild(r.Commit.Config, params)
if err != nil {
log.Printf("Error parsing YAML for %s/%s, Err: %s", r.Repo.Owner, r.Repo.Name, err.Error())
}
// append private parameters to the environment
// variable section of the .drone.yml file, iff
// this is not a pull request (for security purposes)
if params != nil && (r.Repo.Private || len(r.Commit.PullRequest) == 0) {
for k, v := range params {
script.Env = append(script.Env, k+"="+v)
}
}
path := r.Repo.Host + "/" + r.Repo.Owner + "/" + r.Repo.Name
repo := &repo.Repo{
Name: path,
Path: r.Repo.CloneURL,
Branch: r.Commit.Branch,
Commit: r.Commit.Sha,
PR: r.Commit.PullRequest,
Dir: filepath.Join("/var/cache/drone/src", git.GitPath(script.Git, path)),
Depth: git.GitDepth(script.Git),
}
// Instantiate a new Docker client
var dockerClient *docker.Client
switch {
case len(w.server.Host) == 0:
dockerClient = docker.New()
default:
dockerClient = docker.NewHost(w.server.Host)
}
// send all "started" notifications
if script.Notifications == nil {
script.Notifications = &notify.Notification{}
}
script.Notifications.Send(r)
// create an instance of the Docker builder
builder := build.New(dockerClient)
builder.Build = script
builder.Repo = repo
builder.Stdout = buf
builder.Key = []byte(r.Repo.PrivateKey)
builder.Timeout = time.Duration(r.Repo.Timeout) * time.Second
builder.Privileged = r.Repo.Privileged
// run the build
err = builder.Run()
// update the build status based on the results
// from the build runner.
switch {
case err != nil:
r.Commit.Status = model.StatusError
log.Printf("Error building %s, Err: %s", r.Commit.Sha, err)
buf.WriteString(err.Error())
case builder.BuildState == nil:
r.Commit.Status = model.StatusFailure
case builder.BuildState.ExitCode != 0:
r.Commit.Status = model.StatusFailure
default:
r.Commit.Status = model.StatusSuccess
}
// calcualte the build finished and duration details and
// update the commit
r.Commit.Finished = time.Now().UTC().Unix()
r.Commit.Duration = (r.Commit.Finished - r.Commit.Started)
w.commits.Update(r.Commit)
w.commits.UpdateOutput(r.Commit, buf.Bytes())
// notify all listeners that the build is finished
commitc.Publish(r)
// send all "finished" notifications
script.Notifications.Send(r)
} }

View file

@ -3,8 +3,6 @@ package httputil
import ( import (
"net/http" "net/http"
"strings" "strings"
"code.google.com/p/xsrftoken"
) )
// IsHttps is a helper function that evaluates the http.Request // IsHttps is a helper function that evaluates the http.Request
@ -105,26 +103,3 @@ func DelCookie(w http.ResponseWriter, r *http.Request, name string) {
http.SetCookie(w, &cookie) http.SetCookie(w, &cookie)
} }
// SetXsrf writes the cookie value.
func SetXsrf(w http.ResponseWriter, r *http.Request, token, login string) {
cookie := http.Cookie{
Name: "XSRF-TOKEN",
Value: xsrftoken.Generate(token, login, "/"),
Path: "/",
Domain: r.URL.Host,
HttpOnly: false,
Secure: IsHttps(r),
}
http.SetCookie(w, &cookie)
}
// CheckXsrf verifies the xsrf value.
func CheckXsrf(r *http.Request, token, login string) bool {
if r.Method == "GET" {
return true
}
return xsrftoken.Valid(
r.Header.Get("X-XSRF-TOKEN"), token, login, "/")
}