chore(gonf): first draft of the gonf lib with commands, files, packages and variables
This commit is contained in:
parent
ada87c622c
commit
ceac85dbc1
10 changed files with 522 additions and 0 deletions
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module git.adyxax.org/adyxax/gonf/v2
|
||||||
|
|
||||||
|
go 1.21.5
|
95
gonf/commands.go
Normal file
95
gonf/commands.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package gonf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log/slog"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----- Globals ---------------------------------------------------------------
|
||||||
|
var commands []*CommandPromise
|
||||||
|
|
||||||
|
// ----- Init ------------------------------------------------------------------
|
||||||
|
func init() {
|
||||||
|
commands = make([]*CommandPromise, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Public ----------------------------------------------------------------
|
||||||
|
func Command(cmd string, args ...string) *CommandPromise {
|
||||||
|
return CommandWithEnv([]string{}, cmd, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CommandWithEnv(env []string, cmd string, args ...string) *CommandPromise {
|
||||||
|
return &CommandPromise{
|
||||||
|
args: args,
|
||||||
|
chain: nil,
|
||||||
|
cmd: cmd,
|
||||||
|
env: env,
|
||||||
|
err: nil,
|
||||||
|
Status: PROMISED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandPromise struct {
|
||||||
|
args []string
|
||||||
|
chain []Promise
|
||||||
|
cmd string
|
||||||
|
env []string
|
||||||
|
err error
|
||||||
|
Status Status
|
||||||
|
Stdout bytes.Buffer
|
||||||
|
Stderr bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandPromise) IfRepaired(p ...Promise) Promise {
|
||||||
|
c.chain = p
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandPromise) Promise() Promise {
|
||||||
|
commands = append(commands, c)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CommandPromise) Resolve() {
|
||||||
|
cmd := exec.Command(c.cmd, c.args...)
|
||||||
|
for _, e := range c.env {
|
||||||
|
cmd.Env = append(cmd.Environ(), e)
|
||||||
|
}
|
||||||
|
cmd.Stdout = &c.Stdout
|
||||||
|
cmd.Stderr = &c.Stderr
|
||||||
|
|
||||||
|
if c.err = cmd.Run(); c.err != nil {
|
||||||
|
c.Status = BROKEN
|
||||||
|
slog.Error("command", "args", c.args, "cmd", c.cmd, "env", c.env, "err", c.err, "stdout", c.Stdout.String(), "stderr", c.Stderr.String(), "status", c.Status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.Stdout.Len() == 0 && c.Stderr.Len() > 0 {
|
||||||
|
c.Status = BROKEN
|
||||||
|
slog.Error("command", "args", c.args, "cmd", c.cmd, "env", c.env, "stdout", c.Stdout.String(), "stderr", c.Stderr.String(), "status", c.Status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Status = REPAIRED
|
||||||
|
slog.Info("command", "args", c.args, "cmd", c.cmd, "env", c.env, "stderr", c.Stderr.String(), "status", c.Status)
|
||||||
|
// TODO add a notion of repaired?
|
||||||
|
for _, p := range c.chain {
|
||||||
|
p.Resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Internal --------------------------------------------------------------
|
||||||
|
func resolveCommands() (status Status) {
|
||||||
|
status = KEPT
|
||||||
|
for _, c := range commands {
|
||||||
|
if c.Status == PROMISED {
|
||||||
|
c.Resolve()
|
||||||
|
switch c.Status {
|
||||||
|
case BROKEN:
|
||||||
|
return BROKEN
|
||||||
|
case REPAIRED:
|
||||||
|
status = REPAIRED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
153
gonf/files.go
Normal file
153
gonf/files.go
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
package gonf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----- Globals ---------------------------------------------------------------
|
||||||
|
var files []*FilePromise
|
||||||
|
|
||||||
|
// ----- Init ------------------------------------------------------------------
|
||||||
|
func init() {
|
||||||
|
files = make([]*FilePromise, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Public ----------------------------------------------------------------
|
||||||
|
func File(filename string, contents []byte) *FilePromise {
|
||||||
|
return &FilePromise{
|
||||||
|
chain: nil,
|
||||||
|
contents: contents,
|
||||||
|
err: nil,
|
||||||
|
filename: filename,
|
||||||
|
status: PROMISED,
|
||||||
|
templateFunctions: nil,
|
||||||
|
useTemplate: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilePromise struct {
|
||||||
|
chain []Promise
|
||||||
|
contents []byte
|
||||||
|
err error
|
||||||
|
filename string
|
||||||
|
status Status
|
||||||
|
templateFunctions map[string]any
|
||||||
|
useTemplate bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FilePromise) IfRepaired(p ...Promise) Promise {
|
||||||
|
f.chain = p
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FilePromise) Promise() Promise {
|
||||||
|
files = append(files, f)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FilePromise) Resolve() {
|
||||||
|
if f.useTemplate {
|
||||||
|
tpl := template.New(f.filename)
|
||||||
|
tpl.Option("missingkey=error")
|
||||||
|
tpl.Funcs(builtinTemplateFunctions)
|
||||||
|
tpl.Funcs(f.templateFunctions)
|
||||||
|
if ttpl, err := tpl.Parse(string(f.contents)); err != nil {
|
||||||
|
f.status = BROKEN
|
||||||
|
slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
var buff bytes.Buffer
|
||||||
|
if err := ttpl.Execute(&buff, 0 /* TODO */); err != nil {
|
||||||
|
f.status = BROKEN
|
||||||
|
slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.contents = buff.Bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var sumFile []byte
|
||||||
|
sumFile, f.err = sha256sumOfFile(f.filename)
|
||||||
|
if f.err != nil {
|
||||||
|
f.status = BROKEN
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sumContents := sha256sum(f.contents)
|
||||||
|
if !bytes.Equal(sumFile, sumContents) {
|
||||||
|
if f.err = writeFile(f.filename, f.contents); f.err != nil {
|
||||||
|
f.status = BROKEN
|
||||||
|
slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.status = REPAIRED
|
||||||
|
slog.Info("file", "filename", f.filename, "status", f.status)
|
||||||
|
for _, p := range f.chain {
|
||||||
|
p.Resolve()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.status = KEPT
|
||||||
|
slog.Debug("file", "filename", f.filename, "status", f.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Template(filename string, contents []byte) *FilePromise {
|
||||||
|
f := File(filename, contents)
|
||||||
|
f.useTemplate = true
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func TemplateWith(filename string, contents []byte, templateFunctions map[string]any) *FilePromise {
|
||||||
|
f := Template(filename, contents)
|
||||||
|
f.templateFunctions = templateFunctions
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Internal --------------------------------------------------------------
|
||||||
|
var builtinTemplateFunctions = map[string]any{
|
||||||
|
"encodeURIQueryParameter": url.QueryEscape,
|
||||||
|
"var": getVariable,
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveFiles() (status Status) {
|
||||||
|
status = KEPT
|
||||||
|
for _, f := range files {
|
||||||
|
if f.status == PROMISED {
|
||||||
|
f.Resolve()
|
||||||
|
switch f.status {
|
||||||
|
case BROKEN:
|
||||||
|
return BROKEN
|
||||||
|
case REPAIRED:
|
||||||
|
status = REPAIRED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sha256sumOfFile(filename string) ([]byte, error) {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
h := sha256.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return h.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFile(filename string, contents []byte) error {
|
||||||
|
f, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.Write(contents)
|
||||||
|
return err
|
||||||
|
}
|
44
gonf/gonf.go
Normal file
44
gonf/gonf.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package gonf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EnableDebugLogs() {
|
||||||
|
h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
})
|
||||||
|
slog.SetDefault(slog.New(h))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resolve() (status Status) {
|
||||||
|
for {
|
||||||
|
// ----- Files -------------------------------------------------
|
||||||
|
status = resolveFiles()
|
||||||
|
switch status {
|
||||||
|
case BROKEN:
|
||||||
|
return BROKEN
|
||||||
|
case REPAIRED:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// ----- Packages ----------------------------------------------
|
||||||
|
status = resolvePackages()
|
||||||
|
switch status {
|
||||||
|
case BROKEN:
|
||||||
|
return BROKEN
|
||||||
|
case REPAIRED:
|
||||||
|
packages_list_function()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// ----- Commands ----------------------------------------------
|
||||||
|
status = resolveCommands()
|
||||||
|
switch status {
|
||||||
|
case BROKEN:
|
||||||
|
return BROKEN
|
||||||
|
case REPAIRED:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
73
gonf/packages.go
Normal file
73
gonf/packages.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package gonf
|
||||||
|
|
||||||
|
// ----- Globals ---------------------------------------------------------------
|
||||||
|
var packages []*PackagePromise
|
||||||
|
|
||||||
|
// packages management functions
|
||||||
|
var packages_install_function func([]string) Status
|
||||||
|
var packages_list_function func()
|
||||||
|
var packages_update_function *CommandPromise
|
||||||
|
|
||||||
|
// ----- Init ------------------------------------------------------------------
|
||||||
|
func init() {
|
||||||
|
packages = make([]*PackagePromise, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Public ----------------------------------------------------------------
|
||||||
|
func SetPackagesConfiguration(install func([]string) Status, list func(), update *CommandPromise) {
|
||||||
|
packages_install_function = install
|
||||||
|
packages_list_function = list
|
||||||
|
packages_update_function = update
|
||||||
|
}
|
||||||
|
|
||||||
|
func Package(names ...string) *PackagePromise {
|
||||||
|
return &PackagePromise{
|
||||||
|
chain: nil,
|
||||||
|
err: nil,
|
||||||
|
names: names,
|
||||||
|
status: PROMISED,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PackagePromise struct {
|
||||||
|
chain []Promise
|
||||||
|
err error
|
||||||
|
names []string
|
||||||
|
status Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PackagePromise) IfRepaired(ps ...Promise) Promise {
|
||||||
|
p.chain = ps
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PackagePromise) Promise() Promise {
|
||||||
|
packages = append(packages, p)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PackagePromise) Resolve() {
|
||||||
|
status := packages_install_function(p.names)
|
||||||
|
if status == REPAIRED {
|
||||||
|
for _, pp := range p.chain {
|
||||||
|
pp.Resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Internal --------------------------------------------------------------
|
||||||
|
func resolvePackages() (status Status) {
|
||||||
|
status = KEPT
|
||||||
|
for _, c := range packages {
|
||||||
|
if c.status == PROMISED {
|
||||||
|
c.Resolve()
|
||||||
|
switch c.status {
|
||||||
|
case BROKEN:
|
||||||
|
return BROKEN
|
||||||
|
case REPAIRED:
|
||||||
|
status = REPAIRED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
50
gonf/promises.go
Normal file
50
gonf/promises.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package gonf
|
||||||
|
|
||||||
|
type Promise interface {
|
||||||
|
IfRepaired(...Promise) Promise
|
||||||
|
Promise() Promise
|
||||||
|
Resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
//type Operation int
|
||||||
|
//
|
||||||
|
//const (
|
||||||
|
// AND = iota
|
||||||
|
// OR
|
||||||
|
// NOT
|
||||||
|
//)
|
||||||
|
//
|
||||||
|
//func (o Operation) String() string {
|
||||||
|
// switch o {
|
||||||
|
// case AND:
|
||||||
|
// return "and"
|
||||||
|
// case OR:
|
||||||
|
// return "or"
|
||||||
|
// case NOT:
|
||||||
|
// return "not"
|
||||||
|
// }
|
||||||
|
// panic("unknown")
|
||||||
|
//}
|
||||||
|
|
||||||
|
type Status int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PROMISED = iota
|
||||||
|
BROKEN
|
||||||
|
KEPT
|
||||||
|
REPAIRED
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s Status) String() string {
|
||||||
|
switch s {
|
||||||
|
case PROMISED:
|
||||||
|
return "promised"
|
||||||
|
case BROKEN:
|
||||||
|
return "broken"
|
||||||
|
case KEPT:
|
||||||
|
return "kept"
|
||||||
|
case REPAIRED:
|
||||||
|
return "repaired"
|
||||||
|
}
|
||||||
|
panic("unknown")
|
||||||
|
}
|
11
gonf/triggers.go
Normal file
11
gonf/triggers.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package gonf
|
||||||
|
|
||||||
|
type simpleTrigger struct {
|
||||||
|
fact string
|
||||||
|
status Status
|
||||||
|
}
|
||||||
|
|
||||||
|
//type compositeTrigger struct {
|
||||||
|
// triggers []simpleTrigger
|
||||||
|
// operation Operation
|
||||||
|
//}
|
9
gonf/utils.go
Normal file
9
gonf/utils.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package gonf
|
||||||
|
|
||||||
|
import "crypto/sha256"
|
||||||
|
|
||||||
|
func sha256sum(contents []byte) []byte {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(contents)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
27
gonf/values.go
Normal file
27
gonf/values.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package gonf
|
||||||
|
|
||||||
|
type Value interface {
|
||||||
|
Equals(Value) bool
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- String variables ------------------------------------------------------
|
||||||
|
type StringValue struct {
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s StringValue) Equals(v Value) bool {
|
||||||
|
sv, ok := v.(StringValue)
|
||||||
|
return ok && s.Value == sv.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s StringValue) String() string {
|
||||||
|
// TODO handle interpolation
|
||||||
|
return s.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO lists
|
||||||
|
|
||||||
|
// TODO maps
|
||||||
|
|
||||||
|
// TODO what else?
|
57
gonf/variables.go
Normal file
57
gonf/variables.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package gonf
|
||||||
|
|
||||||
|
import "log/slog"
|
||||||
|
|
||||||
|
// ----- Globals ---------------------------------------------------------------
|
||||||
|
var variables map[string]*VariablePromise
|
||||||
|
|
||||||
|
// ----- Init ------------------------------------------------------------------
|
||||||
|
func init() {
|
||||||
|
variables = make(map[string]*VariablePromise)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Public ----------------------------------------------------------------
|
||||||
|
func Default(name string, value Value) *VariablePromise {
|
||||||
|
if v, ok := variables[name]; ok {
|
||||||
|
if !v.isDefault {
|
||||||
|
slog.Debug("default would overwrite a variable, ignoring", "name", name, "old_value", v.value, "new_value", value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
slog.Error("default is being overwritten", "name", name, "old_value", v.value, "new_value", value)
|
||||||
|
}
|
||||||
|
v := &VariablePromise{
|
||||||
|
isDefault: true,
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
}
|
||||||
|
variables[name] = v
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
func Variable(name string, value Value) *VariablePromise {
|
||||||
|
if v, ok := variables[name]; ok && !v.isDefault {
|
||||||
|
slog.Error("variable is being overwritten", "name", name, "old_value", v, "new_value", value)
|
||||||
|
}
|
||||||
|
v := &VariablePromise{
|
||||||
|
isDefault: false,
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
}
|
||||||
|
variables[name] = v
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
type VariablePromise struct {
|
||||||
|
isDefault bool
|
||||||
|
name string
|
||||||
|
value Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Internal --------------------------------------------------------------
|
||||||
|
func getVariable(name string) string {
|
||||||
|
if v, ok := variables[name]; ok {
|
||||||
|
return v.value.String()
|
||||||
|
} else {
|
||||||
|
slog.Error("undefined variable or default", "name", name)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue