Initial Commit

This commit is contained in:
2023-06-09 10:48:46 -07:00
commit 8890eea322
14 changed files with 1418 additions and 0 deletions

344
internal/builtins/http.go Normal file
View File

@@ -0,0 +1,344 @@
package builtins
import (
"bytes"
"errors"
"fmt"
"io"
"net/http"
"strings"
"go.elara.ws/logger/log"
"go.elara.ws/lure-updater/internal/config"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
"golang.org/x/crypto/bcrypt"
)
const maxBodySize = 16384
var (
ErrInvalidBodyType = errors.New("invalid body type")
ErrInvalidHdrKeyType = errors.New("invalid header key type")
ErrInvalidHdrVal = errors.New("invalid header value type")
ErrInvalidType = errors.New("invalid type")
ErrInsecureWebhook = errors.New("secure webhook missing authorization")
)
var httpModule = &starlarkstruct.Module{
Name: "http",
Members: starlark.StringDict{
"get": starlark.NewBuiltin("http.get", httpGet),
"post": starlark.NewBuiltin("http.post", httpPost),
"put": starlark.NewBuiltin("http.put", httpPut),
"head": starlark.NewBuiltin("http.head", httpHead),
},
}
func httpGet(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return makeRequest("http.get", http.MethodGet, args, kwargs, thread)
}
func httpPost(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return makeRequest("http.post", http.MethodPost, args, kwargs, thread)
}
func httpPut(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return makeRequest("http.put", http.MethodPut, args, kwargs, thread)
}
func httpHead(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return makeRequest("http.head", http.MethodHead, args, kwargs, thread)
}
type starlarkBodyReader struct {
io.Reader
}
func (sbr *starlarkBodyReader) Unpack(v starlark.Value) error {
switch v := v.(type) {
case starlark.String:
sbr.Reader = strings.NewReader(string(v))
case starlark.Bytes:
sbr.Reader = strings.NewReader(string(v))
default:
return fmt.Errorf("%w: %s", ErrInvalidBodyType, v.Type())
}
return nil
}
func newBodyReader() *starlarkBodyReader {
return &starlarkBodyReader{
Reader: bytes.NewReader(nil),
}
}
type starlarkHeaders struct {
http.Header
}
func (sh *starlarkHeaders) Unpack(v starlark.Value) error {
dict, ok := v.(*starlark.Dict)
if !ok {
return fmt.Errorf("%w: %s", ErrInvalidType, v.Type())
}
sh.Header = make(http.Header, dict.Len())
for _, key := range dict.Keys() {
keyStr, ok := key.(starlark.String)
if !ok {
return fmt.Errorf("%w: %s", ErrInvalidHdrKeyType, key.Type())
}
val, _, _ := dict.Get(key)
list, ok := val.(*starlark.List)
if !ok {
return fmt.Errorf("%w: %s", ErrInvalidHdrVal, val.Type())
}
hdrVals := make([]string, list.Len())
for i := 0; i < list.Len(); i++ {
hdrVal, ok := list.Index(i).(starlark.String)
if !ok {
return fmt.Errorf("%w: %s", ErrInvalidHdrVal, list.Index(i).Type())
}
hdrVals[i] = string(hdrVal)
}
sh.Header[string(keyStr)] = hdrVals
}
return nil
}
func makeRequest(name, method string, args starlark.Tuple, kwargs []starlark.Tuple, thread *starlark.Thread) (starlark.Value, error) {
var (
url string
redirect = true
headers = &starlarkHeaders{}
body = newBodyReader()
)
err := starlark.UnpackArgs(name, args, kwargs, "url", &url, "redirect??", &redirect, "headers??", headers, "body??", body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
req.Header = headers.Header
client := http.DefaultClient
if !redirect {
client = &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
log.Debug("Making HTTP request").Str("url", url).Str("method", req.Method).Bool("redirect", redirect).Stringer("pos", thread.CallFrame(1).Pos).Send()
res, err := client.Do(req)
if err != nil {
return nil, err
}
log.Debug("Got HTTP response").Str("host", res.Request.URL.Host).Int("code", res.StatusCode).Stringer("pos", thread.CallFrame(1).Pos).Send()
return starlarkResponse(res), nil
}
func starlarkResponse(res *http.Response) *starlarkstruct.Struct {
return starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{
"code": starlark.MakeInt(res.StatusCode),
"headers": starlarkStringSliceMap(res.Header),
"body": starlarkBody(res.Body),
})
}
func starlarkRequest(req *http.Request) *starlarkstruct.Struct {
return starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{
"method": starlark.String(req.Method),
"remote_addr": starlark.String(req.RemoteAddr),
"headers": starlarkStringSliceMap(req.Header),
"query": starlarkStringSliceMap(req.URL.Query()),
"body": starlarkBody(req.Body),
})
}
func starlarkStringSliceMap(ssm map[string][]string) *starlark.Dict {
dict := starlark.NewDict(len(ssm))
for key, vals := range ssm {
sVals := make([]starlark.Value, len(vals))
for i, val := range vals {
sVals[i] = starlark.String(val)
}
dict.SetKey(starlark.String(key), starlark.NewList(sVals))
}
return dict
}
func starlarkBody(body io.ReadCloser) *starlarkstruct.Struct {
return starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{
"string": bodyAsString(body),
"bytes": bodyAsBytes(body),
"close": bodyClose(body),
})
}
func bodyAsBytes(body io.ReadCloser) *starlark.Builtin {
return starlark.NewBuiltin("http.response.body.bytes", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
data, err := io.ReadAll(io.LimitReader(body, maxBodySize))
if err != nil {
return nil, err
}
return starlark.Bytes(data), nil
})
}
func bodyAsString(body io.ReadCloser) *starlark.Builtin {
return starlark.NewBuiltin("http.response.body.string", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
data, err := io.ReadAll(io.LimitReader(body, maxBodySize))
if err != nil {
return nil, err
}
return starlark.String(data), nil
})
}
func bodyClose(body io.ReadCloser) *starlark.Builtin {
return starlark.NewBuiltin("http.response.body.close", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
err := body.Close()
if err != nil {
return nil, err
}
return starlark.None, nil
})
}
func registerWebhook(mux *http.ServeMux, cfg *config.Config, pluginName string) *starlark.Builtin {
return starlark.NewBuiltin("register_webhook", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var fn *starlark.Function
secure := true
err := starlark.UnpackArgs("register_webhook", args, kwargs, "function", &fn, "secure??", &secure)
if err != nil {
return nil, err
}
if !secure {
log.Warn("Plugin is registering an insecure webhook").Str("plugin", pluginName).Send()
}
path := "/webhook/" + pluginName + "/" + fn.Name()
mux.HandleFunc(path, webhookHandler(pluginName, secure, cfg, thread, fn))
log.Debug("Registered webhook").Str("path", path).Str("function", fn.Name()).Stringer("pos", thread.CallFrame(1).Pos).Send()
return starlark.None, nil
})
}
func webhookHandler(pluginName string, secure bool, cfg *config.Config, thread *starlark.Thread, fn *starlark.Function) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
res.Header().Add("X-Updater-Plugin", pluginName)
if secure {
err := verifySecure(cfg.Webhook.PasswordHash, pluginName, req)
if err != nil {
log.Error("Error verifying webhook").Err(err).Send()
res.WriteHeader(http.StatusForbidden)
_, _ = io.WriteString(res, err.Error())
return
}
}
log.Debug("Calling webhook function").Str("name", fn.Name()).Stringer("pos", fn.Position()).Send()
val, err := starlark.Call(thread, fn, starlark.Tuple{starlarkRequest(req)}, nil)
if err != nil {
log.Error("Error while executing webhook").Err(err).Stringer("pos", fn.Position()).Send()
res.WriteHeader(http.StatusInternalServerError)
_, _ = io.WriteString(res, err.Error())
return
}
switch val := val.(type) {
case starlark.NoneType:
res.WriteHeader(http.StatusOK)
case starlark.Int:
var code int
err = starlark.AsInt(val, &code)
if err == nil {
res.WriteHeader(code)
} else {
res.WriteHeader(http.StatusOK)
}
case starlark.String, starlark.Bytes:
body := newBodyReader()
err = body.Unpack(val)
if err != nil {
log.Error("Error unpacking returned body").Err(err).Send()
return
}
_, err = io.Copy(res, body)
if err != nil {
log.Error("Error writing body").Err(err).Send()
return
}
case *starlark.Dict:
code := http.StatusOK
codeVal, ok, _ := val.Get(starlark.String("code"))
if ok {
err = starlark.AsInt(codeVal, &code)
if err != nil {
log.Error("Error decoding returned status code").Err(err).Send()
return
}
res.WriteHeader(code)
}
body := newBodyReader()
bodyVal, ok, _ := val.Get(starlark.String("body"))
if ok {
err = body.Unpack(bodyVal)
if err != nil {
log.Error("Error unpacking returned body").Err(err).Send()
return
}
_, err = io.Copy(res, body)
if err != nil {
log.Error("Error writing body").Err(err).Send()
return
}
}
}
}
}
func verifySecure(pwdHash, pluginName string, req *http.Request) error {
var pwd []byte
if _, pwdStr, ok := req.BasicAuth(); ok {
pwdStr = strings.TrimSpace(pwdStr)
pwd = []byte(pwdStr)
} else if hdrStr := req.Header.Get("Authorization"); hdrStr != "" {
hdrStr = strings.TrimPrefix(hdrStr, "Bearer")
hdrStr = strings.TrimSpace(hdrStr)
pwd = []byte(hdrStr)
} else {
log.Warn("Insecure webhook request").
Str("from", req.RemoteAddr).
Str("plugin", pluginName).
Send()
return ErrInsecureWebhook
}
fmt.Println(string(pwd))
if err := bcrypt.CompareHashAndPassword([]byte(pwdHash), pwd); err != nil {
return err
}
return nil
}

