Reload job in ui

master
bel 2020-03-15 23:04:48 +00:00
parent 19d4b645b8
commit 8138e31e53
12 changed files with 275 additions and 93 deletions

24
TODO.md
View File

@ -9,29 +9,29 @@
x last output x last output
x add titles to jobs x add titles to jobs
x job title includes last pass/fail icon x job title includes last pass/fail icon
1. button to modify (copies to upsert form) x button to modify (copies to upsert form)
1. button to delete x button to delete
1. UI to mutate 1. UI to mutate
1. submit job x submit job
1. delete job x delete job
1. pause jobs x pause jobs
1. interrupt job 1. interrupt job
1. force run 1. force run
1. JS x JS
x ajax for json calls x ajax for json calls
# Backend # Backend
x load from file x load from file
1. interrupt running jobs 1. interrupt running jobs
1. temporarily disable jobs 1. disable jobs
1. json API 1. json API
1. list 1. list
1. last run output x last run output
1. last run pass/fail bool x last run pass/fail bool
1. last run timestamp x last run timestamp
1. next run 1. next run
1. upsert x upsert
1. delete job 1. delete job
1. pause/disable job 1. pause/disable job
1. running job 1. running job
@ -41,4 +41,4 @@ x load from file
x add optional second for test main x add optional second for test main
1. test main 1. test main
x add titles to jobs x add titles to jobs
1. namespace for jobs, output, lastrun, laststatus x namespace for jobs, output, lastrun, laststatus

View File

