From 21f9aec80845cb42898609bb7c9513a1b5252f30 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Sun, 28 Sep 2014 18:36:24 -0700 Subject: [PATCH] added new handlers and workers --- Makefile | 2 +- server/capability/const.go | 5 + server/database/commit.go | 252 --------------- server/database/commit_test.go | 282 ----------------- server/database/perm.go | 161 ---------- server/database/perm_test.go | 310 ------------------- server/database/repo.go | 78 ----- server/database/repo_test.go | 226 -------------- server/database/schema/schema.go | 128 -------- server/database/testdata/testdata.go | 57 ---- server/database/testdatabase/testdatabase.go | 32 -- server/database/user.go | 127 -------- server/database/user_test.go | 235 -------------- server/datastore/database/database.go | 4 +- server/datastore/perm.go | 2 +- server/handler/badge.go | 117 +++---- server/handler/commit.go | 160 +++------- server/handler/context.go | 51 +++ server/handler/error.go | 59 ---- server/handler/hook.go | 120 +------ server/handler/login.go | 98 +++--- server/handler/output.go | 36 +++ server/handler/repo.go | 288 +++++------------ server/handler/user.go | 140 +++++---- server/handler/users.go | 213 ++++++------- server/handler/util.go | 20 -- server/handler/ws.go | 203 ------------ server/main.go | 177 ++++++----- server/middleware/context.go | 69 +++++ server/middleware/header.go | 28 ++ server/middleware/repo.go | 103 ++++++ server/middleware/user.go | 57 ++++ server/session/session.go | 116 +++---- server/worker/context.go | 31 ++ server/worker/director/context.go | 12 + server/worker/director/director.go | 117 +++++++ server/worker/director/director_test.go | 97 ++++++ server/worker/dispatch.go | 50 --- server/worker/docker/docker.go | 146 +++++++++ server/worker/pool/context.go | 31 ++ server/worker/pool/pool.go | 89 ++++++ server/worker/pool/pool_test.go | 103 ++++++ server/worker/work.go | 14 + server/worker/worker.go | 181 +---------- shared/httputil/httputil.go | 25 -- 45 files changed, 1522 insertions(+), 3330 deletions(-) create mode 100644 server/capability/const.go delete mode 100644 server/database/commit.go delete mode 100644 server/database/commit_test.go delete mode 100644 server/database/perm.go delete mode 100644 server/database/perm_test.go delete mode 100644 server/database/repo.go delete mode 100644 server/database/repo_test.go delete mode 100644 server/database/schema/schema.go delete mode 100644 server/database/testdata/testdata.go delete mode 100644 server/database/testdatabase/testdatabase.go delete mode 100644 server/database/user.go delete mode 100644 server/database/user_test.go create mode 100644 server/handler/context.go delete mode 100644 server/handler/error.go create mode 100644 server/handler/output.go delete mode 100644 server/handler/util.go create mode 100644 server/middleware/context.go create mode 100644 server/middleware/header.go create mode 100644 server/middleware/repo.go create mode 100644 server/middleware/user.go create mode 100644 server/worker/context.go create mode 100644 server/worker/director/context.go create mode 100644 server/worker/director/director.go create mode 100644 server/worker/director/director_test.go delete mode 100644 server/worker/dispatch.go create mode 100644 server/worker/docker/docker.go create mode 100644 server/worker/pool/context.go create mode 100644 server/worker/pool/pool.go create mode 100644 server/worker/pool/pool_test.go create mode 100644 server/worker/work.go diff --git a/Makefile b/Makefile index ccdf238c5..89db72cdb 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ install: install -t /usr/local/bin debian/drone/usr/local/bin/droned run: - @go run server/main.go + @go run server/main.go --config=$$HOME/.drone/config.toml clean: find . -name "*.out" -delete diff --git a/server/capability/const.go b/server/capability/const.go new file mode 100644 index 000000000..7d4da039c --- /dev/null +++ b/server/capability/const.go @@ -0,0 +1,5 @@ +package capability + +const ( + Registration = "REGISTRATION" +) diff --git a/server/database/commit.go b/server/database/commit.go deleted file mode 100644 index 54faf7820..000000000 --- a/server/database/commit.go +++ /dev/null @@ -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 -} diff --git a/server/database/commit_test.go b/server/database/commit_test.go deleted file mode 100644 index a19908179..000000000 --- a/server/database/commit_test.go +++ /dev/null @@ -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) - } -} diff --git a/server/database/perm.go b/server/database/perm.go deleted file mode 100644 index 17d0cecbc..000000000 --- a/server/database/perm.go +++ /dev/null @@ -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 -} diff --git a/server/database/perm_test.go b/server/database/perm_test.go deleted file mode 100644 index 90058ad7c..000000000 --- a/server/database/perm_test.go +++ /dev/null @@ -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) - } -} diff --git a/server/database/repo.go b/server/database/repo.go deleted file mode 100644 index 88a364674..000000000 --- a/server/database/repo.go +++ /dev/null @@ -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 -} diff --git a/server/database/repo_test.go b/server/database/repo_test.go deleted file mode 100644 index bc039adbc..000000000 --- a/server/database/repo_test.go +++ /dev/null @@ -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) - } -} diff --git a/server/database/schema/schema.go b/server/database/schema/schema.go deleted file mode 100644 index 934d2af6e..000000000 --- a/server/database/schema/schema.go +++ /dev/null @@ -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) - } - } -} diff --git a/server/database/testdata/testdata.go b/server/database/testdata/testdata.go deleted file mode 100644 index 174e100da..000000000 --- a/server/database/testdata/testdata.go +++ /dev/null @@ -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) - } - } -} diff --git a/server/database/testdatabase/testdatabase.go b/server/database/testdatabase/testdatabase.go deleted file mode 100644 index 3aec7557d..000000000 --- a/server/database/testdatabase/testdatabase.go +++ /dev/null @@ -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 -} diff --git a/server/database/user.go b/server/database/user.go deleted file mode 100644 index 05183bc45..000000000 --- a/server/database/user.go +++ /dev/null @@ -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 -} diff --git a/server/database/user_test.go b/server/database/user_test.go deleted file mode 100644 index c36c5f9a8..000000000 --- a/server/database/user_test.go +++ /dev/null @@ -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) - } -} diff --git a/server/datastore/database/database.go b/server/datastore/database/database.go index 164639320..e953cacd1 100644 --- a/server/datastore/database/database.go +++ b/server/datastore/database/database.go @@ -72,8 +72,8 @@ func mustConnectTest() *sql.DB { return db } -// New returns a new DataStore -func New(db *sql.DB) datastore.Datastore { +// New returns a new Datastore +func NewDatastore(db *sql.DB) datastore.Datastore { return struct { *Userstore *Permstore diff --git a/server/datastore/perm.go b/server/datastore/perm.go index b31546bb1..ffc431aa0 100644 --- a/server/datastore/perm.go +++ b/server/datastore/perm.go @@ -32,7 +32,7 @@ func GetPerm(c context.Context, user *model.User, repo *model.Repo) (*model.Perm Read: false, Write: false, Admin: false}, nil - case user == nil && !reop.Private: + case user == nil && !repo.Private: return &model.Perm{ Guest: true, Read: true, diff --git a/server/handler/badge.go b/server/handler/badge.go index 3cb35fde1..cc1c67290 100644 --- a/server/handler/badge.go +++ b/server/handler/badge.go @@ -3,12 +3,12 @@ package handler import ( "encoding/xml" "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/model" - "github.com/gorilla/pat" + "github.com/goji/context" + "github.com/zenazn/goji/web" ) // badges that indicate the current build status for a repository @@ -21,56 +21,40 @@ var ( badgeNone = []byte(`buildbuildnonenone`) ) -type BadgeHandler struct { - commits database.CommitManager - repos database.RepoManager -} +// GetBadge accepts a request to retrieve the named +// repo and branhes latest build details from the datastore +// and return an SVG badges representing the build results. +// +// GET /api/badge/: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 { - 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) + repo, err := datastore.GetRepoName(ctx, host, owner, name) if err != nil { w.Write(badgeNone) - return nil + return } - - // if no branch, use the default if len(branch) == 0 { branch = model.DefaultBranch } - - // get the latest commit - c, _ := h.commits.FindLatest(arepo.ID, branch) + commit, _ := datastore.GetCommitLast(ctx, repo, branch) // if no commit was found then display - // the 'none' badge - if c == nil { + // the 'none' badge, instead of throwing + // an error response + if commit == nil { w.Write(badgeNone) - return nil + return } - // determine which badge to load - switch c.Status { + switch commit.Status { case model.StatusSuccess: w.Write(badgeSuccess) case model.StatusFailure: @@ -82,40 +66,33 @@ func (h *BadgeHandler) GetStatus(w http.ResponseWriter, r *http.Request) error { default: w.Write(badgeNone) } - - return nil } -// GetCoverage gets the build status badge. -// GET /v1/badges/:host/:owner/:name/coverage.svg -func (h *BadgeHandler) GetCoverage(w http.ResponseWriter, r *http.Request) error { - return notImplemented{} -} - -func (h *BadgeHandler) GetCC(w http.ResponseWriter, r *http.Request) error { - host, owner, name := parseRepo(r) - - // get the repository from the database - repo, err := h.repos.FindName(host, owner, name) +// GetCC accepts a request to retrieve the latest build +// status for the given repository from the datastore and +// in CCTray XML format. +// +// GET /api/badge/:host/:owner/:name/cc.xml +// +func GetCC(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"] + ) + repo, err := datastore.GetRepoName(ctx, host, owner, name) if err != nil { - return notFound{err} + w.Write(badgeNone) + return } - - // get the latest commits for the repo - commits, err := h.commits.List(repo.ID) + commits, err := datastore.GetCommitList(ctx, repo) if err != nil || len(commits) == 0 { - return notFound{} + w.WriteHeader(http.StatusNotFound) + return } - commit := commits[0] - // generate the URL for the repository - url := httputil.GetURL(r) + "/" + repo.Host + "/" + repo.Owner + "/" + repo.Name - proj := model.NewCC(repo, commit, url) - 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)) + var link = httputil.GetURL(r) + "/" + repo.Host + "/" + repo.Owner + "/" + repo.Name + var cc = model.NewCC(repo, commits[0], link) + xml.NewEncoder(w).Encode(cc) } diff --git a/server/handler/commit.go b/server/handler/commit.go index 8be729430..db73c42a4 100644 --- a/server/handler/commit.go +++ b/server/handler/commit.go @@ -4,139 +4,55 @@ import ( "encoding/json" "net/http" - "github.com/drone/drone/server/database" - "github.com/drone/drone/server/session" - "github.com/drone/drone/shared/httputil" - "github.com/drone/drone/shared/model" - "github.com/gorilla/pat" + "github.com/drone/drone/server/datastore" + "github.com/goji/context" + "github.com/zenazn/goji/web" ) -type CommitHandler struct { - users database.UserManager - perms database.PermManager - repos database.RepoManager - commits database.CommitManager - sess session.Session - queue chan *model.Request -} +// GetCommitList accepts a request to retrieve a list +// of recent commits by Repo, and retur in JSON format. +// +// GET /api/repos/:host/:owner/:name/commits +// +func GetCommitList(c web.C, w http.ResponseWriter, r *http.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 { - 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) + commits, err := datastore.GetCommitList(ctx, repo) 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. -// GET /v1/repos/{host}/{owner}/{name}/branches/{branch}/commits/{commit} -func (h *CommitHandler) GetCommit(w http.ResponseWriter, r *http.Request) error { - var host, owner, name = parseRepo(r) - var branch = r.FormValue(":branch") - var sha = r.FormValue(":commit") +// GetCommit accepts a request to retrieve a commit +// from the datastore for the given repository, branch and +// commit hash. +// +// GET /api/repos/:host/:owner/:name/branches/:branch/commits/: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. - 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) + commit, err := datastore.GetCommitSha(ctx, repo, branch, hash) 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. -// 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 -} +func PostCommit(c web.C, w http.ResponseWriter, r *http.Request) {} +/* // PostCommit gets the commit for the repository and schedules to re-build. // GET /v1/repos/{host}/{owner}/{name}/branches/{branch}/commits/{commit} 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) 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)) -} +*/ diff --git a/server/handler/context.go b/server/handler/context.go new file mode 100644 index 000000000..ac36ef6e3 --- /dev/null +++ b/server/handler/context.go @@ -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 +} diff --git a/server/handler/error.go b/server/handler/error.go deleted file mode 100644 index e07882e5d..000000000 --- a/server/handler/error.go +++ /dev/null @@ -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) - } - } -} diff --git a/server/handler/hook.go b/server/handler/hook.go index f8610b278..751891ccb 100644 --- a/server/handler/hook.go +++ b/server/handler/hook.go @@ -1,121 +1,3 @@ package handler -import ( - "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)) -} +// PostHook diff --git a/server/handler/login.go b/server/handler/login.go index 7e37bafde..04a22e416 100644 --- a/server/handler/login.go +++ b/server/handler/login.go @@ -6,52 +6,53 @@ import ( "time" "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/shared/model" - "github.com/gorilla/pat" + "github.com/goji/context" + "github.com/zenazn/goji/web" ) -type LoginHandler struct { - users database.UserManager - repos database.RepoManager - perms database.PermManager - sess session.Session - open bool -} - -func NewLoginHandler(users database.UserManager, repos database.RepoManager, perms database.PermManager, sess session.Session, open bool) *LoginHandler { - 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") +// GetLogin accepts a request to authorize the user and to +// return a valid OAuth2 access token. The access token is +// returned as url segment #access_token +// +// GET /login/:host +// +func GetLogin(c web.C, w http.ResponseWriter, r *http.Request) { + var ctx = context.FromC(c) + var host = c.URLParams["host"] var redirect = "/" var remote = remote.Lookup(host) if remote == nil { - return notFound{} + w.WriteHeader(http.StatusNotFound) + return } // authenticate the user login, err := remote.Authorize(w, r) if err != nil { - return badRequest{err} + w.WriteHeader(http.StatusBadRequest) + return } else if login == nil { // in this case we probably just redirected // the user, so we can exit with no error - return nil + return } // 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 self-registration is disabled we should // return a notAuthorized error. the only exception // is if no users exist yet in the system we'll proceed. - if h.open == false && h.users.Exist() { - return notAuthorized{} + if capability.Enabled(ctx, capability.Registration) == false { + users, err := datastore.GetUserList(ctx) + if err != nil || len(users) != 0 { + w.WriteHeader(http.StatusForbidden) + return + } } // create the user account @@ -60,8 +61,9 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { u.SetEmail(login.Email) // insert the user into the database - if err := h.users.Insert(u); err != nil { - return badRequest{err} + if err := datastore.PostUser(ctx, u); err != nil { + w.WriteHeader(http.StatusBadRequest) + return } // 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.SetEmail(login.Email) u.Syncing = true //u.IsStale() // todo (badrydzewski) should not always sync - if err := h.users.Update(u); err != nil { - return badRequest{err} + if err := datastore.PutUser(ctx, u); err != nil { + w.WriteHeader(http.StatusBadRequest) + return } // 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 for _, repo := range repos { 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 // 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 { log.Println("Error adding repo.", u.Login, repo.Name, err) continue @@ -120,7 +123,14 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { } // 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) continue } @@ -130,31 +140,19 @@ func (h *LoginHandler) GetLogin(w http.ResponseWriter, r *http.Request) error { u.Synced = time.Now().UTC().Unix() 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) return } }() } - // (re)-create the user session - h.sess.SetUser(w, r, u) + token, err := session.GenerateToken(ctx, 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) - 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)) } diff --git a/server/handler/output.go b/server/handler/output.go new file mode 100644 index 000000000..21a1e037f --- /dev/null +++ b/server/handler/output.go @@ -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) +} diff --git a/server/handler/repo.go b/server/handler/repo.go index dc72c4566..b8cef9a64 100644 --- a/server/handler/repo.go +++ b/server/handler/repo.go @@ -6,96 +6,73 @@ import ( "net/http" "github.com/drone/drone/plugin/remote" - "github.com/drone/drone/server/database" - "github.com/drone/drone/server/session" + "github.com/drone/drone/server/datastore" "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/model" "github.com/drone/drone/shared/sshutil" - "github.com/gorilla/pat" + "github.com/goji/context" + "github.com/zenazn/goji/web" ) -type RepoHandler struct { - commits database.CommitManager - perms database.PermManager - repos database.RepoManager - sess session.Session -} +// GetRepo accepts a request to retrieve a commit +// from the datastore for the given repository, branch and +// commit hash. +// +// 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, - perms database.PermManager, sess session.Session) *RepoHandler { - return &RepoHandler{commits, perms, repos, sess} -} - -// 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{} + // if the user is not requesting (or cannot access) + // admin data then we just return the repo as-is + if len(admin) == 0 || role.Admin == false { + json.NewEncoder(w).Encode(repo) + return } - // user must have read access to the repository. - repo.Role = h.perms.Find(user, repo) - 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 { + // else we should return restricted fields + json.NewEncoder(w).Encode(struct { *model.Repo PublicKey string `json:"public_key"` Params string `json:"params"` }{repo, repo.PublicKey, repo.Params}) } -// PostRepo activates the named repository. -// POST /v1/repos/:host/:owner/:name -func (h *RepoHandler) PostRepo(w http.ResponseWriter, r *http.Request) error { - var host, owner, name = parseRepo(r) +// DelRepo accepts a request to inactivate the named +// repository. This will disable all builds in the system +// for this repository. +// +// 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. - user := h.sess.User(r) - if user == nil { - return notAuthorized{} - } + // disable everything + repo.Active = false + repo.PullRequest = false + repo.PostCommit = false - // 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{} + if err := datastore.PutRepo(ctx, repo); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return } + w.WriteHeader(http.StatusNoContent) +} - // user must have admin access to the repository. - if ok, _ := h.perms.Admin(user, repo); !ok { - return notFound{err} - } +// PostRepo accapets a request to activate the named repository +// in the datastore. It returns a 201 status created if successful +// +// 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 repo.Active = true @@ -104,62 +81,47 @@ func (h *RepoHandler) PostRepo(w http.ResponseWriter, r *http.Request) error { repo.UserID = user.ID repo.Timeout = 3600 // default to 1 hour - // generate the rsa key + // generates the rsa key key, err := sshutil.GeneratePrivateKey() 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.PrivateKey = sshutil.MarshalPrivateKey(key) - var remote = remote.Lookup(host) + var remote = remote.Lookup(repo.Host) if remote == nil { - return notFound{} + w.WriteHeader(http.StatusNotFound) + return } - // post commit hook url - hook := fmt.Sprintf("%s://%s/v1/hook/%s", httputil.GetScheme(r), httputil.GetHost(r), remote.GetKind()) - - // activate the repository in the remote system + // setup the post-commit hook with the remote system and + // if necessary, register the public key + var hook = fmt.Sprintf("%s/v1/hook/%s", httputil.GetURL(r), repo.Remote) 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 := h.repos.Update(repo); err != nil { - return badRequest{err} + if err := datastore.PutRepo(ctx, repo); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return } - w.WriteHeader(http.StatusCreated) - return json.NewEncoder(w).Encode(repo) + json.NewEncoder(w).Encode(repo) } -// PutRepo updates the named repository. -// PUT /v1/repos/:host/:owner/:name -func (h *RepoHandler) PutRepo(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} - } +// PutRepo accapets a request to update the named repository +// in the datastore. It expects a JSON input and returns the +// updated repository in JSON format if successful. +// +// PUT /api/repos/:host/:owner/:name +// +func PutRepo(c web.C, w http.ResponseWriter, r *http.Request) { + var ctx = context.FromC(c) + var repo = ToRepo(c) + var user = ToUser(c) // unmarshal the repository from the payload defer r.Body.Close() @@ -173,28 +135,22 @@ func (h *RepoHandler) PutRepo(w http.ResponseWriter, r *http.Request) error { PrivateKey *string `json:"private_key"` }{} 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 { repo.Params = *in.Params } - // update the post commit flag if in.PostCommit != nil { repo.PostCommit = *in.PostCommit } - // update the pull request flag if in.PullRequest != nil { repo.PullRequest = *in.PullRequest } - // update the privileged flag. This can only be updated by - // the system administrator if in.Privileged != nil && user.Admin { repo.Privileged = *in.Privileged } - // update the timeout. This can only be updated by - // the system administrator if in.Timeout != nil && user.Admin { repo.Timeout = *in.Timeout } @@ -202,93 +158,9 @@ func (h *RepoHandler) PutRepo(w http.ResponseWriter, r *http.Request) error { repo.PublicKey = *in.PublicKey repo.PrivateKey = *in.PrivateKey } - - // update the repository - if err := h.repos.Update(repo); err != nil { - return badRequest{err} + if err := datastore.PutRepo(ctx, repo); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return } - - 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)) + json.NewEncoder(w).Encode(repo) } diff --git a/server/handler/user.go b/server/handler/user.go index f7a03774d..3b0b64dae 100644 --- a/server/handler/user.go +++ b/server/handler/user.go @@ -4,112 +4,110 @@ import ( "encoding/json" "net/http" - "github.com/drone/drone/server/database" - "github.com/drone/drone/server/session" + "github.com/drone/drone/server/datastore" "github.com/drone/drone/shared/model" - "github.com/gorilla/pat" + "github.com/goji/context" + "github.com/zenazn/goji/web" ) -type UserHandler struct { - commits database.CommitManager - repos database.RepoManager - users database.UserManager - sess session.Session -} - -func NewUserHandler(users database.UserManager, repos database.RepoManager, commits database.CommitManager, sess session.Session) *UserHandler { - return &UserHandler{commits, repos, users, sess} -} - -// 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{} +// GetUserCurrent accepts a request to retrieve the +// currently authenticated user from the datastore +// and return in JSON format. +// +// GET /api/user +// +func GetUserCurrent(c web.C, w http.ResponseWriter, r *http.Request) { + var user = ToUser(c) + if user == nil { + w.WriteHeader(http.StatusUnauthorized) + return } - // Normally the Token would not be serialized to json. - // In this case it is appropriate because the user is - // requesting their own data, and will need to display - // the Token on the website. + // return private data for the currently authenticated + // user, specifically, their auth token. data := struct { *model.User Token string `json:"token"` - }{u, u.Token} - return json.NewEncoder(w).Encode(&data) + }{user, user.Token} + json.NewEncoder(w).Encode(&data) } -// PutUser updates the authenticated user. -// PUT /api/user -func (h *UserHandler) PutUser(w http.ResponseWriter, r *http.Request) error { - // get the user form the session - u := h.sess.User(r) - if u == nil { - return notAuthorized{} +// PutUser accepts a request to update the currently +// authenticated User profile. +// +// PUT /api/user +// +func PutUser(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 } // unmarshal the repository from the payload defer r.Body.Close() in := model.User{} if err := json.NewDecoder(r.Body).Decode(&in); err != nil { - return badRequest{err} + w.WriteHeader(http.StatusBadRequest) + return } // update the user email if len(in.Email) != 0 { - u.SetEmail(in.Email) + user.SetEmail(in.Email) } // update the user full name if len(in.Name) != 0 { - u.Name = in.Name + user.Name = in.Name } // update the database - if err := h.users.Update(u); err != nil { - return internalServerError{err} + if err := datastore.PutUser(ctx, user); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return } - return json.NewEncoder(w).Encode(u) + json.NewEncoder(w).Encode(user) } -// GetRepos gets the authenticated user's repositories. -// GET /api/user/repos -func (h *UserHandler) GetRepos(w http.ResponseWriter, r *http.Request) error { - // get the user from the session - u := h.sess.User(r) - if u == nil { - return notAuthorized{} +// GetRepos accepts a request to get the currently +// authenticated user's repository list from the datastore, +// encoded and returned in JSON format. +// +// GET /api/user/repos +// +func GetUserRepos(c 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 } - - // get the user repositories - repos, err := h.repos.List(u.ID) + repos, err := datastore.GetRepoList(ctx, user) 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. -// GET /api/user/feed -func (h *UserHandler) GetFeed(w http.ResponseWriter, r *http.Request) error { - // get the user from the session - u := h.sess.User(r) - if u == nil { - return notAuthorized{} +// GetUserFeed accepts a request to get the user's latest +// build feed, across all repositories, from the datastore. +// The results are encoded and returned in JSON format. +// +// GET /api/user/feed +// +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 } - - // get the user commits - commits, err := h.commits.ListUser(u.ID) + repos, err := datastore.GetCommitListUser(ctx, user) if err != nil { - return badRequest{err} + w.WriteHeader(http.StatusNotFound) + return } - return json.NewEncoder(w).Encode(&commits) -} - -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)) + json.NewEncoder(w).Encode(&repos) } diff --git a/server/handler/users.go b/server/handler/users.go index e9c012871..75840ffa3 100644 --- a/server/handler/users.go +++ b/server/handler/users.go @@ -4,124 +4,127 @@ import ( "encoding/json" "net/http" - "github.com/drone/drone/server/database" - "github.com/drone/drone/server/session" + "github.com/drone/drone/server/datastore" "github.com/drone/drone/shared/model" - "github.com/gorilla/pat" + "github.com/goji/context" + "github.com/zenazn/goji/web" ) -type UsersHandler struct { - users database.UserManager - 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. +// GetUsers accepts a request to retrieve all users +// from the datastore and return encoded in JSON format. // -// DELETE /api/users/:host/:login -func (h *UsersHandler) DeleteUser(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) +// GET /api/users +// +func GetUserList(c web.C, w http.ResponseWriter, r *http.Request) { + var ctx = context.FromC(c) + var user = ToUser(c) switch { case user == nil: - return notAuthorized{} + w.WriteHeader(http.StatusUnauthorized) + return 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 { - 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 { - return badRequest{} + w.WriteHeader(http.StatusBadRequest) + return } - - if err := h.users.Delete(account); err != nil { - return badRequest{err} + if err := datastore.DelUser(ctx, account); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return } - - // return a 200 indicating deletion complete 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)) } diff --git a/server/handler/util.go b/server/handler/util.go deleted file mode 100644 index 46012b21d..000000000 --- a/server/handler/util.go +++ /dev/null @@ -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") -} diff --git a/server/handler/ws.go b/server/handler/ws.go index 6a26d03a8..abeebd162 100644 --- a/server/handler/ws.go +++ b/server/handler/ws.go @@ -1,204 +1 @@ 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)) -} diff --git a/server/main.go b/server/main.go index 0a277313d..4b63f6f98 100644 --- a/server/main.go +++ b/server/main.go @@ -5,28 +5,32 @@ import ( "flag" "fmt" "net/http" - "runtime" "strings" "github.com/drone/config" - "github.com/drone/drone/server/database" - "github.com/drone/drone/server/database/schema" + //"github.com/drone/drone/server/database" "github.com/drone/drone/server/handler" - "github.com/drone/drone/server/pubsub" - "github.com/drone/drone/server/session" - "github.com/drone/drone/server/worker" + "github.com/drone/drone/server/middleware" + //"github.com/drone/drone/server/pubsub" + //"github.com/drone/drone/server/session" + //"github.com/drone/drone/server/worker" "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/justinas/nosurf" - "github.com/GeertJohan/go.rice" - _ "github.com/mattn/go-sqlite3" - "github.com/russross/meddler" + //"github.com/GeertJohan/go.rice" + "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/github" "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 ( @@ -58,6 +62,8 @@ var ( open bool nodes StringArr + + db *sql.DB ) func main() { @@ -65,12 +71,8 @@ func main() { flag.StringVar(&conf, "config", "", "") flag.StringVar(&prefix, "prefix", "DRONE_", "") - flag.StringVar(&port, "port", ":8080", "") flag.StringVar(&driver, "driver", "sqlite3", "") flag.StringVar(&datasource, "datasource", "drone.sqlite", "") - flag.StringVar(&sslcert, "sslcert", "", "") - flag.StringVar(&sslkey, "sslkey", "", "") - flag.IntVar(&workers, "workers", runtime.NumCPU(), "") flag.Parse() config.Var(&nodes, "worker-nodes") @@ -85,86 +87,91 @@ func main() { github.Register() gitlab.Register() - // setup the database - meddler.Default = meddler.SQLite - db, _ := sql.Open(driver, datasource) - schema.Load(db) + // setup the database and cancel all pending + // commits in the system. + db = database.MustConnect(driver, datasource) + go database.NewCommitstore(db).KillCommits() - // setup the database managers - repos := database.NewRepoManager(db) - users := database.NewUserManager(db) - perms := database.NewPermManager(db) - commits := database.NewCommitManager(db) + goji.Get("/api/auth/:host", handler.GetLogin) + goji.Get("/api/badge/:host/:owner/:name/status.svg", handler.GetBadge) + goji.Get("/api/badge/:host/:owner/:name/cc.xml", handler.GetCC) + //goji.Get("/api/hook", handler.PostHook) + //goji.Put("/api/hook", handler.PostHook) + //goji.Post("/api/hook", handler.PostHook) - // message broker - pubsub := pubsub.NewPubSub() + repos := web.New() + 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 - go commits.CancelAll() + users := web.New() + 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) - workerc := make(chan chan *model.Request) - worker.NewDispatch(queue, workerc).Start() + user := web.New() + user.Use(middleware.RequireUser) + 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 // using the default DOCKER_HOST - 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 { - for _, node := range nodes { - println(node) - worker.NewWorker(workerc, users, repos, commits, pubsub, &model.Server{Host: node}).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 { + for _, node := range nodes { + 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 - if len(sslcert) != 0 { - panic(http.ListenAndServeTLS(port, sslcert, sslkey, nil)) - } else { - panic(http.ListenAndServe(port, nil)) + //if len(sslcert) != 0 { + // panic(http.ListenAndServeTLS(port, sslcert, sslkey, nil)) + //} else { + //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 diff --git a/server/middleware/context.go b/server/middleware/context.go new file mode 100644 index 000000000..c329da1ae --- /dev/null +++ b/server/middleware/context.go @@ -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 +} diff --git a/server/middleware/header.go b/server/middleware/header.go new file mode 100644 index 000000000..dbb60873d --- /dev/null +++ b/server/middleware/header.go @@ -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) +} diff --git a/server/middleware/repo.go b/server/middleware/repo.go new file mode 100644 index 000000000..57299334f --- /dev/null +++ b/server/middleware/repo.go @@ -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) +} diff --git a/server/middleware/user.go b/server/middleware/user.go new file mode 100644 index 000000000..ff35407f6 --- /dev/null +++ b/server/middleware/user.go @@ -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) +} diff --git a/server/session/session.go b/server/session/session.go index c48324ae4..ab31c5fc0 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -2,97 +2,67 @@ package session import ( "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/model" "github.com/gorilla/securecookie" - "github.com/gorilla/sessions" ) -// stores sessions using secure cookies. -var cookies = sessions.NewCookieStore( - securecookie.GenerateRandomKey(64)) +// secret key used to create jwt +var secret = securecookie.GenerateRandomKey(32) -// stores sessions using secure cookies. -var xsrftoken = string(securecookie.GenerateRandomKey(32)) - -type Session interface { - User(r *http.Request) *model.User - 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 { +// GetUser gets the currently authenticated user for the +// http.Request. The user details will be stored as either +// a simple API token or JWT bearer token. +func GetUser(c context.Context, r *http.Request) *model.User { + var token = r.FormValue("access_token") switch { - case r.FormValue("access_token") == "": - return s.UserCookie(r) - case r.FormValue("access_token") != "": - return s.UserToken(r) + case len(token) == 0: + return nil + case len(token) == 32: + return getUserToken(c, r) + default: + return getUserBearer(c, r) } - return nil } -// UserXsrf gets the currently authenticated user and -// validates the xsrf session token, if necessary. -func (s *session) UserXsrf(r *http.Request) *model.User { - user := s.User(r) - if user == nil || r.FormValue("access_token") != "" { - return user - } - if !httputil.CheckXsrf(r, xsrftoken, user.Login) { - return nil - } +// GenerateToken generates a JWT token for the user session +// that can be appended to the #access_token segment to +// facilitate client-based OAuth2. +func GenerateToken(c context.Context, r *http.Request, user *model.User) (string, error) { + token := jwt.New(jwt.GetSigningMethod("HS256")) + token.Claims["user_id"] = user.ID + token.Claims["audience"] = httputil.GetURL(r) + token.Claims["expires"] = time.Now().UTC().Add(time.Hour * 72).Unix() + 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 } -// UserToken gets the currently authenticated user for the given auth token. -func (s *session) UserToken(r *http.Request) *model.User { - token := r.FormValue("access_token") - user, _ := s.users.FindToken(token) - return user -} - -// UserCookie gets the currently authenticated user from the secure cookie session. -func (s *session) UserCookie(r *http.Request) *model.User { - sess, err := cookies.Get(r, "_sess") - if err != nil { +// getUserBearer gets the currently authenticated user for the given +// bearer token (JWT) +func getUserBearer(c context.Context, r *http.Request) *model.User { + var tokenstr = r.FormValue("access_token") + var token, err = jwt.Parse(tokenstr, func(t *jwt.Token) (interface{}, error) { + return secret, nil + }) + if err != nil || token.Valid { return nil } - // get the uid from the session - value, ok := sess.Values["uid"] + var userid, ok = token.Claims["user_id"].(int64) if !ok { return nil } - // get the user from the database - user, _ := s.users.Find(value.(int64)) + var user, _ = datastore.GetUser(c, userid) 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) -} diff --git a/server/worker/context.go b/server/worker/context.go new file mode 100644 index 000000000..8e44e1fd0 --- /dev/null +++ b/server/worker/context.go @@ -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) +} diff --git a/server/worker/director/context.go b/server/worker/director/context.go new file mode 100644 index 000000000..9f6ab2f9d --- /dev/null +++ b/server/worker/director/context.go @@ -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) +} diff --git a/server/worker/director/director.go b/server/worker/director/director.go new file mode 100644 index 000000000..8b4f39533 --- /dev/null +++ b/server/worker/director/director.go @@ -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) +} diff --git a/server/worker/director/director_test.go b/server/worker/director/director_test.go new file mode 100644 index 000000000..601378208 --- /dev/null +++ b/server/worker/director/director_test.go @@ -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 +} diff --git a/server/worker/dispatch.go b/server/worker/dispatch.go deleted file mode 100644 index 614dcb6e7..000000000 --- a/server/worker/dispatch.go +++ /dev/null @@ -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 }() -} diff --git a/server/worker/docker/docker.go b/server/worker/docker/docker.go new file mode 100644 index 000000000..02168e642 --- /dev/null +++ b/server/worker/docker/docker.go @@ -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 = ¬ify.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) +} diff --git a/server/worker/pool/context.go b/server/worker/pool/context.go new file mode 100644 index 000000000..b0458dc83 --- /dev/null +++ b/server/worker/pool/context.go @@ -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) +} diff --git a/server/worker/pool/pool.go b/server/worker/pool/pool.go new file mode 100644 index 000000000..e2f3d91f6 --- /dev/null +++ b/server/worker/pool/pool.go @@ -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 +} diff --git a/server/worker/pool/pool_test.go b/server/worker/pool/pool_test.go new file mode 100644 index 000000000..11fdab5b8 --- /dev/null +++ b/server/worker/pool/pool_test.go @@ -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) {} diff --git a/server/worker/work.go b/server/worker/work.go new file mode 100644 index 000000000..21c169948 --- /dev/null +++ b/server/worker/work.go @@ -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 +} diff --git a/server/worker/worker.go b/server/worker/worker.go index 47ac54a16..fa99c499d 100644 --- a/server/worker/worker.go +++ b/server/worker/worker.go @@ -1,184 +1,15 @@ package worker import ( - "log" - "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" + "code.google.com/p/go.net/context" ) type Worker interface { - Start() // Start instructs the worker to start processing requests - Stop() // Stop instructions the worker to stop processing requests + Do(context.Context, *Work) } -type worker struct { - users database.UserManager - repos database.RepoManager - commits database.CommitManager - //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 = ¬ify.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) +// Do retrieves a worker from the session and uses +// it to get work done. +func Do(c context.Context, w *Work) { + FromContext(c).Do(c, w) } diff --git a/shared/httputil/httputil.go b/shared/httputil/httputil.go index 395bca3ee..a71dc73d9 100644 --- a/shared/httputil/httputil.go +++ b/shared/httputil/httputil.go @@ -3,8 +3,6 @@ package httputil import ( "net/http" "strings" - - "code.google.com/p/xsrftoken" ) // 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) } - -// 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, "/") -}