76
internal/builtins/log.go Normal file
View File

@@ -0,0 +1,76 @@
package builtins
import (
"strings"
"go.elara.ws/logger"
"go.elara.ws/logger/log"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
)
func logModule(name string) *starlarkstruct.Module {
return &starlarkstruct.Module{
Name: "log",
Members: starlark.StringDict{
"debug": logDebug(name),
"info": logInfo(name),
"warn": logWarn(name),
"error": logError(name),
},
}
}
func logDebug(name string) *starlark.Builtin {
return starlark.NewBuiltin("log.debug", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return starlark.None, doLogEvent(name, "log.debug", log.Debug, thread, args, kwargs)
})
}
func logInfo(name string) *starlark.Builtin {
return starlark.NewBuiltin("log.info", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return starlark.None, doLogEvent(name, "log.info", log.Info, thread, args, kwargs)
})
}
func logWarn(name string) *starlark.Builtin {
return starlark.NewBuiltin("log.warn", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return starlark.None, doLogEvent(name, "log.warn", log.Warn, thread, args, kwargs)
})
}
func logError(name string) *starlark.Builtin {
return starlark.NewBuiltin("log.error", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return starlark.None, doLogEvent(name, "log.error", log.Error, thread, args, kwargs)
})
}
func doLogEvent(pluginName, fnName string, eventFn func(msg string) logger.LogBuilder, thread *starlark.Thread, args starlark.Tuple, kwargs []starlark.Tuple) error {
var msg string
var fields *starlark.Dict
err := starlark.UnpackArgs(fnName, args, kwargs, "msg", &msg, "fields??", &fields)
if err != nil {
return err
}
evt := eventFn("[" + pluginName + "] " + msg)
if fields != nil {
for _, key := range fields.Keys() {
val, _, err := fields.Get(key)
if err != nil {
return err
}
keyStr := strings.Trim(key.String(), `"`)
evt = evt.Stringer(keyStr, val)
}
}
if fnName == "log.debug" {
evt = evt.Stringer("pos", thread.CallFrame(1).Pos)
}
evt.Send()
return nil
}

