feature: Exec on Calendar file support (#243)
* feature: Exec on calendar file * feature: support for datetime args * feature: Schedule commands based on seconds
This commit is contained in:
parent
a992ef84f5
commit
2fb40ff443
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
grpcapi "github.com/OliveTin/OliveTin/internal/grpcapi"
|
grpcapi "github.com/OliveTin/OliveTin/internal/grpcapi"
|
||||||
"github.com/OliveTin/OliveTin/internal/installationinfo"
|
"github.com/OliveTin/OliveTin/internal/installationinfo"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/oncalendarfile"
|
||||||
"github.com/OliveTin/OliveTin/internal/oncron"
|
"github.com/OliveTin/OliveTin/internal/oncron"
|
||||||
"github.com/OliveTin/OliveTin/internal/onfileindir"
|
"github.com/OliveTin/OliveTin/internal/onfileindir"
|
||||||
"github.com/OliveTin/OliveTin/internal/onstartup"
|
"github.com/OliveTin/OliveTin/internal/onstartup"
|
||||||
|
|
@ -153,6 +154,7 @@ func main() {
|
||||||
go onstartup.Execute(cfg, executor)
|
go onstartup.Execute(cfg, executor)
|
||||||
go oncron.Schedule(cfg, executor)
|
go oncron.Schedule(cfg, executor)
|
||||||
go onfileindir.WatchFilesInDirectory(cfg, executor)
|
go onfileindir.WatchFilesInDirectory(cfg, executor)
|
||||||
|
go oncalendarfile.Schedule(cfg, executor)
|
||||||
|
|
||||||
go entityfiles.SetupEntityFileWatchers(cfg)
|
go entityfiles.SetupEntityFileWatchers(cfg)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ type Action struct {
|
||||||
ExecOnCron []string
|
ExecOnCron []string
|
||||||
ExecOnFileCreatedInDir []string
|
ExecOnFileCreatedInDir []string
|
||||||
ExecOnFileChangedInDir []string
|
ExecOnFileChangedInDir []string
|
||||||
|
ExecOnCalendarFile string
|
||||||
Trigger string
|
Trigger string
|
||||||
MaxConcurrent int
|
MaxConcurrent int
|
||||||
Arguments []ActionArgument
|
Arguments []ActionArgument
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -94,6 +95,16 @@ func TypeSafetyCheck(name string, value string, argumentType string) error {
|
||||||
return typeSafetyCheckUrl(name, value)
|
return typeSafetyCheckUrl(name, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if argumentType == "datetime" {
|
||||||
|
_, err := time.Parse("2006-01-02T15:04:05", value)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return typeSafetyCheckRegex(name, value, argumentType)
|
return typeSafetyCheckRegex(name, value, argumentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package filehelper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WatchFile(fullpath string, callback func()) {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not watch for files being created: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer watcher.Close()
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
|
filename := filepath.Base(fullpath)
|
||||||
|
filedir := filepath.Dir(fullpath)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
processEvent(filename, watcher, fsnotify.Write, callback)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = watcher.Add(filedir)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Could not create watcher: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
func processEvent(filename string, watcher *fsnotify.Watcher, eventType fsnotify.Op, callback func()) {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !consumeEvent(ok, filename, &event, callback) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case err := <-watcher.Errors:
|
||||||
|
log.Errorf("Error in fsnotify: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumeEvent(ok bool, filename string, event *fsnotify.Event, callback func()) bool {
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filepath.Base(event.Name) != filename {
|
||||||
|
log.Tracef("fsnotify irreleventa event different file %+v", event)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeWriteEvents(event, callback)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumeWriteEvents(event *fsnotify.Event, callback func()) {
|
||||||
|
if event.Has(fsnotify.Write) {
|
||||||
|
log.Debugf("fsnotify write event: %v", event)
|
||||||
|
callback()
|
||||||
|
} else {
|
||||||
|
log.Debugf("fsnotify irrelevant event on file %v", event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package filehelper
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Touch(filename string, description string) {
|
||||||
|
_, err := os.Stat(filename)
|
||||||
|
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
_, err := os.Create(filename)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Could not create %v: %v", description, filename)
|
||||||
|
} else {
|
||||||
|
log.Infof("Created %v: %v", description, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
package oncalendarfile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/acl"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/executor"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/filehelper"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var calendar map[*config.Action][]*time.Timer
|
||||||
|
|
||||||
|
func Schedule(cfg *config.Config, ex *executor.Executor) {
|
||||||
|
calendar = make(map[*config.Action][]*time.Timer)
|
||||||
|
|
||||||
|
for _, action := range cfg.Actions {
|
||||||
|
if action.ExecOnCalendarFile != "" {
|
||||||
|
x := func() {
|
||||||
|
parseCalendarFile(action, cfg, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
go filehelper.WatchFile(action.ExecOnCalendarFile, x)
|
||||||
|
|
||||||
|
x()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCalendarFile(action *config.Action, cfg *config.Config, ex *executor.Executor) {
|
||||||
|
filehelper.Touch(action.ExecOnCalendarFile, "calendar file")
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"actionTitle": action.Title,
|
||||||
|
}).Infof("Parsing calendar file")
|
||||||
|
|
||||||
|
yfile, err := ioutil.ReadFile(action.ExecOnCalendarFile)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("ReadIn: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]string, 1)
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(yfile, &data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleCalendarActions(data, action, cfg, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheduleCalendarActions(entries []string, action *config.Action, cfg *config.Config, ex *executor.Executor) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for _, instant := range entries {
|
||||||
|
if instant == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
until, _ := time.Parse(time.RFC3339, instant)
|
||||||
|
|
||||||
|
go sleepUntil(ctx, until, action, cfg, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sleepUntil(ctx context.Context, instant time.Time, action *config.Action, cfg *config.Config, ex *executor.Executor) {
|
||||||
|
if time.Now().After(instant) {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"instant": instant,
|
||||||
|
"actionTitle": action.Title,
|
||||||
|
}).Warnf("Not scheduling stale calendar action")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"instant": instant,
|
||||||
|
"actionTitle": action.Title,
|
||||||
|
}).Infof("Scheduling action on calendar")
|
||||||
|
|
||||||
|
timer := time.NewTimer(time.Until(instant))
|
||||||
|
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
exec(instant, action, cfg, ex)
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Infof("Cancelled scheduled action")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func exec(instant time.Time, action *config.Action, cfg *config.Config, ex *executor.Executor) {
|
||||||
|
// calendar[action] = append(calendar[action], timer)
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"instant": instant,
|
||||||
|
"actionTitle": action.Title,
|
||||||
|
}).Infof("Executing action from calendar")
|
||||||
|
|
||||||
|
req := &executor.ExecutionRequest{
|
||||||
|
Action: action,
|
||||||
|
Cfg: cfg,
|
||||||
|
Tags: []string{"calendar"},
|
||||||
|
AuthenticatedUser: &acl.AuthenticatedUser{
|
||||||
|
Username: "calendar",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ex.ExecRequest(req)
|
||||||
|
}
|
||||||
|
|
@ -102,6 +102,10 @@ class ArgumentForm extends window.HTMLElement {
|
||||||
this.domBtnStart.disabled = false
|
this.domBtnStart.disabled = false
|
||||||
domEl.disabled = true
|
domEl.disabled = true
|
||||||
}
|
}
|
||||||
|
} else if (arg.type === 'datetime') {
|
||||||
|
domEl = document.createElement('input')
|
||||||
|
domEl.setAttribute('type', 'datetime-local')
|
||||||
|
domEl.setAttribute('step', '1')
|
||||||
} else {
|
} else {
|
||||||
domEl = document.createElement('input')
|
domEl = document.createElement('input')
|
||||||
domEl.onchange = () => {
|
domEl.onchange = () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue