Add parameter to render helmfile as go template without .gotmpl extension (#2312)

* Add parameter to render helmfile as go template without gotmpl extension

Signed-off-by: Ronaldo <ronaldo.ur@gmail.com>

* Update pkg/envvar/const.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: Ronaldo <ronaldo.ur@gmail.com>
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Ronaldo Umana 2025-12-09 00:41:47 -05:00 committed by GitHub
parent 7a9175b7c4
commit f3b19fd81e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 153 additions and 1 deletions

View File

@ -586,6 +586,7 @@ Helmfile uses some OS environment variables to override default behaviour:
* `HELMFILE_CACHE_HOME` - specify directory to store cached files for remote operations
* `HELMFILE_FILE_PATH` - specify the path to the helmfile.yaml file
* `HELMFILE_INTERACTIVE` - enable interactive mode, expecting `true` lower case. The same as `--interactive` CLI flag
* `HELMFILE_RENDER_YAML` - force helmfile.yaml to be rendered as a Go template regardless of file extension, expecting `true` lower case. Useful for migrating from v0 to v1 without renaming files to `.gotmpl`
## CLI Reference

View File

@ -4455,3 +4455,149 @@ func TestGetArgs(t *testing.T) {
require.Equalf(t, test.expected, strings.Join(receivedArgs, " "), "expected args %s, received args %s", test.expected, strings.Join(receivedArgs, " "))
}
}
func TestRenderYamlEnvVar(t *testing.T) {
testCases := []struct {
name string
envValue string
filename string
content string
shouldRender bool
expectErr bool
expectedOutput string
}{
{
name: "default behavior - helmfile.yaml without .gotmpl is NOT rendered",
envValue: "",
filename: "helmfile.yaml",
content: "releases:\n- name: {{ .Environment.Name }}-app\n chart: test/chart\n",
shouldRender: false,
expectErr: false,
expectedOutput: "",
},
{
name: "default behavior - helmfile.yaml.gotmpl IS rendered",
envValue: "",
filename: "helmfile.yaml.gotmpl",
content: "releases:\n- name: {{ .Environment.Name }}-app\n chart: test/chart\n",
shouldRender: true,
expectErr: false,
expectedOutput: "default-app",
},
{
name: "HELMFILE_RENDER_YAML=true - helmfile.yaml IS rendered",
envValue: "true",
filename: "helmfile.yaml",
content: "releases:\n- name: {{ .Environment.Name }}-app\n chart: test/chart\n",
shouldRender: true,
expectErr: false,
expectedOutput: "default-app",
},
{
name: "HELMFILE_RENDER_YAML=true - helmfile.yaml.gotmpl IS rendered",
envValue: "true",
filename: "helmfile.yaml.gotmpl",
content: "releases:\n- name: {{ .Environment.Name }}-app\n chart: test/chart\n",
shouldRender: true,
expectErr: false,
expectedOutput: "default-app",
},
{
name: "HELMFILE_RENDER_YAML=false - helmfile.yaml is NOT rendered",
envValue: "false",
filename: "helmfile.yaml",
content: "releases:\n- name: {{ .Environment.Name }}-app\n chart: test/chart\n",
shouldRender: false,
expectErr: false,
expectedOutput: "",
},
{
name: "HELMFILE_RENDER_YAML=false - helmfile.yaml.gotmpl IS rendered (extension takes precedence)",
envValue: "false",
filename: "helmfile.yaml.gotmpl",
content: "releases:\n- name: {{ .Environment.Name }}-app\n chart: test/chart\n",
shouldRender: true,
expectErr: false,
expectedOutput: "default-app",
},
{
name: "HELMFILE_RENDER_YAML=TRUE (uppercase) - should NOT render (strict comparison)",
envValue: "TRUE",
filename: "helmfile.yaml",
content: "releases:\n- name: {{ .Environment.Name }}-app\n chart: test/chart\n",
shouldRender: false,
expectErr: false,
expectedOutput: "",
},
{
name: "HELMFILE_RENDER_YAML=1 - should NOT render (strict comparison)",
envValue: "1",
filename: "helmfile.yaml",
content: "releases:\n- name: {{ .Environment.Name }}-app\n chart: test/chart\n",
shouldRender: false,
expectErr: false,
expectedOutput: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.envValue != "" {
t.Setenv(envvar.RenderYaml, tc.envValue)
}
files := map[string]string{
fmt.Sprintf("/path/to/%s", tc.filename): tc.content,
}
app := &App{
OverrideHelmBinary: DefaultHelmBinary,
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Logger: newAppTestLogger(),
Namespace: "",
Env: "default",
FileOrDir: tc.filename,
}
expectNoCallsToHelm(app)
app = appWithFs(app, files)
err := app.ForEachState(
func(run *Run) (bool, []error) {
if tc.shouldRender {
// If rendering is expected, check that the template was rendered
require.NotNil(t, run.state)
require.NotEmpty(t, run.state.Releases)
// The rendered name should be "default-app" not "{{ .Environment.Name }}-app"
actualName := run.state.Releases[0].Name
require.Equal(t, tc.expectedOutput, actualName, "expected release name to be rendered as %s, got %s", tc.expectedOutput, actualName)
} else {
// If rendering is NOT expected, check that the template was NOT rendered
// In this case, the YAML parser will likely fail or the template syntax will remain
// We just verify that if there are releases, they contain the unrendered template
if run.state != nil && len(run.state.Releases) > 0 {
actualName := run.state.Releases[0].Name
require.Contains(t, actualName, "{{", "expected template syntax to remain unrendered, got %s", actualName)
}
}
return false, nil
},
false,
SetFilter(true),
)
if tc.expectErr {
require.Error(t, err)
} else {
if err != nil && !tc.shouldRender {
// It's OK if there's an error when we don't expect rendering
// because the template syntax might cause YAML parsing issues
t.Logf("Expected error when not rendering template: %v", err)
} else if tc.shouldRender {
require.NoError(t, err, "unexpected error: %v", err)
}
}
})
}
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
@ -12,6 +13,7 @@ import (
"go.uber.org/zap"
"github.com/helmfile/helmfile/pkg/environment"
"github.com/helmfile/helmfile/pkg/envvar"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/policy"
@ -216,7 +218,9 @@ func (ld *desiredStateLoader) load(env, overrodeEnv *environment.Environment, ba
var rawContent []byte
if filepath.Ext(filename) == ".gotmpl" {
shouldRender := filepath.Ext(filename) == ".gotmpl" || os.Getenv(envvar.RenderYaml) == "true"
if shouldRender {
var yamlBuf *bytes.Buffer
var err error

View File

@ -15,6 +15,7 @@ const (
GoYamlV3 = "HELMFILE_GO_YAML_V3"
CacheHome = "HELMFILE_CACHE_HOME"
Interactive = "HELMFILE_INTERACTIVE"
RenderYaml = "HELMFILE_RENDER_YAML" // force helmfile.yaml to be rendered as template regardless of extension, expecting "true" lower case
// AWSSDKLogLevel controls AWS SDK logging level
// Valid values: "off" (default), "minimal", "standard", "verbose", or custom (e.g., "request,response")