127
internal/builtins/regex.go Normal file
View File

@@ -0,0 +1,127 @@
package builtins
import (
"sync"
"go.elara.ws/pcre"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
)
var (
cacheMtx = &sync.Mutex{}
regexCache = map[string]*pcre.Regexp{}
)
var regexModule = &starlarkstruct.Module{
Name: "regex",
Members: starlark.StringDict{
"compile": starlark.NewBuiltin("regex.compile", regexCompile),
"compile_glob": starlark.NewBuiltin("regex.compile_glob", regexCompileGlob),
},
}
func regexCompile(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var regexStr string
err := starlark.UnpackArgs("regex.compile", args, kwargs, "regex", &regexStr)
if err != nil {
return nil, err
}
cacheMtx.Lock()
regex, ok := regexCache[regexStr]
if !ok {
regex, err = pcre.Compile(regexStr)
if err != nil {
return nil, err
}
regexCache[regexStr] = regex
}
cacheMtx.Unlock()
return starlarkRegex(regex), nil
}
func regexCompileGlob(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var globStr string
err := starlark.UnpackArgs("regex.compile_glob", args, kwargs, "glob", &globStr)
if err != nil {
return nil, err
}
cacheMtx.Lock()
regex, ok := regexCache[globStr]
if !ok {
regex, err = pcre.CompileGlob(globStr)
if err != nil {
return nil, err
}
regexCache[globStr] = regex
}
cacheMtx.Unlock()
return starlarkRegex(regex), nil
}
func starlarkRegex(regex *pcre.Regexp) *starlarkstruct.Struct {
return starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{
"find_all": findAll(regex),
"find_one": findOne(regex),
"matches": matches(regex),
})
}
func findAll(regex *pcre.Regexp) *starlark.Builtin {
return starlark.NewBuiltin("regex.regexp.find_all", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var in string
err := starlark.UnpackArgs("regex.compile", args, kwargs, "in", &in)
if err != nil {
return nil, err
}
matches := regex.FindAllStringSubmatch(in, -1)
return matchesToStarlark2D(matches), nil
})
}
func findOne(regex *pcre.Regexp) *starlark.Builtin {
return starlark.NewBuiltin("regex.regexp.find_one", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var in string
err := starlark.UnpackArgs("regex.compile", args, kwargs, "in", &in)
if err != nil {
return nil, err
}
match := regex.FindStringSubmatch(in)
return matchesToStarlark1D(match), nil
})
}
func matches(regex *pcre.Regexp) *starlark.Builtin {
return starlark.NewBuiltin("regex.regexp.find_one", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var in string
err := starlark.UnpackArgs("regex.compile", args, kwargs, "in", &in)
if err != nil {
return nil, err
}
found := regex.MatchString(in)
return starlark.Bool(found), nil
})
}
func matchesToStarlark2D(matches [][]string) *starlark.List {
outer := make([]starlark.Value, len(matches))
for i, match := range matches {
outer[i] = matchesToStarlark1D(match)
}
return starlark.NewList(outer)
}
func matchesToStarlark1D(match []string) *starlark.List {
list := make([]starlark.Value, len(match))
for j, val := range match {
list[j] = starlark.String(val)
}
return starlark.NewList(list)
}

