diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/commands.go | 95 | ||||
-rw-r--r-- | pkg/files.go | 151 | ||||
-rw-r--r-- | pkg/gonf.go | 52 | ||||
-rw-r--r-- | pkg/packages.go | 81 | ||||
-rw-r--r-- | pkg/permissions.go | 89 | ||||
-rw-r--r-- | pkg/promises.go | 50 | ||||
-rw-r--r-- | pkg/services.go | 96 | ||||
-rw-r--r-- | pkg/templates.go | 44 | ||||
-rw-r--r-- | pkg/triggers.go | 11 | ||||
-rw-r--r-- | pkg/utils.go | 27 | ||||
-rw-r--r-- | pkg/values.go | 104 | ||||
-rw-r--r-- | pkg/variables.go | 85 |
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)) + } +} |