Repository: helm/helm (go)

Issue:
Add support for custom template funcs when using Helm as a library
Current Behavior:

When using Helm as a library, users have no mechanism to inject their own custom template functions into the templating engine. This limitation confines users to only the default built-in functions provided by Helm, which may not cover all advanced or specialized use cases.

Desired Behavior:

The opportunity to inject custom template function via an action config.



Pull request:
feat(pkg/engine): add support for custom template funcs
This PR extends the Helm templating engine and action configuration to allow users to inject custom template functions when using Helm as a library. It adds a new field to both the action configuration and the engine structures so that custom functions can be provided via an action config and merged with Helm’s default function set.

How to Use:

Users can configure their custom template functions in the action configuration as shown below:
```
conf := &action.Configuration{
	Capabilities: chartutil.DefaultCapabilities,
	CustomTemplateFuncs: template.FuncMap{
		"exclaim": func(input string) string {
			return input + "!!!"
		},
	},
}
```

With this configuration, the Helm templating engine will have access to the custom exclaim function during rendering.

**What this PR does / why we need it**:
The problem is described in the issue.

Closes #30733

There is an old [PR](https://github.com/helm/helm/pull/12135) about this.
And an [old issue](https://github.com/helm/helm/issues/12138)

It can also help to solve [this](https://github.com/helm/community/issues/340)

**Special notes for your reviewer**:

**If applicable**:
- [ ] this PR contains user facing changes (the `docs needed` label should be applied if so)
- [x] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility


Patch:
diff --git a/pkg/action/action.go b/pkg/action/action.go
index 937b42537..e91054a28 100644
--- a/pkg/action/action.go
+++ b/pkg/action/action.go
@@ -25,6 +25,7 @@ import (
 	"path"
 	"path/filepath"
 	"strings"
+	"text/template"
 
 	"github.com/pkg/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -80,6 +81,9 @@ type Configuration struct {
 	// Capabilities describes the capabilities of the Kubernetes cluster.
 	Capabilities *chartutil.Capabilities
 
+	// CustomTemplateFuncs is defined by users to provide custom template funcs
+	CustomTemplateFuncs template.FuncMap
+
 	// HookOutputFunc called with container name and returns and expects writer that will receive the log output.
 	HookOutputFunc func(namespace, pod, container string) io.Writer
 }
@@ -118,10 +122,14 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
 		}
 		e := engine.New(restConfig)
 		e.EnableDNS = enableDNS
+		e.CustomTemplateFuncs = cfg.CustomTemplateFuncs
+
 		files, err2 = e.Render(ch, values)
 	} else {
 		var e engine.Engine
 		e.EnableDNS = enableDNS
+		e.CustomTemplateFuncs = cfg.CustomTemplateFuncs
+
 		files, err2 = e.Render(ch, values)
 	}
 
diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go
index 7235b026a..0b0933def 100644
--- a/pkg/engine/engine.go
+++ b/pkg/engine/engine.go
@@ -44,6 +44,8 @@ type Engine struct {
 	clientProvider *ClientProvider
 	// EnableDNS tells the engine to allow DNS lookups when rendering templates
 	EnableDNS bool
+	// CustomTemplateFuncs is defined by users to provide custom template funcs
+	CustomTemplateFuncs template.FuncMap
 }
 
 // New creates a new instance of Engine using the passed in rest config.
@@ -244,6 +246,11 @@ func (e Engine) initFunMap(t *template.Template) {
 		}
 	}
 
+	// Set custom template funcs
+	for k, v := range e.CustomTemplateFuncs {
+		funcMap[k] = v
+	}
+
 	t.Funcs(funcMap)
 }
 


Test patch:
diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go
index a54e99cad..68e0158fa 100644
--- a/pkg/engine/engine_test.go
+++ b/pkg/engine/engine_test.go
@@ -1300,3 +1300,63 @@ func TestRenderTplMissingKeyString(t *testing.T) {
 		t.Fatal(err)
 	}
 }
+
+func TestRenderCustomTemplateFuncs(t *testing.T) {
+	// Create a chart with two templates that use custom functions
+	c := &chart.Chart{
+		Metadata: &chart.Metadata{Name: "CustomFunc"},
+		Templates: []*chart.File{
+			{
+				Name: "templates/manifest",
+				Data: []byte(`{{exclaim .Values.message}}`),
+			},
+			{
+				Name: "templates/override",
+				Data: []byte(`{{ upper .Values.message }}`),
+			},
+		},
+	}
+	v := chartutil.Values{
+		"Values": chartutil.Values{
+			"message": "hello",
+		},
+		"Chart": c.Metadata,
+		"Release": chartutil.Values{
+			"Name": "TestRelease",
+		},
+	}
+
+	// Define a custom template function "exclaim" that appends "!!!" to a string and override "upper" function
+	customFuncs := template.FuncMap{
+		"exclaim": func(input string) string {
+			return input + "!!!"
+		},
+		"upper": func(s string) string {
+			return "custom:" + s
+		},
+	}
+
+	// Create an engine instance and set the CustomTemplateFuncs.
+	e := new(Engine)
+	e.CustomTemplateFuncs = customFuncs
+
+	// Render the chart.
+	out, err := e.Render(c, v)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Expected output should be "hello!!!".
+	expected := "hello!!!"
+	key := "CustomFunc/templates/manifest"
+	if rendered, ok := out[key]; !ok || rendered != expected {
+		t.Errorf("Expected %q, got %q", expected, rendered)
+	}
+
+	// Verify that the rendered template used the custom "upper" function.
+	expected = "custom:hello"
+	key = "CustomFunc/templates/override"
+	if rendered, ok := out[key]; !ok || rendered != expected {
+		t.Errorf("Expected %q, got %q", expected, rendered)
+	}
+}