View File

@@ -0,0 +1,29 @@
package builtins
import (
"net/http"
"go.elara.ws/lure-updater/internal/config"
"go.etcd.io/bbolt"
"go.starlark.net/starlark"
"go.starlark.net/starlarkjson"
)
type Options struct {
Name string
DB *bbolt.DB
Config *config.Config
Mux *http.ServeMux
}
func Register(sd starlark.StringDict, opts *Options) {
sd["run_every"] = starlark.NewBuiltin("run_every", runEvery)
sd["sleep"] = starlark.NewBuiltin("sleep", sleep)
sd["http"] = httpModule
sd["regex"] = regexModule
sd["store"] = storeModule(opts.DB, opts.Name)
sd["updater"] = updaterModule(opts.Config)
sd["log"] = logModule(opts.Name)
sd["json"] = starlarkjson.Module
sd["register_webhook"] = registerWebhook(opts.Mux, opts.Config, opts.Name)
}

View File

@@ -0,0 +1,84 @@
package builtins
import (
"sync"
"time"
"go.elara.ws/logger/log"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
)
var (
tickerMtx = &sync.Mutex{}
tickerCount = 0
tickers = map[int]*time.Ticker{}
)
func runEvery(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var every string
var fn *starlark.Function
err := starlark.UnpackArgs("run_every", args, kwargs, "every", &every, "function", &fn)
if err != nil {
return nil, err
}
d, err := time.ParseDuration(every)
if err != nil {
return nil, err
}
tickerMtx.Lock()
t := time.NewTicker(d)
handle := tickerCount
tickers[handle] = t
tickerCount++
tickerMtx.Unlock()
log.Debug("Created new ticker").Int("handle", handle).Str("duration", every).Stringer("pos", thread.CallFrame(1).Pos).Send()
go func() {
for range t.C {
log.Debug("Calling scheduled function").Str("name", fn.Name()).Stringer("pos", fn.Position()).Send()
_, err := starlark.Call(thread, fn, nil, nil)
if err != nil {
log.Warn("Error while executing scheduled function").Str("name", fn.Name()).Stringer("pos", fn.Position()).Err(err).Send()
}
}
}()
return newTickerHandle(handle), nil
}
func sleep(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var duration string
err := starlark.UnpackArgs("sleep", args, kwargs, "duration", &duration)
if err != nil {
return nil, err
}
d, err := time.ParseDuration(duration)
if err != nil {
return nil, err
}
log.Debug("Sleeping").Str("duration", duration).Stringer("pos", thread.CallFrame(1).Pos).Send()
time.Sleep(d)
return starlark.None, nil
}
func stopTicker(handle int) *starlark.Builtin {
return starlark.NewBuiltin("stop", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
tickerMtx.Lock()
tickers[handle].Stop()
delete(tickers, handle)
tickerMtx.Unlock()
log.Debug("Stopped ticker").Int("handle", handle).Stringer("pos", thread.CallFrame(1).Pos).Send()
return starlark.None, nil
})
}
func newTickerHandle(handle int) starlark.Value {
return starlarkstruct.FromStringDict(starlarkstruct.Default, starlark.StringDict{
"stop": stopTicker(handle),
})
}

