Added public project attribute & worker access api endpoints

This commit is contained in:
simon987 2019-01-24 20:39:17 -05:00
parent 1d656099f5
commit f250a2180c
16 changed files with 432 additions and 70 deletions

View File

@ -50,6 +50,9 @@ func New() *WebAPI {
api.router.POST("/worker/create", LogRequestMiddleware(api.WorkerCreate))
api.router.GET("/worker/get/:id", LogRequestMiddleware(api.WorkerGet))
api.router.POST("/access/grant", LogRequestMiddleware(api.WorkerGrantAccess))
api.router.POST("/access/remove", LogRequestMiddleware(api.WorkerRemoveAccess))
api.router.POST("/project/create", LogRequestMiddleware(api.ProjectCreate))
api.router.GET("/project/get/:id", LogRequestMiddleware(api.ProjectGet))
api.router.GET("/project/stats/:id", LogRequestMiddleware(api.ProjectGetStats))

View File

@ -13,6 +13,7 @@ type CreateProjectRequest struct {
Version string `json:"version"`
Priority int64 `json:"priority"`
Motd string `json:"motd"`
Public bool `json:"public"`
}
type CreateProjectResponse struct {
@ -51,6 +52,7 @@ func (api *WebAPI) ProjectCreate(r *Request) {
GitRepo: createReq.GitRepo,
Priority: createReq.Priority,
Motd: createReq.Motd,
Public: createReq.Public,
}
if isValidProject(project) {

View File

@ -13,6 +13,7 @@ type CreateTaskRequest struct {
MaxRetries int64 `json:"max_retries"`
Recipe string `json:"recipe"`
Priority int64 `json:"priority"`
MaxAssignTime int64 `json:"max_assign_time"`
}
type ReleaseTaskRequest struct {
@ -46,6 +47,8 @@ func (api *WebAPI) TaskCreate(r *Request) {
MaxRetries: createReq.MaxRetries,
Recipe: createReq.Recipe,
Priority: createReq.Priority,
AssignTime: 0,
MaxAssignTime: createReq.MaxAssignTime,
}
if isTaskValid(task) {
@ -99,12 +102,23 @@ func (api *WebAPI) TaskGetFromProject(r *Request) {
handleErr(err, r)
task := api.Database.GetTaskFromProject(worker, int64(project))
if task == nil {
r.OkJson(GetTaskResponse{
Ok: false,
Message: "No task available",
})
} else {
r.OkJson(GetTaskResponse{
Ok: true,
Task: task,
})
}
}
func (api *WebAPI) TaskGet(r *Request) {
worker, err := api.workerFromQueryArgs(r)

View File

@ -22,6 +22,16 @@ type GetWorkerResponse struct {
Worker *storage.Worker `json:"worker,omitempty"`
}
type WorkerAccessRequest struct {
WorkerId *uuid.UUID `json:"worker_id"`
ProjectId int64 `json:"project_id"`
}
type WorkerAccessResponse struct {
Ok bool `json:"ok"`
Message string `json:"message"`
}
func (api *WebAPI) WorkerCreate(r *Request) {
workerReq := &CreateWorkerRequest{}
@ -86,6 +96,46 @@ func (api *WebAPI) WorkerGet(r *Request) {
}
}
func (api *WebAPI) WorkerGrantAccess(r *Request) {
req := &WorkerAccessRequest{}
if r.GetJson(req) {
ok := api.Database.GrantAccess(req.WorkerId, req.ProjectId)
if ok {
r.OkJson(WorkerAccessResponse{
Ok: true,
})
} else {
r.OkJson(WorkerAccessResponse{
Ok: false,
Message: "Worker already has access to this project",
})
}
}
}
func (api *WebAPI) WorkerRemoveAccess(r *Request) {
req := &WorkerAccessRequest{}
if r.GetJson(req) {
ok := api.Database.RemoveAccess(req.WorkerId, req.ProjectId)
if ok {
r.OkJson(WorkerAccessResponse{
Ok: true,
})
} else {
r.OkJson(WorkerAccessResponse{
Ok: false,
Message: "Worker did not have access to this project",
})
}
}
}
func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.Identity) (uuid.UUID, error) {
worker := storage.Worker{

View File

@ -1,18 +1,20 @@
DROP TABLE IF EXISTS workeridentity, Worker, Project, Task, log_entry;
DROP TABLE IF EXISTS worker_identity, worker, project, task, log_entry,
worker_has_access_to_project;
DROP TYPE IF EXISTS status;
DROP TYPE IF EXISTS loglevel;
DROP TYPE IF EXISTS log_level;
CREATE TYPE status as ENUM (
'new',
'failed',
'closed'
'closed',
'timeout'
);
CREATE TYPE loglevel as ENUM (
CREATE TYPE log_level as ENUM (
'fatal', 'panic', 'error', 'warning', 'info', 'debug', 'trace'
);
CREATE TABLE workerIdentity
CREATE TABLE worker_identity
(
id SERIAL PRIMARY KEY,
remote_addr TEXT,
@ -24,6 +26,7 @@ CREATE TABLE workerIdentity
CREATE TABLE worker
(
id TEXT PRIMARY KEY,
alias TEXT DEFAULT NULL,
created INTEGER,
identity INTEGER REFERENCES workerIdentity (id)
);
@ -35,7 +38,16 @@ CREATE TABLE project
name TEXT UNIQUE,
clone_url TEXT,
git_repo TEXT UNIQUE,
version TEXT
version TEXT,
motd TEXT,
public boolean
);
CREATE TABLE worker_has_access_to_project
(
worker TEXT REFERENCES worker (id),
project INTEGER REFERENCES project (id),
primary key (worker, project)
);
CREATE TABLE task
@ -47,12 +59,14 @@ CREATE TABLE task
retries INTEGER DEFAULT 0,
max_retries INTEGER,
status Status DEFAULT 'new',
recipe TEXT
recipe TEXT,
max_assign_time INTEGER DEFAULT 0,
assign_time INTEGER DEFAULT 0
);
CREATE TABLE log_entry
(
level loglevel,
level log_level,
message TEXT,
message_data TEXT,
timestamp INT

View File

@ -15,6 +15,7 @@ type Project struct {
GitRepo string `json:"git_repo"`
Version string `json:"version"`
Motd string `json:"motd"`
Public bool `json:"public"`
}
type AssignedTasks struct {
@ -39,9 +40,9 @@ func (database *Database) SaveProject(project *Project) (int64, error) {
func saveProject(project *Project, db *sql.DB) (int64, error) {
row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority, motd)
VALUES ($1,$2,$3,$4,$5,$6) RETURNING id`,
project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd)
row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority, motd, public)
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id`,
project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd, project.Public)
var id int64
err := row.Scan(&id)
@ -93,8 +94,8 @@ func getProject(id int64, db *sql.DB) *Project {
func scanProject(row *sql.Row) (*Project, error) {
project := &Project{}
err := row.Scan(&project.Id, &project.Priority, &project.Motd, &project.Name, &project.CloneUrl,
&project.GitRepo, &project.Version)
err := row.Scan(&project.Id, &project.Priority, &project.Name, &project.CloneUrl,
&project.GitRepo, &project.Version, &project.Motd, &project.Public)
return project, err
}
@ -120,8 +121,8 @@ func (database *Database) UpdateProject(project *Project) {
db := database.getDB()
res, err := db.Exec(`UPDATE project
SET (priority, name, clone_url, git_repo, version, motd) = ($1,$2,$3,$4,$5,$6) WHERE id=$7`,
project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd, project.Id)
SET (priority, name, clone_url, git_repo, version, motd, public) = ($1,$2,$3,$4,$5,$6,$7) WHERE id=$8`,
project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd, project.Public, project.Id)
handleErr(err)
rowsAffected, _ := res.RowsAffected()
@ -156,6 +157,7 @@ func (database *Database) GetProjectStats(id int64) *ProjectStats {
return nil
}
//todo: only expose worker alias
rows, err := db.Query(`SELECT assignee, COUNT(*) FROM TASK
LEFT JOIN worker ON TASK.assignee = worker.id WHERE project=$1 GROUP BY assignee`, id)
@ -189,7 +191,7 @@ func (database Database) GetAllProjectsStats() *[]ProjectStats {
stats := ProjectStats{}
p := &Project{}
err := rows.Scan(&stats.NewTaskCount, &stats.FailedTaskCount, &stats.ClosedTaskCount,
&p.Id, &p.Priority, &p.Motd, &p.Name, &p.CloneUrl, &p.GitRepo, &p.Version)
&p.Id, &p.Priority, &p.Motd, &p.Name, &p.CloneUrl, &p.GitRepo, &p.Version, &p.Public)
handleErr(err)
stats.Project = p

View File

@ -15,6 +15,8 @@ type Task struct {
MaxRetries int64 `json:"max_retries"`
Status string `json:"status"`
Recipe string `json:"recipe"`
MaxAssignTime int64 `json:"max_assign_time"`
AssignTime int64 `json:"assign_time"`
}
func (database *Database) SaveTask(task *Task, project int64) error {
@ -22,9 +24,9 @@ func (database *Database) SaveTask(task *Task, project int64) error {
db := database.getDB()
res, err := db.Exec(`
INSERT INTO task (project, max_retries, recipe, priority)
VALUES ($1,$2,$3,$4)`,
project, task.MaxRetries, task.Recipe, task.Priority)
INSERT INTO task (project, max_retries, recipe, priority, max_assign_time)
VALUES ($1,$2,$3,$4,$5)`,
project, task.MaxRetries, task.Recipe, task.Priority, task.MaxAssignTime)
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"task": task,
@ -56,6 +58,9 @@ func (database *Database) GetTask(worker *Worker) *Task {
FROM task
INNER JOIN project p on task.project = p.id
WHERE assignee IS NULL
AND (p.public OR EXISTS (
SELECT 1 FROM worker_has_access_to_project a WHERE a.worker=$1 AND a.project=p.id
))
ORDER BY p.priority DESC, task.priority DESC
LIMIT 1
)
@ -134,6 +139,9 @@ func (database *Database) GetTaskFromProject(worker *Worker, projectId int64) *T
FROM task
INNER JOIN project p on task.project = p.id
WHERE assignee IS NULL AND p.id=$2
AND (p.public OR EXISTS (
SELECT 1 FROM worker_has_access_to_project a WHERE a.worker=$1 AND a.project=$2
))
ORDER BY task.priority DESC
LIMIT 1
)
@ -165,8 +173,9 @@ func scanTask(row *sql.Row) *Task {
task.Project = project
err := row.Scan(&task.Id, &task.Priority, &project.Id, &task.Assignee,
&task.Retries, &task.MaxRetries, &task.Status, &task.Recipe, &project.Id,
&project.Priority, &project.Motd, &project.Name, &project.CloneUrl, &project.GitRepo, &project.Version)
&task.Retries, &task.MaxRetries, &task.Status, &task.Recipe, &task.MaxAssignTime,
&task.AssignTime, &project.Id, &project.Priority, &project.Name,
&project.CloneUrl, &project.GitRepo, &project.Version, &project.Motd, &project.Public)
handleErr(err)
return task

View File

@ -101,3 +101,46 @@ func getOrCreateIdentity(identity *Identity, db *sql.DB) int64 {
return rowId
}
func (database *Database) GrantAccess(workerId *uuid.UUID, projectId int64) bool {
db := database.getDB()
res, err := db.Exec(`INSERT INTO worker_has_access_to_project (worker, project) VALUES ($1,$2)
ON CONFLICT DO NOTHING`,
workerId, projectId)
if err != nil {
logrus.WithFields(logrus.Fields{
"workerId": workerId,
"projectId": projectId,
}).WithError(err).Warn("Database.GrantAccess INSERT worker_hase_access_to_project")
return false
}
rowsAffected, _ := res.RowsAffected()
logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected,
"workerId": workerId,
"projectId": projectId,
}).Trace("Database.GrantAccess INSERT worker_has_access_to_project")
return rowsAffected == 1
}
func (database Database) RemoveAccess(workerId *uuid.UUID, projectId int64) bool {
db := database.getDB()
res, err := db.Exec(`DELETE FROM worker_has_access_to_project WHERE worker=$1 AND project=$2`,
workerId, projectId)
handleErr(err)
rowsAffected, _ := res.RowsAffected()
logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected,
"workerId": workerId,
"projectId": projectId,
}).Trace("Database.RemoveAccess DELETE worker_has_access_to_project")
return rowsAffected == 1
}

View File

@ -19,6 +19,7 @@ func TestCreateGetProject(t *testing.T) {
Version: "Test Version",
Priority: 123,
Motd: "motd",
Public: true,
})
id := resp.Id
@ -56,6 +57,9 @@ func TestCreateGetProject(t *testing.T) {
if getResp.Project.Motd != "motd" {
t.Error()
}
if getResp.Project.Public != true {
t.Error()
}
}
func TestCreateProjectInvalid(t *testing.T) {

View File

@ -129,6 +129,7 @@ func TestCreateGetTask(t *testing.T) {
CloneUrl: "http://github.com/test/test",
GitRepo: "myrepo",
Priority: 999,
Public: true,
})
createTask(api.CreateTaskRequest{
@ -170,6 +171,9 @@ func TestCreateGetTask(t *testing.T) {
if taskResp.Task.Project.CloneUrl != "http://github.com/test/test" {
t.Error()
}
if taskResp.Task.Project.Public != true {
t.Error()
}
}
func createTasks(prefix string) (int64, int64) {
@ -180,6 +184,7 @@ func createTasks(prefix string) (int64, int64) {
CloneUrl: "http://github.com/test/test",
GitRepo: prefix + "low1",
Priority: 1,
Public: true,
})
highP := createProject(api.CreateProjectRequest{
Name: prefix + "high",
@ -187,6 +192,7 @@ func createTasks(prefix string) (int64, int64) {
CloneUrl: "http://github.com/test/test",
GitRepo: prefix + "high1",
Priority: 999,
Public: true,
})
createTask(api.CreateTaskRequest{
Project: lowP.Id,
@ -266,6 +272,86 @@ func TestTaskPriority(t *testing.T) {
}
}
func TestTaskNoAccess(t *testing.T) {
wid := genWid()
pid := createProject(api.CreateProjectRequest{
Name: "This is a private proj",
Motd: "private",
Version: "private",
Priority: 1,
CloneUrl: "fjkslejf cesl",
GitRepo: "fffffffff",
Public: false,
}).Id
createResp := createTask(api.CreateTaskRequest{
Project: pid,
Priority: 1,
MaxAssignTime: 10,
MaxRetries: 2,
Recipe: "---",
})
if createResp.Ok != true {
t.Error()
}
grantAccess(wid, pid)
removeAccess(wid, pid)
tResp := getTaskFromProject(pid, wid)
if tResp.Ok != false {
t.Error()
}
if len(tResp.Message) <= 0 {
t.Error()
}
if tResp.Task != nil {
t.Error()
}
}
func TestTaskHasAccess(t *testing.T) {
wid := genWid()
pid := createProject(api.CreateProjectRequest{
Name: "This is a private proj1",
Motd: "private1",
Version: "private1",
Priority: 1,
CloneUrl: "josaeiuf cesl",
GitRepo: "wewwwwwwwwwwwwwwwwwwwwww",
Public: false,
}).Id
createResp := createTask(api.CreateTaskRequest{
Project: pid,
Priority: 1,
MaxAssignTime: 10,
MaxRetries: 2,
Recipe: "---",
})
if createResp.Ok != true {
t.Error()
}
grantAccess(wid, pid)
tResp := getTaskFromProject(pid, wid)
if tResp.Ok != true {
t.Error()
}
if tResp.Task == nil {
t.Error()
}
}
func TestNoMoreTasks(t *testing.T) {
wid := genWid()

View File

@ -66,6 +66,78 @@ func TestGetWorkerInvalid(t *testing.T) {
}
}
func TestGrantAccessFailedProjectConstraint(t *testing.T) {
wid := genWid()
resp := grantAccess(wid, 38274593)
if resp.Ok != false {
t.Error()
}
if len(resp.Message) <= 0 {
t.Error()
}
}
func TestRemoveAccessFailedProjectConstraint(t *testing.T) {
wid := genWid()
resp := removeAccess(wid, 38274593)
if resp.Ok != false {
t.Error()
}
if len(resp.Message) <= 0 {
t.Error()
}
}
func TestRemoveAccessFailedWorkerConstraint(t *testing.T) {
pid := createProject(api.CreateProjectRequest{
Priority: 1,
GitRepo: "dfffffffffff",
CloneUrl: "fffffffffff23r",
Version: "f83w9rw",
Motd: "ddddddddd",
Name: "removeaccessfailedworkerconstraint",
Public: true,
}).Id
resp := removeAccess(&uuid.Nil, pid)
if resp.Ok != false {
t.Error()
}
if len(resp.Message) <= 0 {
t.Error()
}
}
func TestGrantAccessFailedWorkerConstraint(t *testing.T) {
pid := createProject(api.CreateProjectRequest{
Priority: 1,
GitRepo: "dfffffffffff1",
CloneUrl: "fffffffffff23r1",
Version: "f83w9rw1",
Motd: "ddddddddd1",
Name: "grantaccessfailedworkerconstraint",
Public: true,
}).Id
resp := removeAccess(&uuid.Nil, pid)
if resp.Ok != false {
t.Error()
}
if len(resp.Message) <= 0 {
t.Error()
}
}
func createWorker(req api.CreateWorkerRequest) (*api.CreateWorkerResponse, *http.Response) {
r := Post("/worker/create", req)
@ -94,3 +166,33 @@ func genWid() *uuid.UUID {
resp, _ := createWorker(api.CreateWorkerRequest{})
return &resp.WorkerId
}
func grantAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse {
r := Post("/access/grant", api.WorkerAccessRequest{
WorkerId: wid,
ProjectId: project,
})
var resp *api.WorkerAccessResponse
data, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(data, &resp)
handleErr(err)
return resp
}
func removeAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse {
r := Post("/access/remove", api.WorkerAccessRequest{
WorkerId: wid,
ProjectId: project,
})
var resp *api.WorkerAccessResponse
data, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(data, &resp)
handleErr(err)
return resp
}

31
test/schema.sql Normal file → Executable file
View File

@ -1,18 +1,20 @@
DROP TABLE IF EXISTS workeridentity, Worker, Project, Task, log_entry;
DROP TABLE IF EXISTS worker_identity, worker, project, task, log_entry,
worker_has_access_to_project;
DROP TYPE IF EXISTS status;
DROP TYPE IF EXISTS loglevel;
DROP TYPE IF EXISTS log_level;
CREATE TYPE status as ENUM (
'new',
'failed',
'closed'
'closed',
'timeout'
);
CREATE TYPE loglevel as ENUM (
CREATE TYPE log_level as ENUM (
'fatal', 'panic', 'error', 'warning', 'info', 'debug', 'trace'
);
CREATE TABLE workerIdentity
CREATE TABLE worker_identity
(
id SERIAL PRIMARY KEY,
remote_addr TEXT,
@ -24,6 +26,7 @@ CREATE TABLE workerIdentity
CREATE TABLE worker
(
id TEXT PRIMARY KEY,
alias TEXT DEFAULT NULL,
created INTEGER,
identity INTEGER REFERENCES workerIdentity (id)
);
@ -32,11 +35,19 @@ CREATE TABLE project
(
id SERIAL PRIMARY KEY,
priority INTEGER DEFAULT 0,
motd TEXT DEFAULT '',
name TEXT UNIQUE,
clone_url TEXT,
git_repo TEXT UNIQUE,
version TEXT
version TEXT,
motd TEXT,
public boolean
);
CREATE TABLE worker_has_access_to_project
(
worker TEXT REFERENCES worker (id),
project INTEGER REFERENCES project (id),
primary key (worker, project)
);
CREATE TABLE task
@ -48,12 +59,14 @@ CREATE TABLE task
retries INTEGER DEFAULT 0,
max_retries INTEGER,
status Status DEFAULT 'new',
recipe TEXT
recipe TEXT,
max_assign_time INTEGER DEFAULT 0,
assign_time INTEGER DEFAULT 0
);
CREATE TABLE log_entry
(
level loglevel,
level log_level,
message TEXT,
message_data TEXT,
timestamp INT

View File

@ -10,12 +10,16 @@ import {
MatAutocompleteModule,
MatButtonModule,
MatCardModule,
MatCheckboxModule,
MatDividerModule,
MatExpansionModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatMenuModule,
MatPaginatorModule,
MatSliderModule,
MatSlideToggleModule,
MatSortModule,
MatTableModule,
MatToolbarModule,
@ -58,6 +62,11 @@ import {UpdateProjectComponent} from './update-project/update-project.component'
MatTreeModule,
BrowserAnimationsModule,
HttpClientModule,
MatSliderModule,
MatSlideToggleModule,
MatCheckboxModule,
MatDividerModule
],
exports: [],
providers: [

View File

@ -4,21 +4,30 @@
<mat-card-content>
<form>
<mat-form-field>
<input type="text" matInput [(ngModel)]="project.name" name="name" placeholder="Name">
<mat-form-field appearance="outline">
<mat-label>Project name</mat-label>
<input type="text" matInput [(ngModel)]="project.name" name="name" placeholder="Project name">
</mat-form-field>
<mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Git clone URL</mat-label>
<input type="text" matInput [(ngModel)]="project.clone_url" name="clone_url"
placeholder="Git clone url">
</mat-form-field>
<mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Repository name</mat-label>
<input type="text" matInput [(ngModel)]="project.git_repo" name="clone_url"
placeholder='Full repository name (e.g. "simon987/task_tracker")'>
<mat-hint align="start">Changes on the <strong>master</strong> branch will be tracked if webhooks are
enabled
<mat-hint align="start">
Changes on the <strong>master</strong> branch will be tracked if webhooks are enabled
</mat-hint>
</mat-form-field>
<mat-checkbox matInput [(ngModel)]="project.public" name="public" stype="padding-top: 1em">Public project
</mat-checkbox>
<input type="hidden" name="version" value="{{project.version}}">
</form>
</mat-card-content>

View File

@ -11,7 +11,8 @@ export class CreateProjectComponent implements OnInit {
private project = new Project();
constructor() {
this.project.name = "test"
this.project.name = "test";
this.project.public = true;
}
ngOnInit() {

View File

@ -6,4 +6,5 @@ export class Project {
public clone_url: string;
public git_repo: string;
public version: string;
public public: boolean;
}