From fd71221cbfc6b15efe24cdf940fdd0d5d424591a Mon Sep 17 00:00:00 2001 From: bel Date: Sun, 15 Mar 2020 23:04:48 +0000 Subject: [PATCH] Reload job in ui --- TODO.md | 24 +++--- main.go | 9 +-- public/index.html | 3 +- public/js/js.js | 141 +++++++++++++++++++++++------------- scheduler/job.go | 4 +- scheduler/scheduler.go | 36 +++++++++ scheduler/scheduler_test.go | 48 ++++++++++++ server/get.go | 28 +++++++ server/job.go | 28 +++++++ server/list.go | 18 +---- server/routes.go | 4 + server/upsert.go | 25 ++++++- 12 files changed, 275 insertions(+), 93 deletions(-) create mode 100644 server/get.go create mode 100644 server/job.go diff --git a/TODO.md b/TODO.md index 1b3544c..f69c257 100755 --- a/TODO.md +++ b/TODO.md @@ -9,29 +9,29 @@ x last output x add titles to jobs x job title includes last pass/fail icon - 1. button to modify (copies to upsert form) - 1. button to delete + x button to modify (copies to upsert form) + x button to delete 1. UI to mutate - 1. submit job - 1. delete job - 1. pause jobs + x submit job + x delete job + x pause jobs 1. interrupt job 1. force run -1. JS +x JS x ajax for json calls # Backend x load from file 1. interrupt running jobs -1. temporarily disable jobs +1. disable jobs 1. json API 1. list - 1. last run output - 1. last run pass/fail bool - 1. last run timestamp + x last run output + x last run pass/fail bool + x last run timestamp 1. next run - 1. upsert + x upsert 1. delete job 1. pause/disable job 1. running job @@ -41,4 +41,4 @@ x load from file x add optional second for test main 1. test main x add titles to jobs -1. namespace for jobs, output, lastrun, laststatus +x namespace for jobs, output, lastrun, laststatus diff --git a/main.go b/main.go index e818697..9907a40 100755 --- a/main.go +++ b/main.go @@ -14,17 +14,14 @@ import ( ) func main() { - var err error - s := scheduler.New() if config.Config != "" { var err error - s, err = scheduler.NewFromFile(config.Config) + scheduler.Schedule, err = scheduler.NewFromFile(config.Config) if err != nil { panic(err) } } - err = s.Start() - if err != nil { + if err := scheduler.Schedule.Start(); err != nil { panic(err) } @@ -46,7 +43,7 @@ func main() { stop := make(chan os.Signal) signal.Notify(stop, os.Interrupt) <-stop - s.Stop() + scheduler.Schedule.Stop() } func EnqueueBackups() { diff --git a/public/index.html b/public/index.html index aa66f16..cbdd4a5 100755 --- a/public/index.html +++ b/public/index.html @@ -10,11 +10,12 @@ Upsert Job
+ - +
diff --git a/public/js/js.js b/public/js/js.js index 1e1f287..03ab058 100755 --- a/public/js/js.js +++ b/public/js/js.js @@ -14,7 +14,11 @@ function http(method, remote, callback, body) { function upsert() { function cb(body, status) { - console.log(status, body) + if (status == 200) { + init() + } else { + console.log("error upserting:", status, body) + } } http("POST", "/api/job/upsert", cb, jsonifyForm("upsert")) } @@ -28,66 +32,74 @@ function jsonifyForm(id) { } function init() { + var table = document.getElementById("jobs").getElementsByTagName("tbody")[0] function cb(body, status) { var jobs = JSON.parse(body) + getJobsTable().innerHTML = "" jobs.forEach(function(job) { var s = format(job) inject(s) }) } - function format(job) { - var pause = "⏸" - var passing = "9711" - if (job.last.status != 0) { - passing = "129314" - passing = "129324" - } - passing = `&#${passing};` - var buttons = "" - var btns = [ - {"name":"disable", "icon":"11035"}, - {"name":"enable", "icon":"9654"}, - {"name":"modify", "icon":"9999"}, - {"name":"delete", "icon":"10006"} - ] - btns.forEach(function(e) { - buttons += ` - - - - ` - }) - return `
- - ${job.title} - ${passing} - ${buttons} - - - - - - -
- ${job.cron} - ${job.language} -
- ${job.last.run} - ${job.last.runtime} -
- ${job.script} -
- ${job.last.output} -
-
` - } - function inject(s) { - var table = document.getElementById("jobs").getElementsByTagName("tbody")[0] - table.innerHTML += s - } http("GET", "/api/job/list", cb, null) } +function format(job) { + var pause = "⏸" + var passing = "9711" + if (job.last.status != 0) { + passing = "129314" + passing = "129324" + } + passing = `&#${passing};` + var buttons = "" + var btns = [ + {"name":"refresh", "icon":"8635"}, + {"name":"disable", "icon":"11035"}, + {"name":"enable", "icon":"9654"}, + {"name":"modify", "icon":"9999"}, + {"name":"delete", "icon":"10006"} + ] + btns.forEach(function(e) { + buttons += ` + + + + ` + }) + return `
+ + ${job.title} + ${passing} + ${buttons} + + + + + + +
+ ${job.cron} + ${job.language} +
+ ${job.last.run} + ${job.last.runtime} +
+ ${job.script} +
+ ${job.last.output} +
+
` +} + +function inject(s) { + getJobsTable().innerHTML += s +} + +function getJobsTable() { + return document.getElementById("jobs").getElementsByTagName("tbody")[0] +} + init() function jobdisable(input) { @@ -103,7 +115,7 @@ function jobenable(input) { function jobmodify(input) { var form = getForm() var job = jobFromInput(input) - var fields = ["id", "language", "cron", "script", "disabled"] + var fields = ["id", "language", "cron", "script", "disabled", "title"] fields.forEach(function(field) { var e = getField(field) e.checked = job[field] @@ -117,6 +129,31 @@ function jobdelete(input) { http("DELETE", "/api/job/delete", cb, null) } +function jobrefresh(input) { + var job = jobFromInput(input) + function cb(body, status) { + var table = getJobsTable() + var summaries = Array.from(table.getElementsByTagName("summary")) + summaries.forEach(function(e) { + if (e.getAttribute("name") == job.id) { + e = e.parentElement + var showing = false + if (e.getAttribute("open") != null) { + showing = true + } + e = e.parentElement.parentElement + var table = document.createElement("table") + table.innerHTML = format(JSON.parse(body)) + if (showing) { + table.getElementsByTagName("details")[0].setAttribute("open", "") + } + e.innerHTML = table.innerHTML + } + }) + } + http("GET", "/api/job/get/"+job.id, cb, null) +} + function jobFromInput(input) { var b64 = input.getAttribute("job") var json = atob(b64) diff --git a/scheduler/job.go b/scheduler/job.go index 73de80b..3a5f11d 100755 --- a/scheduler/job.go +++ b/scheduler/job.go @@ -20,12 +20,12 @@ type Job struct { Schedule string Raw string Runner Runner + Disabled bool foo func() LastStatus int LastOutput string LastRuntime time.Duration LastRun time.Time - Disabled bool } func NewJob(runner Runner, schedule, raw string) (*Job, error) { @@ -59,7 +59,7 @@ func newBashJob(schedule, sh string, title ...string) (*Job, error) { out, err := cmd.CombinedOutput() j.LastRuntime = time.Since(start) if err != nil { - out = []byte(fmt.Sprintf("error running command: %v: %v", err, out)) + out = []byte(fmt.Sprintf("error running command: %v: %s", err, out)) } j.LastOutput = strings.TrimSpace(string(out)) if cmd != nil && cmd.ProcessState != nil { diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index 883dd01..4f6c1ac 100755 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -15,6 +15,8 @@ import ( cron "github.com/robfig/cron/v3" ) +var Schedule *Scheduler = New() + type Scheduler struct { cron *cron.Cron running map[string]cron.EntryID @@ -154,6 +156,40 @@ func (s *Scheduler) loadJobFromStore(k string) (*Job, error) { return j, err } +func (s *Scheduler) Update(j *Job) error { + entryID, ok := s.getEntry(j) + if !ok { + return errors.New("job not found in storage") + } + + i, err := s.loadJobFromStore(j.Name) + if err != nil { + return err + } + j.LastStatus = i.LastStatus + j.LastOutput = i.LastOutput + j.LastRuntime = i.LastRuntime + j.LastRun = i.LastRun + + b, err := j.Encode() + if err != nil { + return err + } + + if err := config.Store.Set(j.Name, b, ns.Jobs...); err != nil { + return err + } + + s.cron.Remove(entryID) + + entryID, err = s.cron.AddJob(j.Schedule, j) + if err != nil { + return err + } + s.running[j.Name] = entryID + return nil +} + func (s *Scheduler) Add(j *Job) error { if _, ok := s.getEntry(j); ok { return ErrDuplicateJob diff --git a/scheduler/scheduler_test.go b/scheduler/scheduler_test.go index 66a79db..e367513 100755 --- a/scheduler/scheduler_test.go +++ b/scheduler/scheduler_test.go @@ -2,11 +2,13 @@ package scheduler import ( "bytes" + "fmt" "io/ioutil" "local/firestormy/config" "local/storage" "os" "testing" + "time" ) func TestSchedulerAddRemove(t *testing.T) { @@ -192,3 +194,49 @@ func TestSplitScheduleCommandTitle(t *testing.T) { }) } } + +func TestSchedulerUpdate(t *testing.T) { + config.Store, _ = storage.New(storage.MAP) + was := config.Store + defer func() { + config.Store = was + }() + s := New() + j, err := NewJob(Bash, "* * * * *", "hostname") + if err != nil { + t.Fatal(err) + } + if err := s.Add(j); err != nil { + t.Fatal(err) + } + if list, err := s.List(); err != nil { + t.Fatal(err) + } else if len(list) != 1 { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 1500) + j.Raw = "echo 2" + j.Title = "title 2" + if err := s.Update(j); err != nil { + t.Fatal(err) + } + if list, err := s.List(); err != nil { + t.Fatal(err) + } else if len(list) != 1 { + t.Fatal(err) + } else if j, err := s.loadJobFromStore(j.Name); err != nil { + t.Fatal(err) + } else if j.Raw != "echo 2" { + t.Error(j.Raw) + } else if j.Title != "title 2" { + t.Error(j.Title) + } else if entry := s.cron.Entry(s.running[j.Name]); entry == s.cron.Entry(-99) { + t.Error(entry) + } else if entries := s.cron.Entries(); len(entries) != 1 { + t.Error(entries) + } else if job, ok := entries[0].Job.(*Job); !ok { + t.Error(fmt.Sprintf("%T", entries[0].Job)) + } else if fmt.Sprintf("%+v", job) != fmt.Sprintf("%+v", j) { + t.Error(job) + } +} diff --git a/server/get.go b/server/get.go new file mode 100644 index 0000000..6a45534 --- /dev/null +++ b/server/get.go @@ -0,0 +1,28 @@ +package server + +import ( + "encoding/json" + "local/firestormy/config" + "local/firestormy/config/ns" + "local/firestormy/scheduler" + "net/http" + "strings" +) + +func (s *Server) get(w http.ResponseWriter, r *http.Request) { + keys := strings.Split(r.URL.Path, "/") + key := keys[len(keys)-1] + + j := &scheduler.Job{} + b, err := config.Store.Get(key, ns.Jobs...) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + if err := j.Decode(b); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(toMap(j)) +} diff --git a/server/job.go b/server/job.go new file mode 100644 index 0000000..ce6fa78 --- /dev/null +++ b/server/job.go @@ -0,0 +1,28 @@ +package server + +import ( + "local/firestormy/scheduler" + "strings" + "time" +) + +func toMap(j *scheduler.Job) map[string]interface{} { + tz, err := time.LoadLocation("America/Denver") + if err != nil { + panic(err) + } + out := make(map[string]interface{}) + out["disabled"] = j.Disabled + out["id"] = j.Name + out["title"] = j.Title + out["cron"] = j.Schedule + out["language"] = j.Runner.String() + out["script"] = j.Raw + out["last"] = map[string]interface{}{ + "run": j.LastRun.In(tz).Format(`2006-01-02 15:04:05 MST`), + "runtime": j.LastRuntime.String(), + "output": strings.ReplaceAll(j.LastOutput, "\n", "
"), + "status": j.LastStatus, + } + return out +} diff --git a/server/list.go b/server/list.go index 3a9b8c4..b012f17 100755 --- a/server/list.go +++ b/server/list.go @@ -7,7 +7,6 @@ import ( "local/firestormy/scheduler" "net/http" "sort" - "time" ) func (s *Server) list(w http.ResponseWriter, r *http.Request) { @@ -29,22 +28,7 @@ func (s *Server) list(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - tz, err := time.LoadLocation("America/Denver") - if err != nil { - panic(err) - } - out[i]["disabled"] = j.Disabled - out[i]["id"] = j.Name - out[i]["title"] = j.Title - out[i]["cron"] = j.Schedule - out[i]["language"] = j.Runner.String() - out[i]["script"] = j.Raw - out[i]["last"] = map[string]interface{}{ - "run": j.LastRun.In(tz).Format(`2006-01-02 15:04:05 MST`), - "runtime": j.LastRuntime.String(), - "output": string(j.LastOutput), - "status": j.LastStatus, - } + out[i] = toMap(j) } sort.Slice(out, func(i, j int) bool { return out[i]["title"].(string) < out[j]["title"].(string) diff --git a/server/routes.go b/server/routes.go index aae54c5..c610f3b 100755 --- a/server/routes.go +++ b/server/routes.go @@ -12,6 +12,10 @@ func (s *Server) Routes() error { path string handler http.HandlerFunc }{ + { + path: fmt.Sprintf("/api/job/get/%s", router.Wildcard), + handler: s.gzip(s.authenticate(s.get)), + }, { path: fmt.Sprintf("/api/job/upsert"), handler: s.gzip(s.authenticate(s.upsert)), diff --git a/server/upsert.go b/server/upsert.go index 34fe3b1..fe65e39 100755 --- a/server/upsert.go +++ b/server/upsert.go @@ -7,7 +7,7 @@ import ( "io" "local/firestormy/config" "local/firestormy/config/ns" - "log" + "local/firestormy/scheduler" "net/http" "github.com/google/uuid" @@ -52,12 +52,31 @@ func (u *upsertRequest) validate() error { return nil } +func (u *upsertRequest) toJob() (*scheduler.Job, error) { + j, err := scheduler.NewJob(scheduler.Bash, u.Cron, u.Script) + if err != nil { + return nil, err + } + j.Title = u.Title + j.Name = u.ID + j.Disabled = u.Disabled + return j, err +} + func (s *Server) upsert(w http.ResponseWriter, r *http.Request) { upsert, err := newUpsertRequest(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - log.Println("received", upsert) - http.Error(w, "not impl", http.StatusNotImplemented) + job, err := upsert.toJob() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := scheduler.Schedule.Update(job); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(map[string]interface{}{}) }