Fix job encode decode

master
bel 2020-03-15 16:13:44 +00:00
parent 2efb0bfcf3
commit f4016da220
21 changed files with 232 additions and 41 deletions

23
TODO.md Normal file → Executable file
View File

@ -2,18 +2,22 @@
1. UI to view 1. UI to view
1. running job 1. running job
1. jobs x jobs
1. job definition 1. job definition
1. next runtime 1. next runtime
1. last runtime 1. last runtime
1. last output 1. last output
1. add titles to jobs
1. job title includes last pass/fail icon
1. button to modify (copies to upsert form)
1. button to delete
1. UI to mutate 1. UI to mutate
1. submit job 1. submit job
1. delete job 1. delete job
1. pause jobs 1. pause jobs
1. interrupt job 1. interrupt job
1. JS 1. JS
1. ajax for json calls x ajax for json calls
# Backend # Backend
@ -21,5 +25,18 @@ x load from file
1. interrupt running jobs 1. interrupt running jobs
1. temporarily disable jobs 1. temporarily disable jobs
1. json API 1. json API
1. list
1. last run output
1. last run pass/fail bool
1. last run timestamp
1. next run
1. upsert
1. delete job
1. pause/disable job
1. running job
1. interrupt job
1. change cron to load the full job from storage so not holding big queued jobs in ram 1. change cron to load the full job from storage so not holding big queued jobs in ram
1. add optional second for test main x add optional second for test main
1. test main
1. add titles to jobs
1. namespace for jobs, output, lastrun, laststatus

0
config/config_test.go Normal file → Executable file
View File

6
config/ns/ns.go Executable file
View File

@ -0,0 +1,6 @@
package ns
var (
Jobs = []string{"jobs"}
Output = []string{"jobs", "output"}
)

0
logger/logger.go Normal file → Executable file
View File

0
logger/logger_test.go Normal file → Executable file
View File

View File

