package main import ( "errors" "fmt" "net/url" "os" "path/filepath" "strconv" "strings" "syscall" "text/template" "github.com/Masterminds/sprig/v3" "github.com/jwilder/gojq" ) var errNotADirectory = errors.New("not a directory") type templateConfig struct { noOverwrite bool strict bool delims delimsFlag data struct { Env map[string]string } } func exists(path string) (bool, error) { _, err := os.Stat(path) switch { case err == nil: return true, nil case os.IsNotExist(err): return false, nil default: return false, err } } func isTrue(s string) bool { b, _ := strconv.ParseBool(strings.ToLower(s)) return b } func jsonQuery(jsonObj string, query string) (interface{}, error) { parser, err := gojq.NewStringQuery(jsonObj) if err != nil { return nil, err } return parser.Query(query) } func readFile(fileName string) (string, error) { data, err := os.ReadFile(fileName) //nolint:gosec // File inclusion via variable. if os.IsNotExist(err) { return "", nil } return string(data), err } func processTemplatePaths(cfg templateConfig, paths []string) error { for _, srcdst := range paths { src, dst, _ := strings.Cut(srcdst, ":") fi, err := os.Stat(src) if err == nil { if fi.IsDir() { err = processTemplateDir(cfg, src, dst) } else { err = processTemplate(cfg, src, dst) } } if err != nil { return err } } return nil } func processTemplate(cfg templateConfig, src, dst string) error { option := "missingkey=default" if cfg.strict { option = "missingkey=error" } tmpl, err := template.New(filepath.Base(src)). Funcs(sprig.TxtFuncMap()). Funcs(template.FuncMap{ "exists": exists, "parseUrl": url.Parse, "isTrue": isTrue, "jsonQuery": jsonQuery, "readFile": readFile, }). Delims(cfg.delims[0], cfg.delims[1]). Option(option). ParseFiles(src) if err != nil { return err } file := os.Stdout if dst != "" { file, err = createDestFile(src, dst, cfg.noOverwrite) if err != nil { return err } defer warnIfFail(file.Close) } return tmpl.Execute(file, cfg.data) } func processTemplateDir(cfg templateConfig, src, dst string) error { if dst != "" { err := ensureDestDir(src, dst) if err != nil { return err } } entries, err := os.ReadDir(src) if err != nil { return err } for _, entry := range entries { nextSrc := filepath.Join(src, entry.Name()) nextDst := filepath.Join(dst, entry.Name()) if dst == "" { nextDst = "" } if entry.IsDir() { err = processTemplateDir(cfg, nextSrc, nextDst) } else { err = processTemplate(cfg, nextSrc, nextDst) } if err != nil { return err } } return nil } func createDestFile(src, dst string, noOverwrite bool) (*os.File, error) { //nolint:revive // TODO. like, err := os.Stat(src) if err != nil { return nil, err } likeSys, ok := like.Sys().(*syscall.Stat_t) openFlags := os.O_RDWR | os.O_CREATE | os.O_TRUNC if noOverwrite { openFlags = os.O_RDWR | os.O_CREATE | os.O_EXCL } file, err := os.OpenFile(dst, openFlags, like.Mode().Perm()) //nolint:gosec // File inclusion. if err != nil { return nil, err } if ok { err = file.Chown(int(likeSys.Uid), int(likeSys.Gid)) if err != nil && !os.IsPermission(err) { warnIfFail(file.Close) return nil, err } } return file, nil } func ensureDestDir(src, dst string) error { like, err := os.Stat(src) if err != nil { return err } if !like.IsDir() { return fmt.Errorf("%w: %s", errNotADirectory, src) } likeSys, ok := like.Sys().(*syscall.Stat_t) fi, err := os.Stat(dst) switch { case err == nil && fi.IsDir(): return nil case err == nil: return fmt.Errorf("%w: %s", errNotADirectory, dst) case !os.IsNotExist(err): return err } err = os.Mkdir(dst, like.Mode()) if err == nil && ok { err = os.Chown(dst, int(likeSys.Uid), int(likeSys.Gid)) if os.IsPermission(err) { err = nil } } return err }