mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-18 01:31:36 +02:00
fix: render template expressions in inline values passed to sub-helmfiles
This fixes an issue where template expressions like {{ env "VAR" }},
{{ readFile "file" }}, or {{ fetchSecretValue "ref+..." }} in inline
values passed to sub-helmfiles were not being rendered when the parent
helmfile used a .yaml extension instead of .yaml.gotmpl.
The fix adds template rendering for inline map values in
LoadEnvironmentValues, ensuring that template expressions are
properly evaluated in the context of the parent helmfile's directory.
Fixes #2290
Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
This commit is contained in:
co-authored by
yxxhero
parent
3567c87efd
commit
470ce4c6fc
+101
-1
@@ -80,7 +80,26 @@ func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *str
|
||||
}
|
||||
}
|
||||
case map[any]any, map[string]any:
|
||||
maps = append(maps, strOrMap)
|
||||
// Render template expressions in inline map values
|
||||
// This allows users to use template functions like {{ env "VAR" }} or {{ readFile "file" }}
|
||||
// in inline values passed to sub-helmfiles
|
||||
var env environment.Environment
|
||||
if ctxEnv == nil {
|
||||
env = *environment.New(envName)
|
||||
} else {
|
||||
env = *ctxEnv
|
||||
}
|
||||
tmplData := NewEnvironmentTemplateData(env, "", env.Values)
|
||||
r := tmpl.NewTextRenderer(ld.fs, ld.storage.basePath, tmplData)
|
||||
|
||||
// Change working directory to basePath for template rendering
|
||||
// This is needed because some template functions like fetchSecretValue
|
||||
// resolve relative paths based on the current working directory
|
||||
renderedMap, err := ld.renderInBasePath(strOrMap, r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render inline values: %v", err)
|
||||
}
|
||||
maps = append(maps, renderedMap)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", strOrMap, strOrMap)
|
||||
}
|
||||
@@ -105,6 +124,87 @@ func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *str
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// renderInBasePath temporarily changes the working directory to basePath before rendering
|
||||
// template expressions. This is needed because some template functions like fetchSecretValue
|
||||
// resolve relative paths based on the current working directory.
|
||||
func (ld *EnvironmentValuesLoader) renderInBasePath(value any, r tmpl.TextRenderer) (any, error) {
|
||||
// If filesystem doesn't support Getwd/Chdir (e.g., in tests), just render without changing directory
|
||||
if ld.fs.Getwd == nil || ld.fs.Chdir == nil {
|
||||
return ld.renderTemplateExpressions(value, r)
|
||||
}
|
||||
|
||||
// Save current working directory using the filesystem abstraction
|
||||
cwd, err := ld.fs.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current working directory: %v", err)
|
||||
}
|
||||
|
||||
// Change to basePath for rendering using the filesystem abstraction
|
||||
if err := ld.fs.Chdir(ld.storage.basePath); err != nil {
|
||||
return nil, fmt.Errorf("failed to change to base directory %q: %v", ld.storage.basePath, err)
|
||||
}
|
||||
|
||||
// Ensure we change back to the original directory
|
||||
defer func() {
|
||||
if err := ld.fs.Chdir(cwd); err != nil {
|
||||
ld.logger.Warnf("failed to change back to original directory %q: %v", cwd, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return ld.renderTemplateExpressions(value, r)
|
||||
}
|
||||
|
||||
// renderTemplateExpressions recursively renders template expressions in map values.
|
||||
// This allows users to use template functions like {{ env "VAR" }}, {{ readFile "file" }},
|
||||
// {{ fetchSecretValue "ref+..." }}, etc. in inline values passed to sub-helmfiles.
|
||||
func (ld *EnvironmentValuesLoader) renderTemplateExpressions(value any, r tmpl.TextRenderer) (any, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
// Check if the string contains template expressions (has {{ and }})
|
||||
if strings.Contains(v, "{{") && strings.Contains(v, "}}") {
|
||||
rendered, err := r.RenderTemplateText(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render template expression %q: %v", v, err)
|
||||
}
|
||||
return rendered, nil
|
||||
}
|
||||
return v, nil
|
||||
case map[string]any:
|
||||
result := make(map[string]any, len(v))
|
||||
for key, val := range v {
|
||||
renderedVal, err := ld.renderTemplateExpressions(val, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[key] = renderedVal
|
||||
}
|
||||
return result, nil
|
||||
case map[any]any:
|
||||
result := make(map[any]any, len(v))
|
||||
for key, val := range v {
|
||||
renderedVal, err := ld.renderTemplateExpressions(val, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[key] = renderedVal
|
||||
}
|
||||
return result, nil
|
||||
case []any:
|
||||
result := make([]any, len(v))
|
||||
for i, item := range v {
|
||||
renderedItem, err := ld.renderTemplateExpressions(item, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[i] = renderedItem
|
||||
}
|
||||
return result, nil
|
||||
default:
|
||||
// For other types (int, bool, etc.), return as-is
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func mapMerge(dest map[string]any, maps []any) (map[string]any, error) {
|
||||
for _, m := range maps {
|
||||
// All the nested map key should be string. Otherwise we get strange errors due to that
|
||||
|
||||
@@ -247,3 +247,73 @@ func TestEnvValsLoad_EnvironmentValues(t *testing.T) {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnvValsLoad_InlineMapWithTemplateExpressions tests that template expressions
|
||||
// in inline map values are rendered correctly.
|
||||
// This is a regression test for https://github.com/helmfile/helmfile/issues/2290
|
||||
func TestEnvValsLoad_InlineMapWithTemplateExpressions(t *testing.T) {
|
||||
l := newLoader()
|
||||
|
||||
// Set an environment variable for testing
|
||||
t.Setenv("TEST_VAR", "test-value")
|
||||
|
||||
// Test inline map with template expression
|
||||
actual, err := l.LoadEnvironmentValues(nil, []any{
|
||||
map[string]any{
|
||||
"simple_key": "simple-value",
|
||||
"template_key": `{{ env "TEST_VAR" }}`,
|
||||
"nested": map[string]any{
|
||||
"nested_template": `{{ env "TEST_VAR" }}-nested`,
|
||||
},
|
||||
},
|
||||
}, nil, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := map[string]any{
|
||||
"simple_key": "simple-value",
|
||||
"template_key": "test-value",
|
||||
"nested": map[string]any{
|
||||
"nested_template": "test-value-nested",
|
||||
},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnvValsLoad_InlineMapWithNoTemplateExpressions tests that inline map values
|
||||
// without template expressions are passed through unchanged.
|
||||
func TestEnvValsLoad_InlineMapWithNoTemplateExpressions(t *testing.T) {
|
||||
l := newLoader()
|
||||
|
||||
// Test inline map without template expressions
|
||||
actual, err := l.LoadEnvironmentValues(nil, []any{
|
||||
map[string]any{
|
||||
"key1": "value1",
|
||||
"key2": true,
|
||||
"key3": 123,
|
||||
"nested": map[string]any{
|
||||
"nested_key": "nested_value",
|
||||
},
|
||||
},
|
||||
}, nil, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := map[string]any{
|
||||
"key1": "value1",
|
||||
"key2": true,
|
||||
"key3": 123,
|
||||
"nested": map[string]any{
|
||||
"nested_key": "nested_value",
|
||||
},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user