@ -3,8 +3,10 @@ package main
import ( import (
"local/firestormy/config" "local/firestormy/config"
"local/firestormy/scheduler" "local/firestormy/scheduler"
"local/firestormy/server"
"local/lastn/lastn" "local/lastn/lastn"
"log" "log"
"net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
@ -26,13 +28,12 @@ func main() {
panic(err) panic(err)
} }
/*
server := server.New() server := server.New()
if err := server.Routes(); err != nil { if err := server.Routes(); err != nil {
panic(err) panic(err)
} }
go EnqueueBackups() //go EnqueueBackups()
go func() { go func() {
log.Printf("Serving on %q", config.Port) log.Printf("Serving on %q", config.Port)
@ -40,7 +41,6 @@ func main() {
panic(err) panic(err)
} }
}() }()
*/
// catch stop // catch stop
stop := make(chan os.Signal) stop := make(chan os.Signal)

0
scheduler/errors.go Normal file → Executable file
View File

0
scheduler/errors_test.go Normal file → Executable file
View File

43
scheduler/job.go Normal file → Executable file
View File

@ -4,8 +4,12 @@ import (
"bytes" "bytes"
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"local/firestormy/config"
"local/firestormy/config/ns"
"local/firestormy/logger" "local/firestormy/logger"
"os/exec" "os/exec"
"strings"
"time"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -16,6 +20,10 @@ type Job struct {
Raw string Raw string
Runner Runner Runner Runner
foo func() foo func()
LastStatus int
LastOutput string
LastRuntime time.Duration
LastRun time.Time
} }
func NewJob(runner Runner, schedule, raw string) (*Job, error) { func NewJob(runner Runner, schedule, raw string) (*Job, error) {
@ -31,20 +39,33 @@ func newBashJob(schedule, sh string) (*Job, error) {
if !validCron(schedule) { if !validCron(schedule) {
return nil, ErrBadCron return nil, ErrBadCron
} }
return &Job{ j := &Job{
Name: uuid.New().String(), Name: uuid.New().String(),
Schedule: schedule, Schedule: schedule,
Raw: sh, Raw: sh,
Runner: Bash, Runner: Bash,
foo: func() {
cmd := exec.Command("bash", "-c", sh)
out, err := cmd.CombinedOutput()
if err != nil {
panic(err)
} }
logger.New().Info(fmt.Sprintf("executed %s: %s", sh, out)) j.foo = func() {
}, cmd := exec.Command("bash", "-c", sh)
}, nil j.LastRun = time.Now()
start := time.Now()
out, err := cmd.CombinedOutput()
j.LastRuntime = time.Since(start)
if err != nil {
out = []byte(fmt.Sprintf("error running command: %v: %v", err, out))
}
j.LastOutput = strings.TrimSpace(string(out))
if cmd != nil && cmd.ProcessState != nil {
j.LastStatus = cmd.ProcessState.ExitCode()
}
logger.New().Info(fmt.Sprintf("%+v", j))
b, err := j.Encode()
if err == nil {
// TODO webpage doenst load post SET despite this returning nil
config.Store.Set(j.Name, b, ns.Jobs...)
}
}
return j, nil
} }
func (j *Job) Run() { func (j *Job) Run() {
@ -68,6 +89,10 @@ func (j *Job) Decode(b []byte) error {
k, err := NewJob(j.Runner, j.Schedule, j.Raw) k, err := NewJob(j.Runner, j.Schedule, j.Raw)
if err == nil { if err == nil {
k.Name = j.Name k.Name = j.Name
k.LastStatus = j.LastStatus
k.LastOutput = j.LastOutput
k.LastRuntime = j.LastRuntime
k.LastRun = j.LastRun
*j = *k *j = *k
} }
return err return err

22
scheduler/job_test.go Normal file → Executable file
View File

@ -3,9 +3,12 @@ package scheduler
import ( import (
"bytes" "bytes"
"io/ioutil" "io/ioutil"
"local/firestormy/config"
"local/logb" "local/logb"
"local/storage"
"os" "os"
"testing" "testing"
"time"
) )
func TestNewBashJobBadCron(t *testing.T) { func TestNewBashJobBadCron(t *testing.T) {
@ -16,6 +19,7 @@ func TestNewBashJobBadCron(t *testing.T) {
} }
func TestNewBashJobAndRun(t *testing.T) { func TestNewBashJobAndRun(t *testing.T) {
config.Store = storage.NewMap()
cases := []struct { cases := []struct {
sched string sched string
cmd string cmd string
@ -45,12 +49,7 @@ func TestNewBashJobAndRun(t *testing.T) {
t.Error(err) t.Error(err)
continue continue
} }
func() {
defer func() {
recover()
}()
j.Run() j.Run()
}()
if !bytes.Contains(b.Bytes(), []byte(c.out)) { if !bytes.Contains(b.Bytes(), []byte(c.out)) {
t.Errorf("(%s, %s) => %s", c.sched, c.cmd, b.Bytes()) t.Errorf("(%s, %s) => %s", c.sched, c.cmd, b.Bytes())
} }
@ -58,6 +57,7 @@ func TestNewBashJobAndRun(t *testing.T) {
} }
func TestJobEncodeDecode(t *testing.T) { func TestJobEncodeDecode(t *testing.T) {
config.Store = storage.NewMap()
buff, clean := captureLog() buff, clean := captureLog()
defer clean() defer clean()
@ -89,6 +89,18 @@ func TestJobEncodeDecode(t *testing.T) {
if k.Name != j.Name { if k.Name != j.Name {
t.Error(k.Name, "vs", j.Name) t.Error(k.Name, "vs", j.Name)
} }
if diff := k.LastRun.Unix() - j.LastRun.Unix(); (diff > 0 && diff < int64(time.Hour)) || (diff < 0 && -1*diff < int64(time.Hour)) {
t.Error(j.LastRun, "vs", k.LastRun)
}
if k.LastStatus != j.LastStatus {
t.Error(j.LastStatus, "vs", k.LastStatus)
}
if k.LastRuntime != j.LastRuntime {
t.Error(j.LastRuntime, "vs", k.LastRuntime)
}
if string(k.LastOutput) != string(j.LastOutput) {
t.Error(j.LastOutput, "vs", k.LastOutput)
}
k.foo() k.foo()
if !bytes.Contains(buff.Bytes(), []byte(os.Getenv("HOSTNAME"))) { if !bytes.Contains(buff.Bytes(), []byte(os.Getenv("HOSTNAME"))) {

0
scheduler/parser.go Normal file → Executable file
View File

0
scheduler/parser_test.go Normal file → Executable file
View File

20
scheduler/runner.go Normal file → Executable file
View File

@ -5,3 +5,23 @@ type Runner int
const ( const (
Bash Runner = iota + 1 Bash Runner = iota + 1
) )
func (r Runner) String() string {
switch r {
case Bash:
return "bash"
default:
return ""
}
}
func NewRunner(s string) Runner {
for _, r := range []Runner{
Bash,
} {
if r.String() == s {
return r
}
}
return 0
}

9
scheduler/scheduler.go Normal file → Executable file
View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"local/firestormy/config" "local/firestormy/config"
"local/firestormy/config/ns"
"local/firestormy/logger" "local/firestormy/logger"
"regexp" "regexp"
"strings" "strings"
@ -117,7 +118,7 @@ func (s *Scheduler) Stop() error {
} }
func (s *Scheduler) List() ([]*Job, error) { func (s *Scheduler) List() ([]*Job, error) {
entries, err := config.Store.List(nil) entries, err := config.Store.List(ns.Jobs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -139,7 +140,7 @@ func (s *Scheduler) List() ([]*Job, error) {
} }
func (s *Scheduler) loadJobFromStore(k string) (*Job, error) { func (s *Scheduler) loadJobFromStore(k string) (*Job, error) {
b, err := config.Store.Get(k) b, err := config.Store.Get(k, ns.Jobs...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -160,7 +161,7 @@ func (s *Scheduler) Add(j *Job) error {
if err != nil { if err != nil {
return err return err
} }
if err := config.Store.Set(j.Name, b); err != nil { if err := config.Store.Set(j.Name, b, ns.Jobs...); err != nil {
return err return err
} }
s.running[j.Name] = entryID s.running[j.Name] = entryID
@ -178,7 +179,7 @@ func (s *Scheduler) Remove(j *Job) error {
if was == is { if was == is {
return ErrJobNotFound return ErrJobNotFound
} }
return config.Store.Set(j.Name, nil) return config.Store.Set(j.Name, nil, ns.Jobs...)
} }
func (s *Scheduler) getEntry(j *Job) (cron.EntryID, bool) { func (s *Scheduler) getEntry(j *Job) (cron.EntryID, bool) {

0
scheduler/scheduler_test.go Normal file → Executable file
View File

View File

@ -1,9 +1,15 @@
package server package server
import ( import (
"encoding/json"
"fmt" "fmt"
"local/firestormy/config"
"local/firestormy/config/ns"
"local/firestormy/scheduler"
"local/router" "local/router"
"log"
"net/http" "net/http"
"sort"
) )
func (s *Server) Routes() error { func (s *Server) Routes() error {
@ -12,6 +18,14 @@ func (s *Server) Routes() error {
path string path string
handler http.HandlerFunc handler http.HandlerFunc
}{ }{
{
path: fmt.Sprintf("/upserts"),
handler: s.gzip(s.authenticate(s.upserts)),
},
{
path: fmt.Sprintf("/list"),
handler: s.gzip(s.authenticate(s.list)),
},
{ {
path: fmt.Sprintf("%s%s", wildcard, wildcard), path: fmt.Sprintf("%s%s", wildcard, wildcard),
handler: s.gzip(s.authenticate(s.static)), handler: s.gzip(s.authenticate(s.static)),
@ -29,3 +43,47 @@ func (s *Server) Routes() error {
func (s *Server) static(w http.ResponseWriter, r *http.Request) { func (s *Server) static(w http.ResponseWriter, r *http.Request) {
s.fileServer.ServeHTTP(w, r) s.fileServer.ServeHTTP(w, r)
} }
func (s *Server) upserts(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)
}
func (s *Server) list(w http.ResponseWriter, r *http.Request) {
jobs, err := config.Store.List(ns.Jobs)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sort.Strings(jobs)
out := make([]map[string]interface{}, len(jobs))
for i, job := range jobs {
out[i] = make(map[string]interface{})
b, err := config.Store.Get(job, ns.Jobs...)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
j := &scheduler.Job{}
if err := j.Decode(b); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
out[i]["id"] = j.Name
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,
"runtime": j.LastRuntime,
"output": j.LastOutput,
"status": j.LastStatus,
}
}
json.NewEncoder(w).Encode(out)
}

46
server/upserts.go Executable file
View File

@ -0,0 +1,46 @@
package server
import (
"encoding/json"
"errors"
"fmt"
"io"
"local/firestormy/config"
"local/firestormy/config/ns"
"github.com/google/uuid"
)
type upsertRequest struct {
ID string `json:"id"`
Language string `json:"language"`
Cron string `json:"cron"`
Script string `json:"script"`
}
func newUpsertRequest(r io.Reader) (upsertRequest, error) {
u := upsertRequest{}
if err := json.NewDecoder(r).Decode(&u); err != nil {
return u, err
}
err := u.validate()
return u, err
}
func (u *upsertRequest) validate() error {
if u.ID == "" {
u.ID = uuid.New().String()
} else if _, err := config.Store.Get(u.ID, ns.Jobs...); err != nil {
return fmt.Errorf("ID provided but not accessible: %v", err)
}
if u.Language == "" {
return errors.New("language required")
}
if u.Cron == "" {
return errors.New("cron required")
}
if u.Script == "" {
return errors.New("script required")
}
return nil
}

5
testdata/5_jobs.cron vendored Executable file
View File

@ -0,0 +1,5 @@
0 */5 * * * * echo first job
0 */15 * * * * echo second job
0 */25 * * * * echo third job
0 */35 * * * * echo fourth job
0 */45 * * * * true

0
testdata/comment_only.cron vendored Normal file → Executable file
View File

1
testdata/hostname_per_5m.cron vendored Executable file
View File

@ -0,0 +1 @@
0 */5 * * * * hostname

0
testdata/hostname_per_second.cron vendored Normal file → Executable file
View File