@ -14,17 +14,14 @@ import (
) )
func main() { func main() {
var err error
s := scheduler.New()
if config.Config != "" { if config.Config != "" {
var err error var err error
s, err = scheduler.NewFromFile(config.Config) scheduler.Schedule, err = scheduler.NewFromFile(config.Config)
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
err = s.Start() if err := scheduler.Schedule.Start(); err != nil {
if err != nil {
panic(err) panic(err)
} }
@ -46,7 +43,7 @@ func main() {
stop := make(chan os.Signal) stop := make(chan os.Signal)
signal.Notify(stop, os.Interrupt) signal.Notify(stop, os.Interrupt)
<-stop <-stop
s.Stop() scheduler.Schedule.Stop()
} }
func EnqueueBackups() { func EnqueueBackups() {

View File

@ -10,11 +10,12 @@
<summary>Upsert Job</summary> <summary>Upsert Job</summary>
<form id="upsert" action="#" method="get" onsubmit="upsert(); return false;"> <form id="upsert" action="#" method="get" onsubmit="upsert(); return false;">
<input type="text" name="id" placeholder="id"/> <input type="text" name="id" placeholder="id"/>
<input type="text" name="title" placeholder="title"/>
<label><input type="checkbox" name="disabled"/> Disabled</label> <label><input type="checkbox" name="disabled"/> Disabled</label>
<select name="language" required> <select name="language" required>
<option value="bash" selected>bash</option> <option value="bash" selected>bash</option>
</select> </select>
<input type="text" name="cron" placeholder="cron" value="@daily"/> <input type="text" name="cron" placeholder="cron" value="@daily" required/>
<textarea name="script" placeholder="script" required rows="5"></textarea> <textarea name="script" placeholder="script" required rows="5"></textarea>
<button type="submit">Upsert</button> <button type="submit">Upsert</button>
</form> </form>

View File

@ -14,7 +14,11 @@ function http(method, remote, callback, body) {
function upsert() { function upsert() {
function cb(body, status) { 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")) http("POST", "/api/job/upsert", cb, jsonifyForm("upsert"))
} }
@ -28,14 +32,19 @@ function jsonifyForm(id) {
} }
function init() { function init() {
var table = document.getElementById("jobs").getElementsByTagName("tbody")[0]
function cb(body, status) { function cb(body, status) {
var jobs = JSON.parse(body) var jobs = JSON.parse(body)
getJobsTable().innerHTML = ""
jobs.forEach(function(job) { jobs.forEach(function(job) {
var s = format(job) var s = format(job)
inject(s) inject(s)
}) })
} }
function format(job) { http("GET", "/api/job/list", cb, null)
}
function format(job) {
var pause = "&#9208" var pause = "&#9208"
var passing = "9711" var passing = "9711"
if (job.last.status != 0) { if (job.last.status != 0) {
@ -45,6 +54,7 @@ function init() {
passing = `&#${passing};` passing = `&#${passing};`
var buttons = "" var buttons = ""
var btns = [ var btns = [
{"name":"refresh", "icon":"8635"},
{"name":"disable", "icon":"11035"}, {"name":"disable", "icon":"11035"},
{"name":"enable", "icon":"9654"}, {"name":"enable", "icon":"9654"},
{"name":"modify", "icon":"9999"}, {"name":"modify", "icon":"9999"},
@ -80,12 +90,14 @@ function init() {
</td></tr> </td></tr>
</table> </table>
</details></td></tr>` </details></td></tr>`
} }
function inject(s) {
var table = document.getElementById("jobs").getElementsByTagName("tbody")[0] function inject(s) {
table.innerHTML += s getJobsTable().innerHTML += s
} }
http("GET", "/api/job/list", cb, null)
function getJobsTable() {
return document.getElementById("jobs").getElementsByTagName("tbody")[0]
} }
init() init()
@ -103,7 +115,7 @@ function jobenable(input) {
function jobmodify(input) { function jobmodify(input) {
var form = getForm() var form = getForm()
var job = jobFromInput(input) var job = jobFromInput(input)
var fields = ["id", "language", "cron", "script", "disabled"] var fields = ["id", "language", "cron", "script", "disabled", "title"]
fields.forEach(function(field) { fields.forEach(function(field) {
var e = getField(field) var e = getField(field)
e.checked = job[field] e.checked = job[field]
@ -117,6 +129,31 @@ function jobdelete(input) {
http("DELETE", "/api/job/delete", cb, null) 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) { function jobFromInput(input) {
var b64 = input.getAttribute("job") var b64 = input.getAttribute("job")
var json = atob(b64) var json = atob(b64)

View File

@ -20,12 +20,12 @@ type Job struct {
Schedule string Schedule string
Raw string Raw string
Runner Runner Runner Runner
Disabled bool
foo func() foo func()
LastStatus int LastStatus int
LastOutput string LastOutput string
LastRuntime time.Duration LastRuntime time.Duration
LastRun time.Time LastRun time.Time
Disabled bool
} }
func NewJob(runner Runner, schedule, raw string) (*Job, error) { 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() out, err := cmd.CombinedOutput()
j.LastRuntime = time.Since(start) j.LastRuntime = time.Since(start)
if err != nil { 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)) j.LastOutput = strings.TrimSpace(string(out))
if cmd != nil && cmd.ProcessState != nil { if cmd != nil && cmd.ProcessState != nil {

View File

@ -15,6 +15,8 @@ import (
cron "github.com/robfig/cron/v3" cron "github.com/robfig/cron/v3"
) )
var Schedule *Scheduler = New()
type Scheduler struct { type Scheduler struct {
cron *cron.Cron cron *cron.Cron
running map[string]cron.EntryID running map[string]cron.EntryID
@ -154,6 +156,40 @@ func (s *Scheduler) loadJobFromStore(k string) (*Job, error) {
return j, err 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 { func (s *Scheduler) Add(j *Job) error {
if _, ok := s.getEntry(j); ok { if _, ok := s.getEntry(j); ok {
return ErrDuplicateJob return ErrDuplicateJob

View File

@ -2,11 +2,13 @@ package scheduler
import ( import (
"bytes" "bytes"
"fmt"
"io/ioutil" "io/ioutil"
"local/firestormy/config" "local/firestormy/config"
"local/storage" "local/storage"
"os" "os"
"testing" "testing"
"time"
) )
func TestSchedulerAddRemove(t *testing.T) { 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)
}
}

28
server/get.go Normal file
View File

@ -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))
}

28
server/job.go Normal file
View File

@ -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", "<br>"),
"status": j.LastStatus,
}
return out
}

View File

@ -7,7 +7,6 @@ import (
"local/firestormy/scheduler" "local/firestormy/scheduler"
"net/http" "net/http"
"sort" "sort"
"time"
) )
func (s *Server) list(w http.ResponseWriter, r *http.Request) { 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) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
tz, err := time.LoadLocation("America/Denver") out[i] = toMap(j)
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,
}
} }
sort.Slice(out, func(i, j int) bool { sort.Slice(out, func(i, j int) bool {
return out[i]["title"].(string) < out[j]["title"].(string) return out[i]["title"].(string) < out[j]["title"].(string)

View File

@ -12,6 +12,10 @@ func (s *Server) Routes() error {
path string path string
handler http.HandlerFunc 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"), path: fmt.Sprintf("/api/job/upsert"),
handler: s.gzip(s.authenticate(s.upsert)), handler: s.gzip(s.authenticate(s.upsert)),

View File

@ -7,7 +7,7 @@ import (
"io" "io"
"local/firestormy/config" "local/firestormy/config"
"local/firestormy/config/ns" "local/firestormy/config/ns"
"log" "local/firestormy/scheduler"
"net/http" "net/http"
"github.com/google/uuid" "github.com/google/uuid"
@ -52,12 +52,31 @@ func (u *upsertRequest) validate() error {
return nil 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) { func (s *Server) upsert(w http.ResponseWriter, r *http.Request) {
upsert, err := newUpsertRequest(r.Body) upsert, err := newUpsertRequest(r.Body)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
log.Println("received", upsert) job, err := upsert.toJob()
http.Error(w, "not impl", http.StatusNotImplemented) 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{}{})
} }