127 lines
3.6 KiB
Go
127 lines
3.6 KiB
Go
package modules
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"text/template"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// FunctionDef is a single callable function within a module.
|
|
type FunctionDef struct {
|
|
Exec string `yaml:"exec"` // Go text/template that renders to a shell command
|
|
Params map[string]string `yaml:"params"` // default parameter values
|
|
}
|
|
|
|
// ModuleDef maps function names to their definitions.
|
|
type ModuleDef map[string]FunctionDef
|
|
|
|
// Registry holds all loaded modules.
|
|
type Registry struct {
|
|
Modules map[string]ModuleDef `yaml:"modules"`
|
|
}
|
|
|
|
// LoadRegistry reads a modules.yaml file.
|
|
// A missing file is not an error — returns an empty Registry so callers don't need to
|
|
// special-case systems without a modules.yaml.
|
|
func LoadRegistry(path string) (*Registry, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return &Registry{}, nil
|
|
}
|
|
return nil, fmt.Errorf("read modules %q: %w", path, err)
|
|
}
|
|
var r Registry
|
|
if err := yaml.Unmarshal(data, &r); err != nil {
|
|
return nil, fmt.Errorf("parse modules %q: %w", path, err)
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// Resolve looks up module/function, merges params (defaults overridden by overrides),
|
|
// and renders the exec template. Returns the shell command string ready for sh -c.
|
|
func (r *Registry) Resolve(module, function string, overrides map[string]string) (string, error) {
|
|
if r == nil || r.Modules == nil {
|
|
return "", fmt.Errorf("no modules loaded")
|
|
}
|
|
mod, ok := r.Modules[module]
|
|
if !ok {
|
|
return "", fmt.Errorf("unknown module %q", module)
|
|
}
|
|
fn, ok := mod[function]
|
|
if !ok {
|
|
return "", fmt.Errorf("module %q has no function %q", module, function)
|
|
}
|
|
|
|
// Merge: start with defaults, apply overrides on top.
|
|
params := make(map[string]string, len(fn.Params)+len(overrides))
|
|
for k, v := range fn.Params {
|
|
params[k] = v
|
|
}
|
|
for k, v := range overrides {
|
|
params[k] = v
|
|
}
|
|
|
|
tmpl, err := template.New("exec").Funcs(funcMap()).Parse(fn.Exec)
|
|
if err != nil {
|
|
return "", fmt.Errorf("module %q function %q: parse template: %w", module, function, err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, params); err != nil {
|
|
return "", fmt.Errorf("module %q function %q: render template: %w", module, function, err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// Execute resolves the module/function and runs it via sh -c.
|
|
func (r *Registry) Execute(module, function string, overrides map[string]string) error {
|
|
shell, err := r.Resolve(module, function, overrides)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c := exec.Command("sh", "-c", shell)
|
|
c.Stdout = os.Stdout
|
|
c.Stderr = os.Stderr
|
|
return c.Run()
|
|
}
|
|
|
|
// funcMap returns the template helper functions available in exec templates.
|
|
func funcMap() template.FuncMap {
|
|
return template.FuncMap{
|
|
// env returns the value of an environment variable.
|
|
// Use this to keep secrets (tokens, passwords) out of modules.yaml.
|
|
"env": os.Getenv,
|
|
|
|
// envDefault returns the value of an environment variable, or
|
|
// fallback if the variable is empty/unset.
|
|
"envDefault": func(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
},
|
|
|
|
// expiry converts a duration string like "+1h" to a Unix epoch timestamp string.
|
|
// Pass "" or "0" to get "0" (no expiry). Supports Go duration syntax (e.g. "30m", "2h").
|
|
// Note: time.ParseDuration does not support days (d) or weeks (w).
|
|
"expiry": func(s string) string {
|
|
if s == "" || s == "0" {
|
|
return "0"
|
|
}
|
|
d, err := time.ParseDuration(s)
|
|
if err != nil {
|
|
return "0"
|
|
}
|
|
return strconv.FormatInt(time.Now().Add(d).Unix(), 10)
|
|
},
|
|
}
|
|
}
|