View File

@@ -0,0 +1,98 @@
package builtins
import (
"go.elara.ws/logger/log"
"go.etcd.io/bbolt"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
)
func storeModule(db *bbolt.DB, bucketName string) *starlarkstruct.Module {
return &starlarkstruct.Module{
Name: "store",
Members: starlark.StringDict{
"set": storeSet(db, bucketName),
"get": storeGet(db, bucketName),
"delete": storeDelete(db, bucketName),
},
}
}
func storeSet(db *bbolt.DB, bucketName string) *starlark.Builtin {
return starlark.NewBuiltin("store.set", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var key, value string
err := starlark.UnpackArgs("store.set", args, kwargs, "key", &key, "value", &value)
if err != nil {
return nil, err
}
err = db.Update(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
err = bucket.Put([]byte(key), []byte(value))
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
log.Debug("Set value").Str("bucket", bucketName).Str("key", key).Str("value", value).Stringer("pos", thread.CallFrame(1).Pos).Send()
return starlark.None, nil
})
}
func storeGet(db *bbolt.DB, bucketName string) *starlark.Builtin {
return starlark.NewBuiltin("store.get", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var key string
err := starlark.UnpackArgs("store.get", args, kwargs, "key", &key)
if err != nil {
return nil, err
}
var value string
err = db.Update(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
data := bucket.Get([]byte(key))
value = string(data)
return nil
})
if err != nil {
return nil, err
}
log.Debug("Retrieved value").Str("bucket", bucketName).Str("key", key).Str("value", value).Stringer("pos", thread.CallFrame(1).Pos).Send()
return starlark.String(value), nil
})
}
func storeDelete(db *bbolt.DB, bucketName string) *starlark.Builtin {
return starlark.NewBuiltin("store.delete", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var key string
err := starlark.UnpackArgs("store.delete", args, kwargs, "key", &key)
if err != nil {
return nil, err
}
err = db.Update(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
return bucket.Delete([]byte(key))
})
if err != nil {
return nil, err
}
log.Debug("Deleted value").Str("bucket", bucketName).Str("key", key).Stringer("pos", thread.CallFrame(1).Pos).Send()
return starlark.None, nil
})
}

View File

