summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
Diffstat (limited to 'pkg')
-rw-r--r--pkg/commands.go95
-rw-r--r--pkg/files.go151
-rw-r--r--pkg/gonf.go52
-rw-r--r--pkg/packages.go81
-rw-r--r--pkg/permissions.go89
-rw-r--r--pkg/promises.go50
-rw-r--r--pkg/services.go96
-rw-r--r--pkg/templates.go44
-rw-r--r--pkg/triggers.go11
-rw-r--r--pkg/utils.go27
-rw-r--r--pkg/values.go104
-rw-r--r--pkg/variables.go85
12 files changed, 885 insertions, 0 deletions
diff --git a/pkg/commands.go b/pkg/commands.go
new file mode 100644
index 0000000..b5a11ca
--- /dev/null
+++ b/pkg/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/pkg/files.go b/pkg/files.go
new file mode 100644
index 0000000..35420d4
--- /dev/null
+++ b/pkg/files.go
@@ -0,0 +1,151 @@
+package gonf
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "errors"
+ "io"
+ "io/fs"
+ "log/slog"
+ "os"
+)
+
+// ----- Globals ---------------------------------------------------------------
+var files []*FilePromise
+
+// ----- Init ------------------------------------------------------------------
+func init() {
+ files = make([]*FilePromise, 0)
+}
+
+// ----- Public ----------------------------------------------------------------
+type FilePromise struct {
+ chain []Promise
+ contents Value
+ err error
+ filename Value
+ permissions *Permissions
+ status Status
+}
+
+func File(filename any) *FilePromise {
+ return &FilePromise{
+ chain: nil,
+ contents: nil,
+ err: nil,
+ filename: interfaceToTemplateValue(filename),
+ permissions: nil,
+ status: PROMISED,
+ }
+}
+
+func (f *FilePromise) Contents(contents any) *FilePromise {
+ f.contents = interfaceToValue(contents)
+ return f
+}
+
+func (f *FilePromise) Permissions(p *Permissions) *FilePromise {
+ f.permissions = p
+ return f
+}
+
+func (f *FilePromise) Template(contents any) *FilePromise {
+ f.contents = interfaceToTemplateValue(contents)
+ return f
+}
+
+// We want to satisfy the Promise interface
+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() {
+ filename := f.filename.String()
+ if f.contents != nil {
+ var sumFile []byte
+ sumFile, f.err = sha256sumOfFile(filename)
+ if f.err != nil {
+ if !errors.Is(f.err, fs.ErrNotExist) {
+ slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err)
+ f.status = BROKEN
+ return
+ }
+ }
+ contents := f.contents.Bytes()
+ sumContents := sha256sum(contents)
+ if !bytes.Equal(sumFile, sumContents) {
+ if f.err = writeFile(filename, contents); f.err != nil {
+ f.status = BROKEN
+ slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err)
+ return
+ }
+ f.status = REPAIRED
+ }
+ }
+ if f.permissions != nil {
+ var status Status
+ status, f.err = f.permissions.resolve(filename)
+ if f.status == PROMISED || status == BROKEN {
+ f.status = status
+ }
+ if f.err != nil {
+ slog.Error("file", "filename", f.filename, "status", f.status, "error", f.err)
+ return
+ }
+ }
+ if f.status == REPAIRED {
+ slog.Info("file", "filename", f.filename, "status", f.status)
+ for _, p := range f.chain {
+ p.Resolve()
+ }
+ } else {
+ f.status = KEPT
+ slog.Debug("file", "filename", f.filename, "status", f.status)
+ }
+}
+
+// ----- Internal --------------------------------------------------------------
+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/pkg/gonf.go b/pkg/gonf.go
new file mode 100644
index 0000000..363a227
--- /dev/null
+++ b/pkg/gonf.go
@@ -0,0 +1,52 @@
+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
+ }
+ // ----- Services ----------------------------------------------
+ status = resolveServices()
+ switch status {
+ case BROKEN:
+ return BROKEN
+ case REPAIRED:
+ continue
+ }
+ // ----- Commands ----------------------------------------------
+ status = resolveCommands()
+ switch status {
+ case BROKEN:
+ return BROKEN
+ case REPAIRED:
+ continue
+ }
+ return
+ }
+}
diff --git a/pkg/packages.go b/pkg/packages.go
new file mode 100644
index 0000000..114a227
--- /dev/null
+++ b/pkg/packages.go
@@ -0,0 +1,81 @@
+package gonf
+
+import "log/slog"
+
+// ----- Globals ---------------------------------------------------------------
+var packages []*PackagePromise
+
+// packages management functions
+var packages_install_function func([]string) (Status, []string)
+var packages_list_function func()
+var packages_update_function *CommandPromise
+
+// ----- Init ------------------------------------------------------------------
+func init() {
+ packages = make([]*PackagePromise, 0)
+}
+
+// ----- Public ----------------------------------------------------------------
+func SetPackagesConfiguration(install func([]string) (Status, []string), 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, affected := packages_install_function(p.names)
+ switch status {
+ case BROKEN:
+ slog.Error("package", "names", p.names, "status", status, "broke", affected)
+ case KEPT:
+ slog.Debug("package", "names", p.names, "status", status)
+ case REPAIRED:
+ slog.Info("package", "names", p.names, "status", status, "repaired", affected)
+ 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/pkg/permissions.go b/pkg/permissions.go
new file mode 100644
index 0000000..2bd73a9
--- /dev/null
+++ b/pkg/permissions.go
@@ -0,0 +1,89 @@
+package gonf
+
+import (
+ "errors"
+ "io/fs"
+ "os"
+ "os/user"
+ "strconv"
+ "syscall"
+)
+
+type Permissions struct {
+ group Value
+ mode Value
+ user Value
+}
+
+func ModeUserGroup(mode, user, group interface{}) *Permissions {
+ return &Permissions{
+ group: interfaceToTemplateValue(group),
+ mode: interfaceToTemplateValue(mode),
+ user: interfaceToTemplateValue(user),
+ }
+}
+
+func (p *Permissions) resolve(filename string) (Status, error) {
+ g, ok := p.group.(*IntValue)
+ if !ok {
+ if group, err := user.LookupGroup(p.group.String()); err != nil {
+ return BROKEN, err
+ } else {
+ if groupId, err := strconv.Atoi(group.Gid); err != nil {
+ return BROKEN, err
+ } else {
+ g = &IntValue{groupId}
+ p.group = g
+ }
+ }
+ }
+ m, ok := p.mode.(*IntValue)
+ if !ok {
+ if i, err := strconv.Atoi(p.mode.String()); err != nil {
+ return BROKEN, err
+ } else {
+ m = &IntValue{i}
+ p.mode = m
+ }
+ }
+ u, ok := p.user.(*IntValue)
+ if !ok {
+ if user, err := user.Lookup(p.user.String()); err != nil {
+ return BROKEN, err
+ } else {
+ if userId, err := strconv.Atoi(user.Uid); err != nil {
+ return BROKEN, err
+ } else {
+ u = &IntValue{userId}
+ p.group = u
+ }
+ }
+ }
+ var status Status = KEPT
+ if fileInfo, err := os.Lstat(filename); err != nil {
+ return BROKEN, err
+ } else {
+ gv := g.Int()
+ mv := fs.FileMode(m.Int())
+ uv := u.Int()
+ if fileInfo.Mode() != mv {
+ if err := os.Chmod(filename, mv); err != nil {
+ return BROKEN, err
+ }
+ status = REPAIRED
+ }
+ if stat, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
+ if stat.Gid != uint32(gv) || stat.Uid != uint32(uv) {
+ if err := os.Chown(filename, uv, gv); err != nil {
+ return BROKEN, err
+ }
+ status = REPAIRED
+ }
+ } else {
+ return BROKEN, errors.New("Unsupported operating system")
+ }
+ _ = gv
+ _ = uv
+ }
+ return status, nil
+}
diff --git a/pkg/promises.go b/pkg/promises.go
new file mode 100644
index 0000000..326d500
--- /dev/null
+++ b/pkg/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/pkg/services.go b/pkg/services.go
new file mode 100644
index 0000000..58c7383
--- /dev/null
+++ b/pkg/services.go
@@ -0,0 +1,96 @@
+package gonf
+
+import "log/slog"
+
+// ----- Globals ---------------------------------------------------------------
+var services []*ServicePromise
+
+// service management function
+var serviceFunction func(string, string) (Status, error)
+
+// ----- Init ------------------------------------------------------------------
+func init() {
+ services = make([]*ServicePromise, 0)
+}
+
+// ----- Public ----------------------------------------------------------------
+func SetServiceFunction(f func(string, string) (Status, error)) {
+ serviceFunction = f
+}
+
+func Service(names ...string) *ServicePromise {
+ return &ServicePromise{
+ chain: nil,
+ err: nil,
+ names: names,
+ states: nil,
+ status: PROMISED,
+ }
+}
+
+func (s *ServicePromise) State(states ...string) *ServicePromise {
+ s.states = states
+ return s
+}
+
+type ServicePromise struct {
+ chain []Promise
+ err error
+ names []string
+ states []string
+ status Status
+}
+
+func (s *ServicePromise) IfRepaired(ps ...Promise) Promise {
+ s.chain = ps
+ return s
+}
+
+func (s *ServicePromise) Promise() Promise {
+ services = append(services, s)
+ return s
+}
+
+func (s *ServicePromise) Resolve() {
+ for _, name := range s.names {
+ var repaired = false
+ for _, state := range s.states {
+ s.status, s.err = serviceFunction(name, state)
+ if s.status == BROKEN {
+ slog.Error("service", "name", name, "state", state, "status", s.status, "error", s.err)
+ return
+ } else if s.status == REPAIRED {
+ repaired = true
+ }
+ }
+ if repaired {
+ s.status = REPAIRED
+ slog.Info("service", "name", name, "state", s.states, "status", s.status)
+ } else {
+ s.status = KEPT
+ slog.Debug("service", "name", name, "state", s.states, "status", s.status)
+ }
+ }
+ if s.status == REPAIRED {
+ for _, pp := range s.chain {
+ pp.Resolve()
+ }
+ }
+}
+
+// ----- Internal --------------------------------------------------------------
+func resolveServices() (status Status) {
+ status = KEPT
+ for _, c := range services {
+ if c.status == PROMISED {
+ c.Resolve()
+ switch c.status {
+ case BROKEN:
+ return BROKEN
+ case REPAIRED:
+ status = REPAIRED
+ }
+ }
+ }
+ return
+}
diff --git a/pkg/templates.go b/pkg/templates.go
new file mode 100644
index 0000000..0b7e11b
--- /dev/null
+++ b/pkg/templates.go
@@ -0,0 +1,44 @@
+package gonf
+
+import (
+ "bytes"
+ "log/slog"
+ "text/template"
+)
+
+// ----- Globals ---------------------------------------------------------------
+var templates *template.Template
+
+// ----- Init ------------------------------------------------------------------
+func init() {
+ templates = template.New("")
+ templates.Option("missingkey=error")
+ templates.Funcs(builtinTemplateFunctions)
+}
+
+// ----- Public ----------------------------------------------------------------
+type TemplateValue struct {
+ contents []byte
+ data string
+}
+
+func (t *TemplateValue) Bytes() []byte {
+ if t.contents == nil {
+ tpl := templates.New("")
+ if _, err := tpl.Parse(t.data); err != nil {
+ slog.Error("template", "step", "Parse", "data", t.data, "error", err)
+ return nil
+ }
+ var buff bytes.Buffer
+ if err := tpl.Execute(&buff, nil /* no data needed */); err != nil {
+ slog.Error("template", "step", "Execute", "data", t.data, "error", err)
+ return nil
+ }
+ t.contents = buff.Bytes()
+ }
+ return t.contents
+}
+
+func (t TemplateValue) String() string {
+ return string(t.Bytes()[:])
+}
diff --git a/pkg/triggers.go b/pkg/triggers.go
new file mode 100644
index 0000000..370ab1c
--- /dev/null
+++ b/pkg/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/pkg/utils.go b/pkg/utils.go
new file mode 100644
index 0000000..f62d1c3
--- /dev/null
+++ b/pkg/utils.go
@@ -0,0 +1,27 @@
+package gonf
+
+import (
+ "crypto/sha256"
+)
+
+var builtinTemplateFunctions = map[string]any{
+ //"encodeURIQueryParameter": url.QueryEscape,
+ "var": getVariable,
+}
+
+func FilterSlice[T any](slice *[]T, predicate func(T) bool) {
+ i := 0
+ for _, element := range *slice {
+ if predicate(element) { // if the element matches the predicate function
+ (*slice)[i] = element // then we keep it in the slice
+ i++
+ } // otherwise the element will get overwritten
+ }
+ *slice = (*slice)[:i] // or truncated out of the slice
+}
+
+func sha256sum(contents []byte) []byte {
+ h := sha256.New()
+ h.Write(contents)
+ return h.Sum(nil)
+}
diff --git a/pkg/values.go b/pkg/values.go
new file mode 100644
index 0000000..509b6bf
--- /dev/null
+++ b/pkg/values.go
@@ -0,0 +1,104 @@
+package gonf
+
+import (
+ "fmt"
+ "log/slog"
+ "strings"
+)
+
+type Value interface {
+ Bytes() []byte
+ String() string
+}
+
+func interfaceToValue(v any) Value {
+ if vv, ok := v.([]byte); ok {
+ return &BytesValue{vv}
+ }
+ if vv, ok := v.(int); ok {
+ return &IntValue{vv}
+ }
+ if vv, ok := v.(string); ok {
+ return &StringValue{vv}
+ }
+ if vv, ok := v.(*VariablePromise); ok {
+ return vv
+ }
+ slog.Error("interfaceToValue", "value", v, "error", "Not Implemented")
+ panic(fmt.Sprintf("interfaceToValue cannot take type %T as argument. Value was %#v.", v, v))
+}
+
+func interfaceToTemplateValue(v any) Value {
+ if vv, ok := v.([]byte); ok {
+ return &TemplateValue{data: string(vv)}
+ }
+ if vv, ok := v.(int); ok {
+ return &IntValue{vv}
+ }
+ if vv, ok := v.(string); ok {
+ return &TemplateValue{data: vv}
+ }
+ if vv, ok := v.(*VariablePromise); ok {
+ return vv
+ }
+ slog.Error("interfaceToTemplateValue", "value", v, "error", "Not Implemented")
+ panic(fmt.Sprintf("interfaceToTemplateValue cannot take type %T as argument. Value was %#v.", v, v))
+}
+
+// ----- BytesValue ------------------------------------------------------------
+type BytesValue struct {
+ value []byte
+}
+
+func (b BytesValue) Bytes() []byte {
+ return b.value
+}
+func (b BytesValue) String() string {
+ return string(b.value[:])
+}
+
+// ----- IntValue --------------------------------------------------------------
+type IntValue struct {
+ value int
+}
+
+func (i IntValue) Bytes() []byte {
+ return []byte(string(i.value))
+}
+func (i IntValue) Int() int {
+ return i.value
+}
+func (i IntValue) String() string {
+ return string(i.value)
+}
+
+// ----- StringsListValue ------------------------------------------------------
+type StringsListValue struct {
+ value []string
+}
+
+func (s *StringsListValue) Append(v ...string) {
+ s.value = append(s.value, v...)
+}
+func (s StringsListValue) Bytes() []byte {
+ return []byte(s.String())
+}
+func (s StringsListValue) String() string {
+ return strings.Join(s.value, "\n")
+}
+
+// ----- StringValue -----------------------------------------------------------
+type StringValue struct {
+ value string
+}
+
+func (s StringValue) Bytes() []byte {
+ return []byte(s.value)
+}
+func (s StringValue) String() string {
+ return s.value
+}
+
+// TODO maps
+
+// TODO what else?
diff --git a/pkg/variables.go b/pkg/variables.go
new file mode 100644
index 0000000..ed09d31
--- /dev/null
+++ b/pkg/variables.go
@@ -0,0 +1,85 @@
+package gonf
+
+import (
+ "fmt"
+ "log/slog"
+)
+
+// ----- Globals ---------------------------------------------------------------
+var variables map[string]*VariablePromise
+
+// ----- Init ------------------------------------------------------------------
+func init() {
+ variables = make(map[string]*VariablePromise)
+}
+
+// ----- Public ----------------------------------------------------------------
+func AppendVariable(name string, values ...string) *VariablePromise {
+ if v, ok := variables[name]; ok {
+ if l, ok := v.value.(*StringsListValue); ok {
+ l.Append(values...)
+ }
+ return v
+ }
+ v := &VariablePromise{
+ isDefault: false,
+ name: name,
+ value: &StringsListValue{values},
+ }
+ variables[name] = v
+ return v
+}
+
+func Default(name string, value string) *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: interfaceToTemplateValue(value),
+ }
+ variables[name] = v
+ return v
+}
+
+func Variable(name string, value string) *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: interfaceToTemplateValue(value),
+ }
+ variables[name] = v
+ return v
+}
+
+type VariablePromise struct {
+ isDefault bool
+ name string
+ value Value
+}
+
+// We want VariablePromise to satisfy the Value interface
+func (s VariablePromise) Bytes() []byte {
+ return s.value.Bytes()
+}
+func (s VariablePromise) String() string {
+ return s.value.String()
+}
+
+// ----- 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)
+ panic(fmt.Sprintf("undefined variable or default %s", name))
+ }
+}