~nesv/govern

6cc2481dfa205a02640a24c4b0da7b73ee84df7c — Nick Saika 3 years ago 7019215 templates
pkg/template: New package

"pkg/template" provides some convenience functions around
"text/template" from the standard library, to collect templates and
render them.
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)
	}
}