Skip to content

Commit 910b523

Browse files
committed
feat(template): optimize and deprecate sprig functions
Pre-compute Sprig template functions during package init. Avoids repeated map allocation and string concatenation for Sprig functions on every template execution, addressing performance overhead reported in #2031. In addition, use HermeticTxtFuncMap which only imports repeatable functions. This is primarily to avoid potentially malicious input. Signed-off-by: Ville Vesilehto <[email protected]>
1 parent 9328c5f commit 910b523

File tree

2 files changed

+67
-4
lines changed

2 files changed

+67
-4
lines changed

template/template.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ var (
3434
ErrMissingReaderFunction = errors.New("template: missing a reader function")
3535
)
3636

37+
var (
38+
sprigFuncMap template.FuncMap
39+
)
40+
41+
func init() {
42+
sprigFuncMap = make(template.FuncMap, len(sprig.HermeticTxtFuncMap()))
43+
// Add the Sprig functions to the funcmap
44+
for k, v := range sprig.HermeticTxtFuncMap() {
45+
target := "sprig_" + k
46+
sprigFuncMap[target] = v
47+
}
48+
}
49+
3750
// Template is the internal representation of an individual template to process.
3851
// The template retains the relationship between its contents and is
3952
// responsible for it's own execution.
@@ -436,10 +449,9 @@ func funcMap(i *funcMapInput) template.FuncMap {
436449
"spew_sprintf": spewSprintf,
437450
}
438451

439-
// Add the Sprig functions to the funcmap
440-
for k, v := range sprig.TxtFuncMap() {
441-
target := "sprig_" + k
442-
r[target] = v
452+
// Add the pre-computed Sprig functions to the funcmap
453+
for k, v := range sprigFuncMap {
454+
r[k] = v
443455
}
444456

445457
// Add external functions

template/template_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2745,3 +2745,54 @@ func TestTemplate_ExtFuncMap(t *testing.T) {
27452745
})
27462746
}
27472747
}
2748+
2749+
// TestTemplate_HermeticSprigFunctions tests that hermetic Sprig functions
2750+
// are available, but non-hermetic ones are not.
2751+
func TestTemplate_HermeticSprigFunctions(t *testing.T) {
2752+
cases := []struct {
2753+
name string
2754+
contents string
2755+
expected string
2756+
wantErr bool
2757+
}{
2758+
{
2759+
"hermetic function",
2760+
`{{ sprig_upper "hello" }}`,
2761+
"HELLO",
2762+
false,
2763+
},
2764+
{
2765+
"generic function not available",
2766+
`{{ sprig_repeat "hello" 3 }}`,
2767+
"",
2768+
true, // Should error because env function shouldn't be available in hermetic map
2769+
},
2770+
}
2771+
2772+
for i, tc := range cases {
2773+
t.Run(fmt.Sprintf("%d_%s", i, tc.name), func(t *testing.T) {
2774+
tpl, err := NewTemplate(&NewTemplateInput{
2775+
Contents: tc.contents,
2776+
})
2777+
if err != nil {
2778+
t.Fatal(err)
2779+
}
2780+
2781+
result, err := tpl.Execute(nil)
2782+
if tc.wantErr {
2783+
if err == nil {
2784+
t.Fatalf("expected error for %s, but got nil", tc.name)
2785+
}
2786+
return
2787+
}
2788+
2789+
if err != nil {
2790+
t.Fatal(err)
2791+
}
2792+
2793+
if !bytes.Equal(result.Output, []byte(tc.expected)) {
2794+
t.Errorf("\nexpected: %q\nactual: %q", tc.expected, result.Output)
2795+
}
2796+
})
2797+
}
2798+
}

0 commit comments

Comments
 (0)