mirror of
https://github.com/simon987/task_tracker.git
synced 2025-04-16 08:56:45 +00:00
Initial commit
This commit is contained in:
commit
83276ce8b0
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.idea/
|
25
api/error.go
Normal file
25
api/error.go
Normal file
@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
StackTrace string `json:"stack_trace"`
|
||||
|
||||
}
|
||||
|
||||
func handleErr(err error, r *Request) {
|
||||
|
||||
if err != nil {
|
||||
logrus.Error(err.Error())
|
||||
//debug.PrintStack()
|
||||
|
||||
r.Json(ErrorResponse{
|
||||
Message: err.Error(),
|
||||
StackTrace: string(debug.Stack()),
|
||||
}, 500)
|
||||
}
|
||||
}
|
48
api/helper.go
Normal file
48
api/helper.go
Normal file
@ -0,0 +1,48 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Ctx *fasthttp.RequestCtx
|
||||
}
|
||||
|
||||
|
||||
func (r *Request) OkJson(object interface{}) {
|
||||
|
||||
resp, err := json.Marshal(object)
|
||||
handleErr(err, r)
|
||||
|
||||
r.Ctx.Response.Header.Set("Content-Type", "application/json")
|
||||
_, err = r.Ctx.Write(resp)
|
||||
handleErr(err, r)
|
||||
}
|
||||
|
||||
func (r *Request) Json(object interface{}, code int) {
|
||||
|
||||
resp, err := json.Marshal(object)
|
||||
if err != nil {
|
||||
fmt.Fprint(r.Ctx,"Error during json encoding of error")
|
||||
logrus.Error("Error during json encoding of error")
|
||||
}
|
||||
|
||||
r.Ctx.Response.SetStatusCode(code)
|
||||
r.Ctx.Response.Header.Set("Content-Type", "application/json")
|
||||
_, err = r.Ctx.Write(resp)
|
||||
if err != nil {
|
||||
panic(err) //todo handle differently
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (r *Request) GetJson(x interface{}) bool {
|
||||
|
||||
err := json.Unmarshal(r.Ctx.Request.Body(), x)
|
||||
handleErr(err, r)
|
||||
|
||||
return err == nil
|
||||
}
|
91
api/log.go
Normal file
91
api/log.go
Normal file
@ -0,0 +1,91 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/valyala/fasthttp"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RequestHandler func(*Request)
|
||||
|
||||
type LogEntry struct {
|
||||
Scope string `json:"scope"`
|
||||
Message string `json:"Message"`
|
||||
TimeStamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
func (e *LogEntry) Time() time.Time {
|
||||
|
||||
t := time.Unix(e.TimeStamp, 0)
|
||||
return t
|
||||
}
|
||||
|
||||
func LogRequest(h RequestHandler) fasthttp.RequestHandler {
|
||||
return fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) {
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"path": string(ctx.Path()),
|
||||
}).Info(string(ctx.Method()))
|
||||
|
||||
h(&Request{Ctx: ctx})
|
||||
})
|
||||
}
|
||||
|
||||
func SetupLogger() {
|
||||
logrus.SetLevel(logrus.TraceLevel) //todo: from conf
|
||||
}
|
||||
|
||||
func parseLogEntry(r *Request) *LogEntry {
|
||||
|
||||
entry := LogEntry{}
|
||||
|
||||
if r.GetJson(&entry) {
|
||||
if len(entry.Message) == 0 {
|
||||
handleErr(errors.New("invalid message"), r)
|
||||
} else if len(entry.Scope) == 0 {
|
||||
handleErr(errors.New("invalid scope"), r)
|
||||
} else if entry.TimeStamp <= 0 {
|
||||
handleErr(errors.New("invalid timestamp"), r)
|
||||
}
|
||||
}
|
||||
|
||||
return &entry
|
||||
}
|
||||
|
||||
func LogTrace(r *Request) {
|
||||
|
||||
entry := parseLogEntry(r)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"scope": entry.Scope,
|
||||
}).WithTime(entry.Time()).Trace(entry.Message)
|
||||
}
|
||||
|
||||
func LogInfo(r *Request) {
|
||||
|
||||
entry := parseLogEntry(r)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"scope": entry.Scope,
|
||||
}).WithTime(entry.Time()).Info(entry.Message)
|
||||
}
|
||||
|
||||
func LogWarn(r *Request) {
|
||||
|
||||
entry := parseLogEntry(r)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"scope": entry.Scope,
|
||||
}).WithTime(entry.Time()).Warn(entry.Message)
|
||||
}
|
||||
|
||||
func LogError(r *Request) {
|
||||
|
||||
entry := parseLogEntry(r)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"scope": entry.Scope,
|
||||
}).WithTime(entry.Time()).Error(entry.Message)
|
||||
}
|
||||
|
71
api/main.go
Normal file
71
api/main.go
Normal file
@ -0,0 +1,71 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/buaazp/fasthttprouter"
|
||||
"github.com/valyala/fasthttp"
|
||||
"src/task_tracker/config"
|
||||
"src/task_tracker/storage"
|
||||
)
|
||||
|
||||
type WebAPI struct {
|
||||
server *fasthttp.Server
|
||||
router *fasthttprouter.Router
|
||||
Database *storage.Database
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
var info = Info {
|
||||
Name: "task_tracker",
|
||||
Version: "1.0",
|
||||
}
|
||||
|
||||
func Index(r *Request) {
|
||||
r.OkJson(info)
|
||||
}
|
||||
|
||||
func New() *WebAPI {
|
||||
|
||||
SetupLogger()
|
||||
|
||||
api := new(WebAPI)
|
||||
|
||||
api.router = &fasthttprouter.Router{}
|
||||
|
||||
api.server = &fasthttp.Server{
|
||||
Handler: api.router.Handler,
|
||||
Name: info.Name,
|
||||
}
|
||||
|
||||
api.router.GET("/", LogRequest(Index))
|
||||
|
||||
api.router.POST("/log/trace", LogRequest(LogTrace))
|
||||
api.router.POST("/log/info", LogRequest(LogInfo))
|
||||
api.router.POST("/log/warn", LogRequest(LogWarn))
|
||||
api.router.POST("/log/error", LogRequest(LogError))
|
||||
|
||||
api.router.POST("/worker/create", LogRequest(api.WorkerCreate))
|
||||
api.router.GET("/worker/get/:id", LogRequest(api.WorkerGet))
|
||||
|
||||
api.router.POST("/project/create", LogRequest(api.ProjectCreate))
|
||||
api.router.GET("/project/get/:id", LogRequest(api.ProjectGet))
|
||||
|
||||
api.router.POST("/task/create", LogRequest(api.TaskCreate))
|
||||
api.router.GET("/task/get/", LogRequest(api.TaskGet))
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
func (api *WebAPI) Run() {
|
||||
|
||||
logrus.Infof("Started web api at address %s", config.Cfg.ServerAddr)
|
||||
|
||||
err := api.server.ListenAndServe(config.Cfg.ServerAddr)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Error in ListenAndServe: %s", err)
|
||||
}
|
||||
}
|
92
api/project.go
Normal file
92
api/project.go
Normal file
@ -0,0 +1,92 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
"src/task_tracker/storage"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name"`
|
||||
GitUrl string `json:"git_url"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type CreateProjectResponse struct {
|
||||
Ok bool `json:"ok"`
|
||||
Id int64 `json:"id,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type GetProjectResponse struct {
|
||||
Ok bool `json:"ok"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Project *storage.Project `json:"project,omitempty"`
|
||||
}
|
||||
|
||||
func (api *WebAPI) ProjectCreate(r *Request) {
|
||||
|
||||
createReq := &CreateProjectRequest{}
|
||||
if r.GetJson(createReq) {
|
||||
|
||||
project := &storage.Project{
|
||||
Name: createReq.Name,
|
||||
Version: createReq.Version,
|
||||
GitUrl: createReq.GitUrl,
|
||||
}
|
||||
|
||||
if isValidProject(project) {
|
||||
id, err := api.Database.SaveProject(project)
|
||||
|
||||
if err != nil {
|
||||
r.Json(CreateProjectResponse{
|
||||
Ok: false,
|
||||
Message:err.Error(),
|
||||
}, 500)
|
||||
} else {
|
||||
r.OkJson(CreateProjectResponse{
|
||||
Ok: true,
|
||||
Id: id,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"project": project,
|
||||
}).Warn("Invalid project")
|
||||
|
||||
r.Json(CreateProjectResponse{
|
||||
Ok: false,
|
||||
Message: "Invalid project",
|
||||
}, 400)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func isValidProject(project *storage.Project) bool {
|
||||
if len(project.Name) <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (api *WebAPI) ProjectGet(r *Request) {
|
||||
|
||||
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
||||
handleErr(err, r)
|
||||
|
||||
project := api.Database.GetProject(id)
|
||||
|
||||
if project != nil {
|
||||
r.OkJson(GetProjectResponse{
|
||||
Ok: true,
|
||||
Project:project,
|
||||
})
|
||||
} else {
|
||||
r.Json(GetProjectResponse{
|
||||
Ok: false,
|
||||
Message: "Project not found",
|
||||
}, 404)
|
||||
}
|
||||
}
|
73
api/task.go
Normal file
73
api/task.go
Normal file
@ -0,0 +1,73 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
"src/task_tracker/storage"
|
||||
)
|
||||
|
||||
type CreateTaskRequest struct {
|
||||
Project int64 `json:"project"`
|
||||
MaxRetries int64 `json:"max_retries"`
|
||||
Recipe string `json:"recipe"`
|
||||
}
|
||||
|
||||
type CreateTaskResponse struct {
|
||||
Ok bool
|
||||
Message string
|
||||
}
|
||||
|
||||
func (api *WebAPI) TaskCreate(r *Request) {
|
||||
|
||||
var createReq CreateTaskRequest
|
||||
if r.GetJson(&createReq) {
|
||||
|
||||
task := &storage.Task{
|
||||
Project:createReq.Project,
|
||||
MaxRetries: createReq.MaxRetries,
|
||||
Recipe:createReq.Recipe,
|
||||
}
|
||||
|
||||
if isTaskValid(task) {
|
||||
err := api.Database.SaveTask(task)
|
||||
|
||||
if err != nil {
|
||||
r.Json(CreateTaskResponse{
|
||||
Ok: false,
|
||||
Message: err.Error(), //todo: hide sensitive error?
|
||||
}, 500)
|
||||
} else {
|
||||
r.OkJson(CreateTaskResponse{
|
||||
Ok: true,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"task": task,
|
||||
}).Warn("Invalid task")
|
||||
r.Json(CreateTaskResponse{
|
||||
Ok: false,
|
||||
Message: "Invalid task",
|
||||
}, 400)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func isTaskValid(task *storage.Task) bool {
|
||||
if task.MaxRetries < 0 {
|
||||
return false
|
||||
}
|
||||
if task.Project <= 0 {
|
||||
return false
|
||||
}
|
||||
if len(task.Recipe) <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (api *WebAPI) TaskGet(r *Request) {
|
||||
|
||||
|
||||
}
|
105
api/worker.go
Normal file
105
api/worker.go
Normal file
@ -0,0 +1,105 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/google/uuid"
|
||||
"src/task_tracker/storage"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CreateWorkerRequest struct {
|
||||
}
|
||||
|
||||
type CreateWorkerResponse struct {
|
||||
Ok bool `json:"ok"`
|
||||
Message string `json:"message,omitempty"`
|
||||
WorkerId string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type GetWorkerResponse struct {
|
||||
Ok bool `json:"ok"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Worker *storage.Worker `json:"worker,omitempty"`
|
||||
}
|
||||
|
||||
func (api *WebAPI) WorkerCreate(r *Request) {
|
||||
|
||||
workerReq := &CreateWorkerRequest{}
|
||||
if r.GetJson(workerReq) {
|
||||
identity := getIdentity(r)
|
||||
|
||||
if canCreateWorker(r, workerReq, identity) {
|
||||
|
||||
id, err := api.workerCreate(workerReq, getIdentity(r))
|
||||
if err != nil {
|
||||
handleErr(err, r)
|
||||
} else {
|
||||
r.OkJson(CreateWorkerResponse{
|
||||
Ok: true,
|
||||
WorkerId: id.String(),
|
||||
})
|
||||
}
|
||||
|
||||
} else {
|
||||
r.Json(CreateWorkerResponse{
|
||||
Ok: false,
|
||||
Message: "You are now allowed to create a worker",
|
||||
}, 403)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (api *WebAPI) WorkerGet(r *Request) {
|
||||
|
||||
id, err := uuid.Parse(r.Ctx.UserValue("id").(string))
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
}).Warn("Invalid UUID")
|
||||
|
||||
r.Json(GetWorkerResponse{
|
||||
Ok: false,
|
||||
Message:err.Error(),
|
||||
}, 400)
|
||||
return
|
||||
}
|
||||
|
||||
worker := api.Database.GetWorker(id)
|
||||
|
||||
if worker != nil {
|
||||
r.OkJson(GetWorkerResponse{
|
||||
Ok: true,
|
||||
Worker:worker,
|
||||
})
|
||||
} else {
|
||||
r.Json(GetWorkerResponse{
|
||||
Ok: false,
|
||||
Message:"Worker not found",
|
||||
}, 404)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.Identity) (uuid.UUID, error) {
|
||||
|
||||
worker := storage.Worker{
|
||||
Id: uuid.New(),
|
||||
Created: time.Now().Unix(),
|
||||
Identity: identity,
|
||||
}
|
||||
|
||||
api.Database.SaveWorker(&worker)
|
||||
return worker.Id, nil
|
||||
}
|
||||
|
||||
func canCreateWorker(r *Request, cwr *CreateWorkerRequest, identity *storage.Identity) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func getIdentity(r *Request) *storage.Identity {
|
||||
|
||||
identity := storage.Identity{
|
||||
RemoteAddr: r.Ctx.RemoteAddr().String(),
|
||||
}
|
||||
|
||||
return &identity
|
||||
}
|
7
config.yml
Normal file
7
config.yml
Normal file
@ -0,0 +1,7 @@
|
||||
server:
|
||||
address: "127.0.0.1:5000"
|
||||
test_address: "127.0.0.1:5001"
|
||||
|
||||
database:
|
||||
conn_str : "user=task_tracker dbname=task_tracker sslmode=disable"
|
||||
test_conn_str : "user=task_tracker dbname=task_tracker_test sslmode=disable"
|
24
config/config.go
Normal file
24
config/config.go
Normal file
@ -0,0 +1,24 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var Cfg struct {
|
||||
ServerAddr string
|
||||
DbConnStr string
|
||||
}
|
||||
|
||||
func SetupConfig() {
|
||||
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigName("")
|
||||
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
Cfg.ServerAddr = viper.GetString("server.address")
|
||||
Cfg.DbConnStr = viper.GetString("database.conn_str")
|
||||
}
|
24
main/main.go
Normal file
24
main/main.go
Normal file
@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"src/task_tracker/api"
|
||||
"src/task_tracker/config"
|
||||
"src/task_tracker/storage"
|
||||
)
|
||||
|
||||
func tmpDebugSetup() {
|
||||
|
||||
db := storage.Database{}
|
||||
db.Reset()
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
config.SetupConfig()
|
||||
|
||||
tmpDebugSetup()
|
||||
|
||||
webApi := api.New()
|
||||
webApi.Run()
|
||||
}
|
43
schema.sql
Normal file
43
schema.sql
Normal file
@ -0,0 +1,43 @@
|
||||
DROP TABLE IF EXISTS workerIdentity, Worker, Project, Task;
|
||||
DROP TYPE IF EXISTS Status;
|
||||
|
||||
CREATE TYPE status as ENUM (
|
||||
'new',
|
||||
'failed',
|
||||
'closed'
|
||||
);
|
||||
|
||||
CREATE TABLE workerIdentity
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
remote_addr TEXT,
|
||||
|
||||
UNIQUE (remote_addr)
|
||||
);
|
||||
|
||||
CREATE TABLE worker
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
created INTEGER,
|
||||
identity INTEGER REFERENCES workerIdentity(id)
|
||||
);
|
||||
|
||||
CREATE TABLE project
|
||||
(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT UNIQUE,
|
||||
git_url TEXT,
|
||||
version TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE task
|
||||
(
|
||||
id TEXT PRIMARY KEY,
|
||||
project INTEGER REFERENCES project (id),
|
||||
assignee TEXT REFERENCES worker (id),
|
||||
retries INTEGER DEFAULT 0,
|
||||
max_retries INTEGER,
|
||||
status Status DEFAULT 'new'
|
||||
);
|
||||
|
||||
|
53
storage/database.go
Normal file
53
storage/database.go
Normal file
@ -0,0 +1,53 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/Sirupsen/logrus"
|
||||
_ "github.com/lib/pq"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"src/task_tracker/config"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
}
|
||||
|
||||
func (database *Database) Reset() {
|
||||
|
||||
file, err := os.Open("./schema.sql")
|
||||
handleErr(err)
|
||||
|
||||
buffer, err := ioutil.ReadAll(file)
|
||||
handleErr(err)
|
||||
|
||||
db := database.getDB()
|
||||
_, err = db.Exec(string(buffer))
|
||||
handleErr(err)
|
||||
|
||||
db.Close()
|
||||
file.Close()
|
||||
|
||||
logrus.Info("Database has been reset")
|
||||
}
|
||||
|
||||
func (database *Database) getDB () *sql.DB {
|
||||
db, err := sql.Open("postgres", config.Cfg.DbConnStr)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (database *Database) Test() {
|
||||
|
||||
db := database.getDB()
|
||||
|
||||
rows, err := db.Query("SELECT name FROM Task")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
fmt.Println(rows)
|
||||
|
||||
}
|
7
storage/error.go
Normal file
7
storage/error.go
Normal file
@ -0,0 +1,7 @@
|
||||
package storage
|
||||
|
||||
func handleErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
77
storage/project.go
Normal file
77
storage/project.go
Normal file
@ -0,0 +1,77 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
GitUrl string `json:"git_url"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func (database *Database) SaveProject(project *Project) (int64, error) {
|
||||
db := database.getDB()
|
||||
id, projectErr := saveProject(project, db)
|
||||
err := db.Close()
|
||||
handleErr(err)
|
||||
|
||||
return id, projectErr
|
||||
}
|
||||
|
||||
func saveProject(project *Project, db *sql.DB) (int64, error) {
|
||||
|
||||
row := db.QueryRow("INSERT INTO project (name, git_url, version) VALUES ($1,$2,$3) RETURNING id",
|
||||
project.Name, project.GitUrl, project.Version)
|
||||
|
||||
var id int64
|
||||
err := row.Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithFields(logrus.Fields{
|
||||
"project": project,
|
||||
}).Warn("Database.saveProject INSERT project ERROR")
|
||||
return -1, err
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
"project": project,
|
||||
}).Trace("Database.saveProject INSERT project")
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (database *Database) GetProject(id int64) *Project {
|
||||
|
||||
db := database.getDB()
|
||||
project := getProject(id, db)
|
||||
err := db.Close()
|
||||
handleErr(err)
|
||||
return project
|
||||
}
|
||||
|
||||
func getProject(id int64, db *sql.DB) *Project {
|
||||
|
||||
project := &Project{}
|
||||
|
||||
row := db.QueryRow("SELECT id, name, git_url, version FROM project WHERE id=$1",
|
||||
id)
|
||||
|
||||
err := row.Scan(&project.Id, &project.Name, &project.GitUrl, &project.Version)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
}).Warn("Database.getProject SELECT project NOT FOUND")
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
"project": project,
|
||||
}).Trace("Database.saveProject SELECT project")
|
||||
|
||||
return project
|
||||
}
|
50
storage/task.go
Normal file
50
storage/task.go
Normal file
@ -0,0 +1,50 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
Id int64
|
||||
Project int64
|
||||
Assignee uuid.UUID
|
||||
Retries int64
|
||||
MaxRetries int64
|
||||
Status string
|
||||
Recipe string
|
||||
}
|
||||
|
||||
func (database *Database) SaveTask(task *Task) error {
|
||||
|
||||
db := database.getDB()
|
||||
taskErr := saveTask(task, db)
|
||||
err := db.Close()
|
||||
handleErr(err)
|
||||
|
||||
return taskErr
|
||||
}
|
||||
|
||||
func saveTask(task *Task, db *sql.DB) error {
|
||||
|
||||
res, err := db.Exec("INSERT INTO task (project, max_retries, recipe) "+
|
||||
"VALUES ($1,$2,$3)",
|
||||
task.Project, task.MaxRetries, task.Recipe)
|
||||
if err != nil {
|
||||
logrus.WithError(err).WithFields(logrus.Fields{
|
||||
"task": task,
|
||||
}).Warn("Database.saveTask INSERT task ERROR")
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
handleErr(err)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"rowsAffected": rowsAffected,
|
||||
"task": task,
|
||||
}).Trace("Database.saveTask INSERT task")
|
||||
|
||||
return nil
|
||||
}
|
116
storage/worker.go
Normal file
116
storage/worker.go
Normal file
@ -0,0 +1,116 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
RemoteAddr string
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
Id uuid.UUID `json:"id"`
|
||||
Created int64 `json:"created"`
|
||||
Identity *Identity `json:"identity"`
|
||||
}
|
||||
|
||||
func (database *Database) SaveWorker(worker *Worker) {
|
||||
|
||||
db := database.getDB()
|
||||
saveWorker(worker, db)
|
||||
err := db.Close()
|
||||
handleErr(err)
|
||||
}
|
||||
|
||||
func (database *Database) GetWorker(id uuid.UUID) *Worker {
|
||||
|
||||
db := database.getDB()
|
||||
worker := getWorker(id, db)
|
||||
err := db.Close()
|
||||
handleErr(err)
|
||||
return worker
|
||||
}
|
||||
|
||||
func saveWorker(worker *Worker, db *sql.DB) {
|
||||
|
||||
identityId := getOrCreateIdentity(worker.Identity, db)
|
||||
|
||||
res, err := db.Exec("INSERT INTO worker (id, created, identity) VALUES ($1,$2,$3)",
|
||||
worker.Id, worker.Created, identityId)
|
||||
handleErr(err)
|
||||
|
||||
var rowsAffected, _ = res.RowsAffected()
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"rowsAffected": rowsAffected,
|
||||
}).Trace("Database.saveWorker INSERT worker")
|
||||
}
|
||||
|
||||
func getWorker(id uuid.UUID, db *sql.DB) *Worker {
|
||||
|
||||
worker := &Worker{}
|
||||
var identityId int64
|
||||
|
||||
row := db.QueryRow("SELECT id, created, identity FROM worker WHERE id=$1", id)
|
||||
err := row.Scan(&worker.Id, &worker.Created, &identityId)
|
||||
if err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"id": id,
|
||||
}).Warn("Database.getWorker SELECT worker NOT FOUND")
|
||||
return nil
|
||||
}
|
||||
|
||||
worker.Identity, err = getIdentity(identityId, db)
|
||||
handleErr(err)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"worker": worker,
|
||||
}).Trace("Database.getWorker SELECT worker")
|
||||
|
||||
return worker
|
||||
}
|
||||
|
||||
func getIdentity(id int64, db *sql.DB) (*Identity, error) {
|
||||
|
||||
identity := &Identity{}
|
||||
|
||||
row := db.QueryRow("SELECT (remote_addr) FROM workeridentity WHERE id=$1", id)
|
||||
err := row.Scan(&identity.RemoteAddr)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.New("identity not found")
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"identity": identity,
|
||||
}).Trace("Database.getIdentity SELECT workerIdentity")
|
||||
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
func getOrCreateIdentity(identity *Identity, db *sql.DB) int64 {
|
||||
|
||||
res, err := db.Exec("INSERT INTO workeridentity (remote_addr) VALUES ($1) ON CONFLICT DO NOTHING",
|
||||
identity.RemoteAddr)
|
||||
handleErr(err)
|
||||
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"rowsAffected": rowsAffected,
|
||||
}).Trace("Database.saveWorker INSERT workerIdentity")
|
||||
|
||||
row := db.QueryRow("SELECT (id) FROM workeridentity WHERE remote_addr=$1", identity.RemoteAddr)
|
||||
|
||||
var rowId int64
|
||||
err = row.Scan(&rowId)
|
||||
handleErr(err)
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"rowId": rowId,
|
||||
}).Trace("Database.saveWorker SELECT workerIdentity")
|
||||
|
||||
return rowId
|
||||
}
|
||||
|
110
test/api_log_test.go
Normal file
110
test/api_log_test.go
Normal file
@ -0,0 +1,110 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"src/task_tracker/api"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
func TestTraceValid(t *testing.T) {
|
||||
|
||||
r := Post("/log/trace", api.LogEntry{
|
||||
Scope:"test",
|
||||
Message:"This is a test message",
|
||||
TimeStamp: time.Now().Unix(),
|
||||
})
|
||||
|
||||
if r.StatusCode != 200 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceInvalidScope(t *testing.T) {
|
||||
r := Post("/log/trace", api.LogEntry{
|
||||
Message:"this is a test message",
|
||||
TimeStamp: time.Now().Unix(),
|
||||
})
|
||||
|
||||
if r.StatusCode != 500 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
r = Post("/log/trace", api.LogEntry{
|
||||
Scope:"",
|
||||
Message:"this is a test message",
|
||||
TimeStamp: time.Now().Unix(),
|
||||
})
|
||||
|
||||
if r.StatusCode != 500 {
|
||||
t.Fail()
|
||||
}
|
||||
if GenericJson(r.Body)["message"] != "invalid scope" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceInvalidMessage(t *testing.T) {
|
||||
r := Post("/log/trace", api.LogEntry{
|
||||
Scope:"test",
|
||||
Message:"",
|
||||
TimeStamp: time.Now().Unix(),
|
||||
})
|
||||
|
||||
if r.StatusCode != 500 {
|
||||
t.Fail()
|
||||
}
|
||||
if GenericJson(r.Body)["message"] != "invalid message" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraceInvalidTime(t *testing.T) {
|
||||
r := Post("/log/trace", api.LogEntry{
|
||||
Scope: "test",
|
||||
Message:"test",
|
||||
|
||||
})
|
||||
if r.StatusCode != 500 {
|
||||
t.Fail()
|
||||
}
|
||||
if GenericJson(r.Body)["message"] != "invalid timestamp" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarnValid(t *testing.T) {
|
||||
|
||||
r := Post("/log/warn", api.LogEntry{
|
||||
Scope: "test",
|
||||
Message:"test",
|
||||
TimeStamp:time.Now().Unix(),
|
||||
})
|
||||
if r.StatusCode != 200 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoValid(t *testing.T) {
|
||||
|
||||
r := Post("/log/info", api.LogEntry{
|
||||
Scope: "test",
|
||||
Message:"test",
|
||||
TimeStamp:time.Now().Unix(),
|
||||
})
|
||||
if r.StatusCode != 200 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorValid(t *testing.T) {
|
||||
|
||||
r := Post("/log/error", api.LogEntry{
|
||||
Scope: "test",
|
||||
Message:"test",
|
||||
TimeStamp:time.Now().Unix(),
|
||||
})
|
||||
if r.StatusCode != 200 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
114
test/api_project_test.go
Normal file
114
test/api_project_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"src/task_tracker/api"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateGetProject(t *testing.T) {
|
||||
|
||||
resp := createProject(api.CreateProjectRequest{
|
||||
Name: "Test name",
|
||||
GitUrl: "http://github.com/test/test",
|
||||
Version: "Test Version",
|
||||
})
|
||||
|
||||
id := resp.Id
|
||||
|
||||
if id == 0 {
|
||||
t.Fail()
|
||||
}
|
||||
if resp.Ok != true {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
getResp, _ := getProject(id)
|
||||
|
||||
if getResp.Project.Id != id {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if getResp.Project.Name != "Test name" {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if getResp.Project.Version != "Test Version" {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if getResp.Project.GitUrl != "http://github.com/test/test" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProjectInvalid(t *testing.T) {
|
||||
resp := createProject(api.CreateProjectRequest{
|
||||
|
||||
})
|
||||
|
||||
if resp.Ok != false {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDuplicateProject(t *testing.T) {
|
||||
createProject(api.CreateProjectRequest{
|
||||
Name: "duplicate name",
|
||||
})
|
||||
resp := createProject(api.CreateProjectRequest{
|
||||
Name: "duplicate name",
|
||||
})
|
||||
|
||||
if resp.Ok != false {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if len(resp.Message) <= 0 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProjectNotFound(t *testing.T) {
|
||||
|
||||
getResp, r := getProject(12345)
|
||||
|
||||
if getResp.Ok != false {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if len(getResp.Message) <= 0 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if r.StatusCode != 404 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func createProject(req api.CreateProjectRequest) *api.CreateProjectResponse {
|
||||
|
||||
r := Post("/project/create", req)
|
||||
|
||||
var resp api.CreateProjectResponse
|
||||
data, _ := ioutil.ReadAll(r.Body)
|
||||
err := json.Unmarshal(data, &resp)
|
||||
handleErr(err)
|
||||
|
||||
return &resp
|
||||
}
|
||||
|
||||
func getProject(id int64) (*api.GetProjectResponse, *http.Response) {
|
||||
|
||||
r := Get(fmt.Sprintf("/project/get/%d", id))
|
||||
|
||||
var getResp api.GetProjectResponse
|
||||
data, _ := ioutil.ReadAll(r.Body)
|
||||
err := json.Unmarshal(data, &getResp)
|
||||
handleErr(err)
|
||||
|
||||
return &getResp, r
|
||||
}
|
74
test/api_task_test.go
Normal file
74
test/api_task_test.go
Normal file
@ -0,0 +1,74 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"src/task_tracker/api"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateTaskValid(t *testing.T) {
|
||||
|
||||
//Make sure there is always a project for id:1
|
||||
createProject(api.CreateProjectRequest{
|
||||
Name: "Some Test name",
|
||||
Version: "Test Version",
|
||||
GitUrl: "http://github.com/test/test",
|
||||
|
||||
})
|
||||
|
||||
resp := createTask(api.CreateTaskRequest{
|
||||
Project:1,
|
||||
Recipe: "{}",
|
||||
MaxRetries:3,
|
||||
})
|
||||
|
||||
if resp.Ok != true {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTaskInvalidProject(t *testing.T) {
|
||||
|
||||
resp := createTask(api.CreateTaskRequest{
|
||||
Project:123456,
|
||||
Recipe: "{}",
|
||||
MaxRetries:3,
|
||||
})
|
||||
|
||||
if resp.Ok != false {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if len(resp.Message) <= 0 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTaskInvalidRetries(t *testing.T) {
|
||||
|
||||
resp := createTask(api.CreateTaskRequest{
|
||||
Project:1,
|
||||
MaxRetries:-1,
|
||||
})
|
||||
|
||||
if resp.Ok != false {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if len(resp.Message) <= 0 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func createTask(request api.CreateTaskRequest) *api.CreateTaskResponse {
|
||||
|
||||
r := Post("/task/create", request)
|
||||
|
||||
var resp api.CreateTaskResponse
|
||||
data, _ := ioutil.ReadAll(r.Body)
|
||||
err := json.Unmarshal(data, &resp)
|
||||
handleErr(err)
|
||||
|
||||
return &resp
|
||||
}
|
83
test/api_worker_test.go
Normal file
83
test/api_worker_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"src/task_tracker/api"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateGetWorker(t *testing.T) {
|
||||
|
||||
resp, r := createWorker(api.CreateWorkerRequest{})
|
||||
|
||||
if r.StatusCode != 200 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if resp.Ok != true {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
getResp, r := getWorker(resp.WorkerId)
|
||||
|
||||
if r.StatusCode != 200 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if resp.WorkerId != getResp.Worker.Id.String() {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWorkerNotFound(t *testing.T) {
|
||||
|
||||
resp, r := getWorker("8bfc0ccd-d5ce-4dc5-a235-3a7ae760d9c6")
|
||||
|
||||
if r.StatusCode != 404 {
|
||||
t.Fail()
|
||||
}
|
||||
if resp.Ok != false {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWorkerInvalid(t *testing.T) {
|
||||
|
||||
resp, r := getWorker("invalid-uuid")
|
||||
|
||||
if r.StatusCode != 400 {
|
||||
t.Fail()
|
||||
}
|
||||
if resp.Ok != false {
|
||||
t.Fail()
|
||||
}
|
||||
if len(resp.Message) <= 0 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func createWorker(req api.CreateWorkerRequest) (*api.CreateWorkerResponse, *http.Response) {
|
||||
r := Post("/worker/create", req)
|
||||
|
||||
var resp *api.CreateWorkerResponse
|
||||
data, _ := ioutil.ReadAll(r.Body)
|
||||
err := json.Unmarshal(data, &resp)
|
||||
handleErr(err)
|
||||
|
||||
return resp, r
|
||||
}
|
||||
|
||||
func getWorker(id string) (*api.GetWorkerResponse, *http.Response) {
|
||||
|
||||
r := Get(fmt.Sprintf("/worker/get/%s", id))
|
||||
|
||||
var resp *api.GetWorkerResponse
|
||||
data, _ := ioutil.ReadAll(r.Body)
|
||||
err := json.Unmarshal(data, &resp)
|
||||
handleErr(err)
|
||||
|
||||
return resp, r
|
||||
}
|
53
test/common.go
Normal file
53
test/common.go
Normal file
@ -0,0 +1,53 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"src/task_tracker/config"
|
||||
)
|
||||
|
||||
func Post(path string, x interface{}) *http.Response {
|
||||
|
||||
body, err := json.Marshal(x)
|
||||
buf := bytes.NewBuffer(body)
|
||||
|
||||
r, err := http.Post("http://" + config.Cfg.ServerAddr + path, "application/json", buf)
|
||||
handleErr(err)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func Get(path string) *http.Response {
|
||||
r, err := http.Get("http://" + config.Cfg.ServerAddr + path)
|
||||
handleErr(err)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
func handleErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Print(body io.ReadCloser) {
|
||||
rawBody, _ := ioutil.ReadAll(body)
|
||||
fmt.Println(string(rawBody))
|
||||
}
|
||||
|
||||
func GenericJson(body io.ReadCloser) map[string]interface{} {
|
||||
|
||||
var obj map[string]interface{}
|
||||
|
||||
data, _ := ioutil.ReadAll(body)
|
||||
|
||||
err := json.Unmarshal(data, &obj)
|
||||
handleErr(err)
|
||||
|
||||
return obj
|
||||
}
|
5
test/config.yml
Normal file
5
test/config.yml
Normal file
@ -0,0 +1,5 @@
|
||||
server:
|
||||
address: "127.0.0.1:5001"
|
||||
|
||||
database:
|
||||
conn_str : "user=task_tracker dbname=task_tracker_test sslmode=disable"
|
20
test/main_test.go
Normal file
20
test/main_test.go
Normal file
@ -0,0 +1,20 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"src/task_tracker/api"
|
||||
"src/task_tracker/config"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
config.SetupConfig()
|
||||
|
||||
testApi := api.New()
|
||||
testApi.Database.Reset()
|
||||
go testApi.Run()
|
||||
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
m.Run()
|
||||
}
|
1
test/schema.sql
Symbolic link
1
test/schema.sql
Symbolic link
@ -0,0 +1 @@
|
||||
../schema.sql
|
Loading…
x
Reference in New Issue
Block a user