@@ -0,0 +1,181 @@
package builtins
import (
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"go.elara.ws/logger/log"
"go.elara.ws/lure-updater/internal/config"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
)
func updaterModule(cfg *config.Config) *starlarkstruct.Module {
return &starlarkstruct.Module{
Name: "updater",
Members: starlark.StringDict{
"repo_dir": starlark.String(cfg.Git.RepoDir),
"pull": updaterPull(cfg),
"push_changes": updaterPushChanges(cfg),
"get_package_file": getPackageFile(cfg),
"write_package_file": writePackageFile(cfg),
},
}
}
// repoMtx makes sure two starlark threads can
// never access the repo at the same time
var repoMtx = &sync.Mutex{}
func updaterPull(cfg *config.Config) *starlark.Builtin {
return starlark.NewBuiltin("updater.pull", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
repoMtx.Lock()
defer repoMtx.Unlock()
repo, err := git.PlainOpen(cfg.Git.RepoDir)
if err != nil {
return nil, err
}
w, err := repo.Worktree()
if err != nil {
return nil, err
}
err = w.Pull(&git.PullOptions{Progress: os.Stderr})
if err != git.NoErrAlreadyUpToDate && err != nil {
return nil, err
}
return starlark.None, nil
})
}
func updaterPushChanges(cfg *config.Config) *starlark.Builtin {
return starlark.NewBuiltin("updater.push_changes", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var msg string
err := starlark.UnpackArgs("updater.push_changes", args, kwargs, "msg", &msg)
if err != nil {
return nil, err
}
repoMtx.Lock()
defer repoMtx.Unlock()
repo, err := git.PlainOpen(cfg.Git.RepoDir)
if err != nil {
return nil, err
}
w, err := repo.Worktree()
if err != nil {
return nil, err
}
status, err := w.Status()
if err != nil {
return nil, err
}
if status.IsClean() {
return starlark.None, nil
}
err = w.Pull(&git.PullOptions{Progress: os.Stderr})
if err != git.NoErrAlreadyUpToDate && err != nil {
return nil, err
}
_, err = w.Add(".")
if err != nil {
return nil, err
}
sig := &object.Signature{
Name: cfg.Git.Commit.Name,
Email: cfg.Git.Commit.Email,
When: time.Now(),
}
h, err := w.Commit(msg, &git.CommitOptions{
Author: sig,
Committer: sig,
})
if err != nil {
return nil, err
}
log.Debug("Created new commit").Stringer("hash", h).Stringer("pos", thread.CallFrame(1).Pos).Send()
err = repo.Push(&git.PushOptions{
Progress: os.Stderr,
Auth: &http.BasicAuth{
Username: cfg.Git.Credentials.Username,
Password: cfg.Git.Credentials.Password,
},
})
if err != nil {
return nil, err
}
log.Debug("Successfully pushed to repo").Stringer("pos", thread.CallFrame(1).Pos).Send()
return starlark.None, nil
})
}
func getPackageFile(cfg *config.Config) *starlark.Builtin {
return starlark.NewBuiltin("updater.get_package_file", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var pkg, filename string
err := starlark.UnpackArgs("updater.get_package_file", args, kwargs, "pkg", &pkg, "filename", &filename)
if err != nil {
return nil, err
}
repoMtx.Lock()
defer repoMtx.Unlock()
path := filepath.Join(cfg.Git.RepoDir, pkg, filename)
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
log.Debug("Got package file").Str("package", pkg).Str("filename", filename).Stringer("pos", thread.CallFrame(1).Pos).Send()
return starlark.String(data), nil
})
}
func writePackageFile(cfg *config.Config) *starlark.Builtin {
return starlark.NewBuiltin("updater.write_package_file", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var pkg, filename, content string
err := starlark.UnpackArgs("updater.write_package_file", args, kwargs, "pkg", &pkg, "filename", &filename, "content", &content)
if err != nil {
return nil, err
}
repoMtx.Lock()
defer repoMtx.Unlock()
path := filepath.Join(cfg.Git.RepoDir, pkg, filename)
fl, err := os.Create(path)
if err != nil {
return nil, err
}
_, err = io.Copy(fl, strings.NewReader(content))
if err != nil {
return nil, err
}
log.Debug("Wrote package file").Str("package", pkg).Str("filename", filename).Stringer("pos", thread.CallFrame(1).Pos).Send()
return starlark.None, nil
})
}