M cmd/govern/main.go => cmd/govern/main.go +69 -2
@@ 7,12 7,14 @@ import (
"sort"
"strings"
"text/tabwriter"
+ gotemplate "text/template"
"time"
"git.sr.ht/~nesv/govern/pkg/facts"
"git.sr.ht/~nesv/govern/pkg/resource"
"git.sr.ht/~nesv/govern/pkg/runner"
"git.sr.ht/~nesv/govern/pkg/state"
+ "git.sr.ht/~nesv/govern/pkg/template"
"github.com/nesv/cmndr"
"github.com/pkg/errors"
@@ 26,6 28,7 @@ var (
factsIgnore []string
stateDirs []string
statesIgnore []string
+ templateDirs []string
pretend bool
withAttributes bool
runnerDirs []string
@@ 47,6 50,10 @@ var (
"/usr/local/lib/govern/bin",
"/usr/lib/govern/bin",
}
+
+ defaultTemplateDirs = []string{
+ "/usr/local/etc/govern/templates.d",
+ }
)
func main() {
@@ 66,6 73,7 @@ func main() {
root.Flags.BoolVar(&withAttributes, "with-attributes", false, "Show resource attributes with --pretend, -n")
root.Flags.StringSliceVarP(&runnerDirs, "runners-directory", "R", defaultRunnerDirs, "Directories to look for runners in")
root.Flags.BoolVar(&withSignals, "with-signals", false, "resources: Show the signals a resource can send")
+ root.Flags.StringSliceVarP(&templateDirs, "template-directory", "T", defaultTemplateDirs, "Directories to look for templates")
facts := cmndr.New("facts", gatherFacts)
facts.Description = "Collect facts for the current host"
@@ 87,6 95,14 @@ func main() {
runners.Description = "List all discovered runners"
root.AddCmd(runners)
+ templates := cmndr.New("templates", showTemplates)
+ templates.Description = "List all discovered templates"
+ root.AddCmd(templates)
+
+ render := cmndr.New("render", renderTemplate)
+ render.Description = "Render a template"
+ root.AddCmd(render)
+
root.Exec()
}
@@ 133,7 149,7 @@ func showStates(cmd *cmndr.Cmd, args []string) error {
}
func collectStates() (*state.States, error) {
- for len(stateDirs) == 0 {
+ if len(stateDirs) == 0 {
stateDirs = make([]string, 0, len(defaultStateDirs))
copy(stateDirs, defaultStateDirs)
}
@@ 295,7 311,7 @@ func showRunners(cmd *cmndr.Cmd, args []string) error {
}
func collectRunners() (runner.Runners, error) {
- for len(runnerDirs) == 0 {
+ if len(runnerDirs) == 0 {
runnerDirs = make([]string, 0, len(defaultRunnerDirs))
copy(runnerDirs, defaultRunnerDirs)
}
@@ 307,3 323,54 @@ func collectRunners() (runner.Runners, error) {
return runner.Gather(options...)
}
+
+func showTemplates(cmd *cmndr.Cmd, args []string) error {
+ tpl, err := collectTemplates()
+ if err != nil {
+ return fmt.Errorf("collect templates: %w", err)
+ }
+ if tpl == nil {
+ return nil
+ }
+
+ for _, t := range tpl.Templates() {
+ fmt.Println(t.Name())
+ }
+ return nil
+}
+
+func collectTemplates() (*gotemplate.Template, error) {
+ if len(templateDirs) == 0 {
+ templateDirs = make([]string, 0, len(defaultTemplateDirs))
+ copy(templateDirs, defaultTemplateDirs)
+ }
+
+ ff, err := collectFacts()
+ if err != nil {
+ return nil, errors.Wrap(err, "collect facts")
+ }
+
+ return template.LoadDirs(ff, templateDirs...)
+}
+
+func renderTemplate(cmd *cmndr.Cmd, args []string) error {
+ if len(args) == 0 {
+ return nil
+ }
+
+ tpl, err := collectTemplates()
+ if err != nil {
+ return fmt.Errorf("collect templates: %w", err)
+ }
+
+ t := tpl.Lookup(args[0])
+ if t == nil {
+ return fmt.Errorf("no such template: %s", args[0])
+ }
+
+ if err := template.Render(os.Stdout, t); err != nil {
+ return fmt.Errorf("render template: %s: %w", t.Name(), err)
+ }
+
+ return nil
+}
A example/template/bar/baz.tpl => example/template/bar/baz.tpl +3 -0
@@ 0,0 1,3 @@
+{{define "baz"}}
+yup
+{{end}}<
\ No newline at end of file
A example/template/baz.tpl => example/template/baz.tpl +3 -0
@@ 0,0 1,3 @@
+{{define "baz" -}}
+{{yesno true}}
+{{end}}<
\ No newline at end of file
A example/template/foo.tpl => example/template/foo.tpl +8 -0
@@ 0,0 1,8 @@
+{{define "foo" -}}
+hello from {{ fact "os/name" }}
+
+The following packages are installed:
+{{range fact "packages/installed" | split "," -}}
+- {{.}}
+{{end}}
+{{end}}<
\ No newline at end of file
A pkg/template/template.go => pkg/template/template.go +99 -0
@@ 0,0 1,99 @@
+package template
+
+import (
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+ "text/template"
+
+ "git.sr.ht/~nesv/govern/pkg/facts"
+
+ kerrors "k8s.io/apimachinery/pkg/util/errors"
+)
+
+func LoadDirs(ff *facts.Facts, dirnames ...string) (*template.Template, error) {
+ // Walk each directory in "templateDirs",
+ // and collect all of the files therein.
+ var (
+ files []string
+ errs []error
+ )
+ for _, dir := range dirnames {
+ if !filepath.IsAbs(dir) {
+ var err error
+ dir, err = filepath.Abs(dir)
+ if err != nil {
+ errs = append(errs, fmt.Errorf("make path absolute: %w", err))
+ continue
+ }
+ }
+
+ info, err := os.Stat(dir)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ if !info.IsDir() {
+ errs = append(errs, fmt.Errorf("not a directory: %s", dir))
+ continue
+ }
+
+ if err := filepath.Walk(dir, func(pathname string, info fs.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() {
+ return nil
+ }
+
+ // Skip any files that are not immediately within the
+ // directory we are walking.
+ if d, _ := filepath.Split(pathname); strings.TrimPrefix(d, dir+string(filepath.Separator)) != "" {
+ return nil
+ }
+
+ files = append(files, pathname)
+ return nil
+ }); err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ }
+
+ if len(files) == 0 {
+ return nil, nil
+ }
+
+ // Parse each file.
+ tpl, err := template.New("").Funcs(template.FuncMap{
+ "fact": getFact(ff),
+ "split": func(sep, s string) []string {
+ return strings.Split(s, sep)
+ },
+ "yesno": func(b bool) string {
+ if b {
+ return "yes"
+ }
+ return "no"
+ },
+ }).ParseFiles(files...)
+ if err != nil {
+ errs = append(errs, fmt.Errorf("parse template files: %w", err))
+ }
+
+ return tpl, kerrors.NewAggregate(errs)
+}
+
+func Render(w io.Writer, t *template.Template) error {
+ return t.Execute(w, nil)
+}
+
+func getFact(ff *facts.Facts) func(name string) (string, error) {
+ return func(name string) (string, error) {
+ return ff.Get(name)
+ }
+}