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"
|
2023-10-13 06:22:36 -04:00
|
|
|
"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"
|
2023-10-13 06:22:36 -04:00
|
|
|
yaml "gopkg.in/yaml.v3"
|
2023-10-15 07:43:28 -04:00
|
|
|
|
2023-10-23 08:14:37 -04:00
|
|
|
htl "gitlab.com/CRThaze/helm-tmpl-lang"
|
2023-10-12 16:35:46 -04:00
|
|
|
)
|
|
|
|
|
2023-10-13 08:01:12 -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-23 04:24:16 -04:00
|
|
|
valuesFiles valFileSlice
|
|
|
|
values valSlice
|
|
|
|
outputDelimiter string
|
|
|
|
helm bool
|
|
|
|
helmStrict bool
|
|
|
|
helmDNSFunc bool
|
|
|
|
helmLint bool
|
|
|
|
helmNestValues bool
|
|
|
|
outputFilenamePrefix string
|
|
|
|
outputFilenameSTDERR bool
|
|
|
|
outputDelimiterSTDERR 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(
|
2023-10-21 09:29:26 -04:00
|
|
|
&flags.helm,
|
2023-10-13 09:51:56 -04:00
|
|
|
"helm", false,
|
2023-10-21 09:29:26 -04:00
|
|
|
"Use Helm templating language (superset of Sprig).",
|
|
|
|
)
|
|
|
|
flag.BoolVar(
|
|
|
|
&flags.helmStrict,
|
2023-10-23 05:48:16 -04:00
|
|
|
"helmStrict", false,
|
2023-10-21 09:29:26 -04:00
|
|
|
"When using Helm Tpl Lang, use strict rendering.",
|
|
|
|
)
|
|
|
|
flag.BoolVar(
|
|
|
|
&flags.helmDNSFunc,
|
2023-10-23 04:24:16 -04:00
|
|
|
"helmDNS", true,
|
2023-10-21 09:29:26 -04:00
|
|
|
"When using Helm Tpl Lang, support DNS resolution.",
|
|
|
|
)
|
|
|
|
flag.BoolVar(
|
|
|
|
&flags.helmLint,
|
|
|
|
"helmLint", false,
|
|
|
|
"When using Helm Tpl Lang, enable the Linting Mode.",
|
2023-10-13 09:51:56 -04:00
|
|
|
)
|
2023-10-23 04:24:16 -04:00
|
|
|
flag.BoolVar(
|
|
|
|
&flags.helmNestValues,
|
|
|
|
"helmNestValues", true,
|
|
|
|
"When using Helm Tpl Lang, Nest provided values under '.Values'",
|
|
|
|
)
|
|
|
|
flag.StringVar(
|
|
|
|
&flags.outputFilenamePrefix,
|
|
|
|
"fp", "# ",
|
|
|
|
"Prefix for the filename in the output.",
|
|
|
|
)
|
|
|
|
flag.BoolVar(
|
|
|
|
&flags.outputDelimiterSTDERR,
|
|
|
|
"delimErr", false,
|
|
|
|
"Whether to print output delimiter to STDERR.",
|
|
|
|
)
|
|
|
|
flag.BoolVar(
|
|
|
|
&flags.outputFilenameSTDERR,
|
|
|
|
"filenameErr", true,
|
|
|
|
"Whether to print filename to STDERR.",
|
|
|
|
)
|
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-15 07:43:28 -04:00
|
|
|
fmt.Fprintf(
|
|
|
|
flag.CommandLine.Output(),
|
2023-10-21 09:29:26 -04:00
|
|
|
"Helm Mode: use the Helm Templating Language/Engine\n"+
|
|
|
|
"with the following caveats:\n"+
|
|
|
|
"\t- 'lookup' function unavailable.\n",
|
2023-10-15 07:43:28 -04:00
|
|
|
)
|
|
|
|
fmt.Fprintf(flag.CommandLine.Output(), "\n")
|
2023-10-13 09:51:56 -04:00
|
|
|
}
|
2023-10-12 16:35:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func checkErr(err error) {
|
|
|
|
if err != nil {
|
2023-10-23 04:24:16 -04:00
|
|
|
fmt.Fprintf(os.Stderr, "ERROR: %+v\n", err)
|
2023-10-12 16:35:46 -04:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func expandFiles(files []string) (expFiles []string) {
|
|
|
|
expFiles = []string{}
|
2023-10-13 08:01:12 -04:00
|
|
|
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() {
|
2023-10-13 08:01:12 -04:00
|
|
|
// 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
|
|
|
|
}
|
2023-10-13 08:01:12 -04:00
|
|
|
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
|
2023-10-23 04:24:16 -04:00
|
|
|
if flags.helm && flags.helmNestValues {
|
2023-10-13 09:26:16 -04:00
|
|
|
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)
|
2023-10-13 06:22:36 -04:00
|
|
|
if strings.HasSuffix(vf, ".json") {
|
2023-10-13 07:07:10 -04:00
|
|
|
err = json.Unmarshal(vfContent, &result)
|
2023-10-13 06:22:36 -04:00
|
|
|
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
|
2023-10-23 08:14:37 -04:00
|
|
|
var ht *htl.Templater
|
|
|
|
lo := &htl.LangOpts{
|
2023-10-21 09:29:26 -04:00
|
|
|
Strict: flags.helmStrict,
|
|
|
|
LintMode: flags.helmLint,
|
|
|
|
EnableDNS: flags.helmDNSFunc,
|
|
|
|
}
|
2023-10-12 16:35:46 -04:00
|
|
|
tpl := template.New("base").Funcs(sprig.FuncMap())
|
2023-10-13 08:01:12 -04:00
|
|
|
|
2023-10-13 09:04:13 -04:00
|
|
|
values := mergeValues()
|
|
|
|
|
2023-10-13 06:22:36 -04:00
|
|
|
if len(flag.Args()) == 0 {
|
2023-10-13 08:01:12 -04:00
|
|
|
// With no provided arguments read from STDIN if anything is immediately available through it.
|
|
|
|
// otherwise print the usage and exit.
|
2023-10-13 06:22:36 -04:00
|
|
|
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)
|
2023-10-13 08:01:12 -04:00
|
|
|
// Tell the waiting routine that we got input on STDIN.
|
2023-10-13 06:22:36 -04:00
|
|
|
ch <- struct{}{}
|
|
|
|
checkErr(err)
|
2023-10-15 07:43:28 -04:00
|
|
|
|
2023-10-21 09:29:26 -04:00
|
|
|
if flags.helm {
|
2023-10-23 08:14:37 -04:00
|
|
|
ht = htl.NewTemplaterWithBytes(data, nil)
|
2023-10-15 07:43:28 -04:00
|
|
|
fmt.Fprintf(os.Stderr, "WARN: Helm Compatibility only partially implemented.\n")
|
|
|
|
tpl, err = tpl.Parse(string(data))
|
|
|
|
} else {
|
|
|
|
tpl, err = tpl.Parse(string(data))
|
|
|
|
}
|
2023-10-13 06:22:36 -04:00
|
|
|
checkErr(err)
|
|
|
|
} else if len(flag.Args()) == 1 && flag.Args()[0] == "-" {
|
2023-10-13 08:01:12 -04:00
|
|
|
// 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)
|
2023-10-15 07:43:28 -04:00
|
|
|
|
2023-10-21 09:29:26 -04:00
|
|
|
if flags.helm {
|
2023-10-23 08:14:37 -04:00
|
|
|
ht = htl.NewTemplaterWithBytes(data, lo)
|
2023-10-15 07:43:28 -04:00
|
|
|
} else {
|
|
|
|
tpl, err = tpl.Parse(string(data))
|
|
|
|
}
|
2023-10-12 16:35:46 -04:00
|
|
|
checkErr(err)
|
|
|
|
} else {
|
2023-10-13 09:04:13 -04:00
|
|
|
// Otherwise use the provided tplFiles/directories for templates to render.
|
2023-10-15 07:43:28 -04:00
|
|
|
tplFiles := expandFiles(flag.Args())
|
2023-10-21 09:29:26 -04:00
|
|
|
if flags.helm {
|
2023-10-23 08:14:37 -04:00
|
|
|
ht, err = htl.NewTemplaterWithFiles(tplFiles, lo)
|
2023-10-21 09:06:55 -04:00
|
|
|
checkErr(err)
|
2023-10-21 09:29:26 -04:00
|
|
|
rendered, err := ht.Render(values)
|
2023-10-15 07:43:28 -04:00
|
|
|
checkErr(err)
|
2023-10-21 09:29:26 -04:00
|
|
|
first := true
|
|
|
|
for filename, content := range rendered {
|
|
|
|
if first {
|
|
|
|
first = false
|
|
|
|
} else {
|
2023-10-23 04:24:16 -04:00
|
|
|
out := os.Stdout
|
|
|
|
if flags.outputDelimiterSTDERR {
|
|
|
|
out = os.Stderr
|
|
|
|
}
|
|
|
|
fmt.Fprintf(out, "%s\n", flags.outputDelimiter)
|
|
|
|
|
|
|
|
out = os.Stdout
|
|
|
|
if flags.outputFilenameSTDERR {
|
|
|
|
out = os.Stderr
|
|
|
|
}
|
|
|
|
fmt.Fprintf(out, "%s%s\n", flags.outputFilenamePrefix, filename)
|
2023-10-15 07:43:28 -04:00
|
|
|
}
|
2023-10-23 04:24:16 -04:00
|
|
|
fmt.Fprint(os.Stdout, content)
|
2023-10-15 07:43:28 -04:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
tpl, err = tpl.ParseFiles(tplFiles...)
|
|
|
|
checkErr(err)
|
|
|
|
for i, file := range tplFiles {
|
|
|
|
if i > 0 {
|
2023-10-23 04:24:16 -04:00
|
|
|
out := os.Stdout
|
|
|
|
if flags.outputDelimiterSTDERR {
|
|
|
|
out = os.Stderr
|
|
|
|
}
|
|
|
|
fmt.Fprintf(out, "%s\n", flags.outputDelimiter)
|
|
|
|
|
|
|
|
out = os.Stdout
|
|
|
|
if flags.outputFilenameSTDERR {
|
|
|
|
out = os.Stderr
|
|
|
|
}
|
|
|
|
fmt.Fprintf(out, "%s%s\n", flags.outputFilenamePrefix, file)
|
2023-10-15 07:43:28 -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-21 09:06:55 -04:00
|
|
|
if ht != nil {
|
2023-10-21 09:29:26 -04:00
|
|
|
rendered, err := ht.Render(values)
|
|
|
|
checkErr(err)
|
|
|
|
fmt.Fprintln(os.Stdout, rendered)
|
2023-10-15 07:43:28 -04:00
|
|
|
} else {
|
|
|
|
checkErr(tpl.Execute(os.Stdout, values))
|
|
|
|
}
|
2023-10-12 16:35:46 -04:00
|
|
|
}
|