summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go.mod3
-rw-r--r--gonf/commands.go95
-rw-r--r--gonf/files.go153
-rw-r--r--gonf/gonf.go44
-rw-r--r--gonf/packages.go73
-rw-r--r--gonf/promises.go50
-rw-r--r--gonf/triggers.go11
-rw-r--r--gonf/utils.go9
-rw-r--r--gonf/values.go27
-rw-r--r--gonf/variables.go57
10 files changed, 522 insertions, 0 deletions
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..3faf555
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module git.adyxax.org/adyxax/gonf/v2
+
+go 1.21.5
diff --git a/gonf/commands.go b/gonf/commands.go
new file mode 100644
index 0000000..b5a11ca
--- /dev/null
+++ b/gonf/commands.go
@@ -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
+}
diff --git a/gonf/files.go b/gonf/files.go
new file mode 100644
index 0000000..2401a25
--- /dev/null
+++ b/gonf/files.go
@@ -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
+}
diff --git a/gonf/gonf.go b/gonf/gonf.go
new file mode 100644
index 0000000..b5aec4f
--- /dev/null
+++ b/gonf/gonf.go
@@ -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
+ }
+}
diff --git a/gonf/packages.go b/gonf/packages.go
new file mode 100644
index 0000000..7ff512d
--- /dev/null
+++ b/gonf/packages.go
@@ -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
+}
diff --git a/gonf/promises.go b/gonf/promises.go
new file mode 100644
index 0000000..326d500
--- /dev/null
+++ b/gonf/promises.go
@@ -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")
+}
diff --git a/gonf/triggers.go b/gonf/triggers.go
new file mode 100644
index 0000000..370ab1c
--- /dev/null
+++ b/gonf/triggers.go
@@ -0,0 +1,11 @@
+package gonf
+
+type simpleTrigger struct {
+ fact string
+ status Status
+}
+
+//type compositeTrigger struct {
+// triggers []simpleTrigger
+// operation Operation
+//}
diff --git a/gonf/utils.go b/gonf/utils.go
new file mode 100644
index 0000000..7031062
--- /dev/null
+++ b/gonf/utils.go
@@ -0,0 +1,9 @@
+package gonf
+
+import "crypto/sha256"
+
+func sha256sum(contents []byte) []byte {
+ h := sha256.New()
+ h.Write(contents)
+ return h.Sum(nil)
+}
diff --git a/gonf/values.go b/gonf/values.go
new file mode 100644
index 0000000..15c109b
--- /dev/null
+++ b/gonf/values.go
@@ -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?
diff --git a/gonf/variables.go b/gonf/variables.go
new file mode 100644
index 0000000..3c365ae
--- /dev/null
+++ b/gonf/variables.go
@@ -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
+ }
+}