mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-09 05:30:49 +02:00
feat: include func support (#1187)
* feat: include func support Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
@@ -307,7 +307,13 @@ func TestSetValueAtPath_TwoComponents(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTpl(t *testing.T) {
|
||||
ctx := &Context{basePath: "."}
|
||||
ctx := &Context{
|
||||
basePath: ".",
|
||||
fs: &filesystem.FileSystem{
|
||||
Glob: func(s string) ([]string, error) {
|
||||
return nil, nil
|
||||
}},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
|
||||
@@ -2,28 +2,45 @@ package tmpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const recursionMaxNums = 1000
|
||||
|
||||
// CreateFuncMap creates a template.FuncMap for the Context struct.
|
||||
// It combines the functions from sprig.TxtFuncMap() with the functions
|
||||
// defined in the Context's createFuncMap() method.
|
||||
// It also adds aliases for certain functions based on the aliases map.
|
||||
// The resulting FuncMap is returned.
|
||||
func (c *Context) CreateFuncMap() template.FuncMap {
|
||||
// function aliases
|
||||
aliased := template.FuncMap{}
|
||||
|
||||
// map of function aliases
|
||||
aliases := map[string]string{
|
||||
"get": "sprigGet",
|
||||
}
|
||||
|
||||
// get the default sprig functions
|
||||
funcMap := sprig.TxtFuncMap()
|
||||
|
||||
// add aliases to the aliased FuncMap
|
||||
for orig, alias := range aliases {
|
||||
aliased[alias] = funcMap[orig]
|
||||
}
|
||||
|
||||
// add functions from the Context's createFuncMap() method to the funcMap
|
||||
for name, f := range c.createFuncMap() {
|
||||
funcMap[name] = f
|
||||
}
|
||||
|
||||
// add aliased functions to the funcMap
|
||||
for name, f := range aliased {
|
||||
funcMap[name] = f
|
||||
}
|
||||
@@ -31,22 +48,90 @@ func (c *Context) CreateFuncMap() template.FuncMap {
|
||||
return funcMap
|
||||
}
|
||||
|
||||
func (c *Context) newTemplate() *template.Template {
|
||||
type tplInfo struct {
|
||||
name string
|
||||
content string
|
||||
}
|
||||
|
||||
// helperTPLs returns the contents of all files with names starting with "_" and ending with ".tpl"
|
||||
// in the root directory of the Context. It reads each file and appends its content to the contents slice.
|
||||
// If any error occurs during the file reading or globbing process, it returns an error.
|
||||
func (c *Context) helperTPLs() ([]tplInfo, error) {
|
||||
tplInfos := []tplInfo{}
|
||||
files, err := c.fs.Glob(filepath.Join(c.basePath, "_*.tpl"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to glob helper templates: %v", err)
|
||||
}
|
||||
for _, file := range files {
|
||||
content, err := c.fs.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read helper template %s: %v", file, err)
|
||||
}
|
||||
tplInfos = append(tplInfos, tplInfo{name: file, content: string(content)})
|
||||
}
|
||||
return tplInfos, nil
|
||||
}
|
||||
|
||||
// newTemplate creates a new template based on the context.
|
||||
// It initializes the template with the specified options and parses the helper templates.
|
||||
// It also adds the 'include' function to the template's function map.
|
||||
// The 'include' function allows including and rendering nested templates.
|
||||
// The function returns the created template or an error if any occurred.
|
||||
func (c *Context) newTemplate() (*template.Template, error) {
|
||||
funcMap := c.CreateFuncMap()
|
||||
|
||||
tmpl := template.New("stringTemplate").Funcs(funcMap)
|
||||
tmpl := template.New("stringTemplate")
|
||||
if c.preRender {
|
||||
tmpl = tmpl.Option("missingkey=zero")
|
||||
} else {
|
||||
tmpl = tmpl.Option("missingkey=error")
|
||||
}
|
||||
return tmpl
|
||||
|
||||
tpls, err := c.helperTPLs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, tpl := range tpls {
|
||||
tmpl, err = tmpl.Parse(tpl.content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse helper template %s: %v", tpl.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
includedNames := make(map[string]int)
|
||||
|
||||
// Add the 'include' function here so we can close over t.
|
||||
funcMap["include"] = func(name string, data interface{}) (string, error) {
|
||||
var buf strings.Builder
|
||||
if v, ok := includedNames[name]; ok {
|
||||
if v > recursionMaxNums {
|
||||
return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name)
|
||||
}
|
||||
includedNames[name]++
|
||||
} else {
|
||||
includedNames[name] = 1
|
||||
}
|
||||
err := tmpl.ExecuteTemplate(&buf, name, data)
|
||||
includedNames[name]--
|
||||
return buf.String(), err
|
||||
}
|
||||
tmpl.Funcs(funcMap)
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
// RenderTemplateToBuffer renders the provided template string with the given data and returns the result as a *bytes.Buffer.
|
||||
// The template string is parsed and executed using the Context's newTemplate method.
|
||||
// If an error occurs during parsing or execution, it is returned along with the partially rendered template.
|
||||
// The data parameter is optional and can be used to provide additional data for template rendering.
|
||||
// If no data is provided, the template is rendered with an empty data context.
|
||||
func (c *Context) RenderTemplateToBuffer(s string, data ...any) (*bytes.Buffer, error) {
|
||||
var t, parseErr = c.newTemplate().Parse(s)
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
t, err := c.newTemplate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t, err = t.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tplString bytes.Buffer
|
||||
|
||||
+120
-27
@@ -16,12 +16,16 @@ func TestRenderTemplate_Values(t *testing.T) {
|
||||
bar: FOO_BAR
|
||||
`
|
||||
expectedFilename := "values.yaml"
|
||||
ctx := &Context{fs: &ffs.FileSystem{ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(valuesYamlContent), nil
|
||||
}}}
|
||||
ctx := &Context{fs: &ffs.FileSystem{
|
||||
Glob: func(s string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(valuesYamlContent), nil
|
||||
}}}
|
||||
buf, err := ctx.RenderTemplateToBuffer(`{{ readFile "values.yaml" | fromYaml | setValueAtPath "foo.bar" "FOO_BAR" | toYaml }}`)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
@@ -45,12 +49,16 @@ func TestRenderTemplate_WithData(t *testing.T) {
|
||||
"bar": "FOO_BAR",
|
||||
},
|
||||
}
|
||||
ctx := &Context{fs: &ffs.FileSystem{ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(valuesYamlContent), nil
|
||||
}}}
|
||||
ctx := &Context{fs: &ffs.FileSystem{
|
||||
Glob: func(s string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(valuesYamlContent), nil
|
||||
}}}
|
||||
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
@@ -70,12 +78,16 @@ func TestRenderTemplate_AccessingMissingKeyWithGetOrNil(t *testing.T) {
|
||||
`
|
||||
expectedFilename := "values.yaml"
|
||||
data := map[string]any{}
|
||||
ctx := &Context{fs: &ffs.FileSystem{ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(valuesYamlContent), nil
|
||||
}}}
|
||||
ctx := &Context{fs: &ffs.FileSystem{
|
||||
Glob: func(s string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(valuesYamlContent), nil
|
||||
}}}
|
||||
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
@@ -95,12 +107,16 @@ func TestRenderTemplate_Defaulting(t *testing.T) {
|
||||
`
|
||||
expectedFilename := "values.yaml"
|
||||
data := map[string]any{}
|
||||
ctx := &Context{fs: &ffs.FileSystem{ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(valuesYamlContent), nil
|
||||
}}}
|
||||
ctx := &Context{fs: &ffs.FileSystem{
|
||||
Glob: func(s string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(valuesYamlContent), nil
|
||||
}}}
|
||||
buf, err := ctx.RenderTemplateToBuffer(valuesYamlContent, data)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
@@ -112,9 +128,13 @@ func TestRenderTemplate_Defaulting(t *testing.T) {
|
||||
}
|
||||
|
||||
func renderTemplateToString(s string, data ...any) (string, error) {
|
||||
ctx := &Context{fs: &ffs.FileSystem{ReadFile: func(filename string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename)
|
||||
}}}
|
||||
ctx := &Context{fs: &ffs.FileSystem{
|
||||
Glob: func(s string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
ReadFile: func(filename string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename)
|
||||
}}}
|
||||
tplString, err := ctx.RenderTemplateToBuffer(s, data...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -326,3 +346,76 @@ func TestRenderTemplate_Required(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestContext_helperTPLs(t *testing.T) {
|
||||
c := &Context{
|
||||
fs: &ffs.FileSystem{
|
||||
Glob: func(s string) ([]string, error) {
|
||||
return []string{
|
||||
"/helmfiletmpl/_template1.tpl",
|
||||
"/helmfiletmpl/_template2.tpl",
|
||||
}, nil
|
||||
},
|
||||
ReadFile: func(filename string) ([]byte, error) {
|
||||
switch filename {
|
||||
case "/helmfiletmpl/_template1.tpl":
|
||||
return []byte("Template 1 content"), nil
|
||||
case "/helmfiletmpl/_template2.tpl":
|
||||
return []byte("Template 2 content"), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected filename: %s", filename)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
want := []tplInfo{
|
||||
{
|
||||
name: "/helmfiletmpl/_template1.tpl",
|
||||
content: "Template 1 content",
|
||||
},
|
||||
{
|
||||
name: "/helmfiletmpl/_template2.tpl",
|
||||
content: "Template 2 content",
|
||||
},
|
||||
}
|
||||
got, err := c.helperTPLs()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("unexpected result: got=%v, want=%v", got, want)
|
||||
}
|
||||
}
|
||||
func TestContext_RenderTemplateToBuffer(t *testing.T) {
|
||||
c := &Context{
|
||||
basePath: "/helmfile",
|
||||
fs: &ffs.FileSystem{
|
||||
Glob: func(s string) ([]string, error) {
|
||||
return []string{
|
||||
"/helmfile/_template1.tpl",
|
||||
}, nil
|
||||
},
|
||||
ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename == "/helmfile/_template1.tpl" {
|
||||
return []byte("{{- define \"name\" -}}\n{{ .Name }}\n{{- end }}"), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected filename: %s", filename)
|
||||
},
|
||||
},
|
||||
}
|
||||
s := "Hello, {{ include \"name\" . }}!"
|
||||
data := map[string]interface{}{
|
||||
"Name": "Alice",
|
||||
}
|
||||
expected := "Hello, Alice!"
|
||||
|
||||
buf, err := c.RenderTemplateToBuffer(s, data)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
actual := buf.String()
|
||||
if actual != expected {
|
||||
t.Errorf("unexpected result: expected=%s, actual=%s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,15 +24,19 @@ func TestRenderToBytes_Gotmpl(t *testing.T) {
|
||||
`
|
||||
dataFile := "data.txt"
|
||||
valuesTmplFile := "values.yaml.gotmpl"
|
||||
r := NewFileRenderer(&filesystem.FileSystem{ReadFile: func(filename string) ([]byte, error) {
|
||||
switch filename {
|
||||
case valuesTmplFile:
|
||||
return []byte(valuesYamlTmplContent), nil
|
||||
case dataFile:
|
||||
return []byte(dataFileContent), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v or %v, actual=%s", dataFile, valuesTmplFile, filename)
|
||||
}}, "", emptyEnvTmplData)
|
||||
r := NewFileRenderer(&filesystem.FileSystem{
|
||||
Glob: func(pattern string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
ReadFile: func(filename string) ([]byte, error) {
|
||||
switch filename {
|
||||
case valuesTmplFile:
|
||||
return []byte(valuesYamlTmplContent), nil
|
||||
case dataFile:
|
||||
return []byte(dataFileContent), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v or %v, actual=%s", dataFile, valuesTmplFile, filename)
|
||||
}}, "", emptyEnvTmplData)
|
||||
buf, err := r.RenderToBytes(valuesTmplFile)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
@@ -51,12 +55,16 @@ func TestRenderToBytes_Yaml(t *testing.T) {
|
||||
bar: '{{ readFile "data.txt" }}'
|
||||
`
|
||||
valuesFile := "values.yaml"
|
||||
r := NewFileRenderer(&filesystem.FileSystem{ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename == valuesFile {
|
||||
return []byte(valuesYamlContent), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", valuesFile, filename)
|
||||
}}, "", emptyEnvTmplData)
|
||||
r := NewFileRenderer(&filesystem.FileSystem{
|
||||
Glob: func(pattern string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
ReadFile: func(filename string) ([]byte, error) {
|
||||
if filename == valuesFile {
|
||||
return []byte(valuesYamlContent), nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", valuesFile, filename)
|
||||
}}, "", emptyEnvTmplData)
|
||||
buf, err := r.RenderToBytes(valuesFile)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
|
||||
@@ -2,10 +2,18 @@ package tmpl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/filesystem"
|
||||
)
|
||||
|
||||
func TestMergeOverwrite(t *testing.T) {
|
||||
ctx := &Context{}
|
||||
ctx := &Context{
|
||||
fs: &filesystem.FileSystem{
|
||||
Glob: func(pattern string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
buf, err := ctx.RenderTemplateToBuffer(`
|
||||
{{- $v1 := dict "bool" true "int" 2 "str" "v1" "str2" "v1" -}}
|
||||
{{- $v2 := dict "bool" false "int" 0 "str" "v2" "str2" "" -}}
|
||||
|
||||
Reference in New Issue
Block a user