Reload job in ui
parent
19d4b645b8
commit
8138e31e53
24
TODO.md
24
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
|
||||
|
|
|
|||
9
main.go
9
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() {
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@
|
|||
<summary>Upsert Job</summary>
|
||||
<form id="upsert" action="#" method="get" onsubmit="upsert(); return false;">
|
||||
<input type="text" name="id" placeholder="id"/>
|
||||
<input type="text" name="title" placeholder="title"/>
|
||||
<label><input type="checkbox" name="disabled"/> Disabled</label>
|
||||
<select name="language" required>
|
||||
<option value="bash" selected>bash</option>
|
||||
</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>
|
||||
<button type="submit">Upsert</button>
|
||||
</form>
|
||||
|
|
|
|||
141
public/js/js.js
141
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 += `
|
||||
<span>
|
||||
<input type="button" onclick="job${e.name}(this);" value="&#${e.icon};" alt="${e.name}" title="${e.name}" job="${btoa(JSON.stringify(job))}"/>
|
||||
</span>
|
||||
`
|
||||
})
|
||||
return `<tr><td><details>
|
||||
<summary name="${job.id}">
|
||||
<span>${job.title}</span>
|
||||
<span>${passing}</span>
|
||||
${buttons}
|
||||
</summary>
|
||||
<table>
|
||||
<tr><td>
|
||||
<code>${job.cron}</code>
|
||||
<code>${job.language}</code>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<code>${job.last.run}</code>
|
||||
<code>${job.last.runtime}</code>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<code>${job.script}</code>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<code>${job.last.output}</code>
|
||||
</td></tr>
|
||||
</table>
|
||||
</details></td></tr>`
|
||||
}
|
||||
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 += `
|
||||
<span>
|
||||
<input type="button" onclick="job${e.name}(this);" value="&#${e.icon};" alt="${e.name}" title="${e.name}" job="${btoa(JSON.stringify(job))}"/>
|
||||
</span>
|
||||
`
|
||||
})
|
||||
return `<tr><td><details>
|
||||
<summary name="${job.id}">
|
||||
<span>${job.title}</span>
|
||||
<span>${passing}</span>
|
||||
${buttons}
|
||||
</summary>
|
||||
<table>
|
||||
<tr><td>
|
||||
<code>${job.cron}</code>
|
||||
<code>${job.language}</code>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<code>${job.last.run}</code>
|
||||
<code>${job.last.runtime}</code>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<code>${job.script}</code>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<code>${job.last.output}</code>
|
||||
</td></tr>
|
||||
</table>
|
||||
</details></td></tr>`
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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{}{})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue