From 560becfd32dd7355547938f3c6229060dd395aab Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Thu, 7 Mar 2024 00:54:35 +0100 Subject: chore(repo): renamed gonf subfolder to something more traditional in go land --- gonf/commands.go | 95 --------------------------- gonf/files.go | 151 ------------------------------------------- gonf/gonf.go | 52 --------------- gonf/packages.go | 81 ----------------------- gonf/permissions.go | 89 ------------------------- gonf/promises.go | 50 -------------- gonf/services.go | 96 --------------------------- gonf/templates.go | 44 ------------- gonf/triggers.go | 11 ---- gonf/utils.go | 27 -------- gonf/values.go | 104 ----------------------------- gonf/variables.go | 85 ------------------------ pkg/commands.go | 95 +++++++++++++++++++++++++++ pkg/files.go | 151 +++++++++++++++++++++++++++++++++++++++++++ pkg/gonf.go | 52 +++++++++++++++ pkg/packages.go | 81 +++++++++++++++++++++++ pkg/permissions.go | 89 +++++++++++++++++++++++++ pkg/promises.go | 50 ++++++++++++++ pkg/services.go | 96 +++++++++++++++++++++++++++ pkg/templates.go | 44 +++++++++++++ pkg/triggers.go | 11 ++++ pkg/utils.go | 27 ++++++++ pkg/values.go | 104 +++++++++++++++++++++++++++++ pkg/variables.go | 85 ++++++++++++++++++++++++ stdlib/os/debian/apt.go | 2 +- stdlib/os/systemd/systemd.go | 2 +- 26 files changed, 887 insertions(+), 887 deletions(-) delete mode 100644 gonf/commands.go delete mode 100644 gonf/files.go delete mode 100644 gonf/gonf.go delete mode 100644 gonf/packages.go delete mode 100644 gonf/permissions.go delete mode 100644 gonf/promises.go delete mode 100644 gonf/services.go delete mode 100644 gonf/templates.go delete mode 100644 gonf/triggers.go delete mode 100644 gonf/utils.go delete mode 100644 gonf/values.go delete mode 100644 gonf/variables.go create mode 100644 pkg/commands.go create mode 100644 pkg/files.go create mode 100644 pkg/gonf.go create mode 100644 pkg/packages.go create mode 100644 pkg/permissions.go create mode 100644 pkg/promises.go create mode 100644 pkg/services.go create mode 100644 pkg/templates.go create mode 100644 pkg/triggers.go create mode 100644 pkg/utils.go create mode 100644 pkg/values.go create mode 100644 pkg/variables.go diff --git a/gonf/commands.go b/gonf/commands.go deleted file mode 100644 index b5a11ca..0000000 --- a/gonf/commands.go +++ /dev/null @@ -1,95 +0,0 @@ -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 deleted file mode 100644 index 35420d4..0000000 --- a/gonf/files.go +++ /dev/null @@ -1,151 +0,0 @@ -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/gonf/gonf.go b/gonf/gonf.go deleted file mode 100644 index 363a227..0000000 --- a/gonf/gonf.go +++ /dev/null @@ -1,52 +0,0 @@ -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/gonf/packages.go b/gonf/packages.go deleted file mode 100644 index 114a227..0000000 --- a/gonf/packages.go +++ /dev/null @@ -1,81 +0,0 @@ -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/gonf/permissions.go b/gonf/permissions.go deleted file mode 100644 index 2bd73a9..0000000 --- a/gonf/permissions.go +++ /dev/null @@ -1,89 +0,0 @@ -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/gonf/promises.go b/gonf/promises.go deleted file mode 100644 index 326d500..0000000 --- a/gonf/promises.go +++ /dev/null @@ -1,50 +0,0 @@ -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/services.go b/gonf/services.go deleted file mode 100644 index 58c7383..0000000 --- a/gonf/services.go +++ /dev/null @@ -1,96 +0,0 @@ -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/gonf/templates.go b/gonf/templates.go deleted file mode 100644 index 0b7e11b..0000000 --- a/gonf/templates.go +++ /dev/null @@ -1,44 +0,0 @@ -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/gonf/triggers.go b/gonf/triggers.go deleted file mode 100644 index 370ab1c..0000000 --- a/gonf/triggers.go +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index f62d1c3..0000000 --- a/gonf/utils.go +++ /dev/null @@ -1,27 +0,0 @@ -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/gonf/values.go b/gonf/values.go deleted file mode 100644 index 509b6bf..0000000 --- a/gonf/values.go +++ /dev/null @@ -1,104 +0,0 @@ -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/gonf/variables.go b/gonf/variables.go deleted file mode 100644 index ed09d31..0000000 --- a/gonf/variables.go +++ /dev/null @@ -1,85 +0,0 @@ -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)) - } -} 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)) + } +} diff --git a/stdlib/os/debian/apt.go b/stdlib/os/debian/apt.go index 77c3b21..5ece696 100644 --- a/stdlib/os/debian/apt.go +++ b/stdlib/os/debian/apt.go @@ -9,7 +9,7 @@ import ( "os/exec" "strings" - "git.adyxax.org/adyxax/gonf/v2/gonf" + gonf "git.adyxax.org/adyxax/gonf/v2/pkg" "git.adyxax.org/adyxax/gonf/v2/stdlib/os/systemd" ) diff --git a/stdlib/os/systemd/systemd.go b/stdlib/os/systemd/systemd.go index 782548d..5af1e91 100644 --- a/stdlib/os/systemd/systemd.go +++ b/stdlib/os/systemd/systemd.go @@ -4,7 +4,7 @@ import ( "errors" "os/exec" - "git.adyxax.org/adyxax/gonf/v2/gonf" + gonf "git.adyxax.org/adyxax/gonf/v2/pkg" ) func Promise() { -- cgit v1.2.3