spry/main.go

281 lines
6.3 KiB
Go
Raw Normal View History

2023-10-12 16:35:46 -04:00
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
2023-10-13 11:20:35 -04:00
"text/template"
"time"
2023-10-12 16:35:46 -04:00
2023-10-13 07:07:10 -04:00
"github.com/peterbourgon/mergemap"
2023-10-12 16:35:46 -04:00
sprig "github.com/Masterminds/sprig/v3"
yaml "gopkg.in/yaml.v3"
2023-10-12 16:35:46 -04:00
)
type StrSet map[string]struct{}
func (s *StrSet) Add(str string) {
(*s)[str] = struct{}{}
}
func (s *StrSet) Has(str string) bool {
if _, ok := (*s)[str]; ok {
return true
}
return false
}
func (s *StrSet) AddHas(str string) bool {
if ok := s.Has(str); ok {
return ok
}
s.Add(str)
return false
}
2023-10-12 16:35:46 -04:00
type valFileSlice []string
func (v *valFileSlice) String() string {
return fmt.Sprintf("%v", *v)
}
func (v *valFileSlice) Set(value string) error {
*v = append(*v, value)
return nil
}
type valEnt struct {
address []string
v string
}
type valSlice []valEnt
func (v *valSlice) String() string {
return fmt.Sprintf("%v", *v)
}
func (v *valSlice) Set(value string) error {
pieces := strings.Split(value, "=")
if len(pieces) < 2 {
return fmt.Errorf("Invalid value setting.")
}
*v = append(*v, valEnt{
strings.Split(pieces[0], "."),
pieces[1],
})
return nil
}
var flags struct {
2023-10-13 09:04:13 -04:00
valuesFiles valFileSlice
values valSlice
outputDelimiter string
2023-10-13 09:26:16 -04:00
helmCompat bool
2023-10-12 16:35:46 -04:00
}
func init() {
2023-10-13 09:13:18 -04:00
flag.Var(&flags.values, "set", "Set a specific value (foo.bar=spam)")
flag.Var(&flags.valuesFiles, "f", "Specify a values file (JSON or YAML)")
2023-10-13 09:04:13 -04:00
flag.StringVar(
&flags.outputDelimiter,
"d", "---",
"Output delimiter between template files rendered",
)
2023-10-13 09:51:56 -04:00
flag.BoolVar(
&flags.helmCompat,
"helm", false,
"Helm compatibility: provided values will be nested under .Values",
)
2023-10-12 16:35:46 -04:00
flag.Parse()
2023-10-13 09:51:56 -04:00
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [Options] [Template Files]:\n", os.Args[0])
flag.PrintDefaults()
fmt.Fprintf(flag.CommandLine.Output(), "\n")
fmt.Fprintf(
flag.CommandLine.Output(),
"Template Files can also be directories to recurse for templates.\n",
)
fmt.Fprintf(flag.CommandLine.Output(), "\n")
fmt.Fprintf(
flag.CommandLine.Output(),
"If no template files are provided it will attempt read from STDIN.\n",
)
fmt.Fprintf(
flag.CommandLine.Output(),
"If no input is available from STDIN it will print this usage message instead.\n",
)
fmt.Fprintf(
flag.CommandLine.Output(),
"Pass only '-' to Template Files to force waiting for input.\n",
)
fmt.Fprintf(flag.CommandLine.Output(), "\n")
}
2023-10-12 16:35:46 -04:00
}
func checkErr(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %+v", err)
os.Exit(1)
}
}
func expandFiles(files []string) (expFiles []string) {
expFiles = []string{}
fileSet := StrSet{}
directories := []string{}
// Ensure we don't process any provided values files as templates.
for _, path := range flags.valuesFiles {
abs, err := filepath.Abs(path)
if err != nil {
fileSet.Add(path)
break
}
fileSet.Add(abs)
}
2023-10-12 16:35:46 -04:00
for _, path := range files {
fileInfo, err := os.Stat(path)
checkErr(err)
if fileInfo.IsDir() {
// When provided path is a directory, defer loading files from it.
directories = append(directories, path)
} else {
abs, err := filepath.Abs(path)
if err != nil {
// On failure to find the absolute path, just use the provided path.
abs = path
}
// Deduplicate files.
if ok := fileSet.AddHas(abs); !ok {
expFiles = append(expFiles, path)
}
}
// Walk each provided directory to find templates.
for _, path := range directories {
2023-10-12 16:35:46 -04:00
filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: %+v", err)
2023-10-13 09:04:13 -04:00
return nil
2023-10-12 16:35:46 -04:00
}
if d.IsDir() {
2023-10-13 09:04:13 -04:00
// Don't recurse hidden directories.
if d.Name() != "." && strings.HasPrefix(d.Name(), ".") {
return filepath.SkipDir
}
return nil
}
// Skip hidden files.
if strings.HasPrefix(d.Name(), ".") {
2023-10-12 16:35:46 -04:00
return nil
}
abs, err := filepath.Abs(path)
if err != nil {
// On failure to find the absolute path, just use the provided path.
abs = path
}
// Deduplicate files.
if ok := fileSet.AddHas(abs); !ok {
expFiles = append(expFiles, path)
}
2023-10-12 16:35:46 -04:00
return nil
})
}
}
return
}
2023-10-13 09:26:16 -04:00
func mergeValues() (res map[string]interface{}) {
res = map[string]interface{}{}
result := res
if flags.helmCompat {
res["Values"] = map[string]interface{}{}
result = res["Values"].(map[string]interface{})
}
2023-10-12 16:35:46 -04:00
for _, vf := range flags.valuesFiles {
2023-10-13 07:07:10 -04:00
vfContent, err := os.ReadFile(vf)
2023-10-12 16:35:46 -04:00
checkErr(err)
if strings.HasSuffix(vf, ".json") {
2023-10-13 07:07:10 -04:00
err = json.Unmarshal(vfContent, &result)
checkErr(err)
} else {
err = yaml.Unmarshal(vfContent, result)
checkErr(err)
}
2023-10-12 16:35:46 -04:00
}
2023-10-13 07:07:10 -04:00
for _, v := range flags.values {
valueMap := map[string]interface{}{}
subMap := valueMap
for i, node := range v.address {
2023-10-12 16:35:46 -04:00
if i == len(v.address)-1 {
subMap[node] = v.v
2023-10-13 07:07:10 -04:00
break
2023-10-12 16:35:46 -04:00
}
2023-10-13 07:07:10 -04:00
subMap[node] = map[string]interface{}{}
subMap = subMap[node].(map[string]interface{})
2023-10-12 16:35:46 -04:00
}
2023-10-13 07:07:10 -04:00
mergemap.Merge(result, valueMap)
2023-10-12 16:35:46 -04:00
}
return
}
func main() {
var err error
tpl := template.New("base").Funcs(sprig.FuncMap())
2023-10-13 09:04:13 -04:00
values := mergeValues()
// fmt.Fprintf(os.Stderr, "%+v\n", values)
if len(flag.Args()) == 0 {
// With no provided arguments read from STDIN if anything is immediately available through it.
// otherwise print the usage and exit.
ch := make(chan struct{})
go func() {
select {
case <-ch:
return
case <-time.After(100 * time.Millisecond):
flag.Usage()
os.Exit(2)
}
}()
var data []byte
data, err = io.ReadAll(os.Stdin)
// Tell the waiting routine that we got input on STDIN.
ch <- struct{}{}
checkErr(err)
tpl, err = tpl.Parse(string(data))
checkErr(err)
} else if len(flag.Args()) == 1 && flag.Args()[0] == "-" {
// If only a single non-flag argument is provided and it's a -, wait for STDIN indefinitely.
2023-10-12 16:35:46 -04:00
var data []byte
data, err = io.ReadAll(os.Stdin)
checkErr(err)
tpl, err = tpl.Parse(string(data))
checkErr(err)
} else {
2023-10-13 09:04:13 -04:00
tplFiles := expandFiles(flag.Args())
// Otherwise use the provided tplFiles/directories for templates to render.
// fmt.Fprintf(os.Stderr, "%+v\n", tplFiles)
tpl, err = tpl.ParseFiles(tplFiles...)
2023-10-12 16:35:46 -04:00
checkErr(err)
2023-10-13 09:04:13 -04:00
for i, file := range tplFiles {
if i > 0 {
fmt.Println(flags.outputDelimiter)
}
2023-10-13 11:25:33 -04:00
checkErr(tpl.ExecuteTemplate(os.Stdout, filepath.Base(file), values))
2023-10-13 09:04:13 -04:00
}
return
2023-10-12 16:35:46 -04:00
}
2023-10-13 09:04:13 -04:00
checkErr(tpl.Execute(os.Stdout, values))
2023-10-12 16:35:46 -04:00
}