fix: include release values in .Values for patch template rendering (#2556)

* fix: include release values in .Values for jsonPatches/strategicMergePatches/transformers gotmpl rendering

Before this fix, .Values in patch template files only contained environment
values, not the release's own values. This meant references like
{{ .Values.ingress.enabled }} would fail when ingress.enabled was set in
the release's values: file rather than environment values.

Now patch gotmpl files see .Values as merged(environment values, release values),
matching user expectations that values defined in the release should be
accessible in conditional patches.

Fixes #1904

Signed-off-by: yxxhero <aiopsclub@163.com>

* test: add more tests for resolveReleaseValues, renderValuesFileToBytesWithData, and generateTemporaryReleaseValuesFilesWithData

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/5da5c9d8-7464-4146-84b5-1433ed6193f3

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* test: simplify newTestHelmStateWithFiles by removing empty cleanup func

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/5da5c9d8-7464-4146-84b5-1433ed6193f3

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: remove always-constant basePath param from newTestHelmStateWithFiles to fix unparam lint error

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/b4a669cb-692c-4ca6-a68b-1b04a062b989

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: address c66017c review comments - error messages, defer-in-loop, map normalization, test cleanup

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/44988dd8-1c67-465b-995c-80525a24eb93

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* refactor: extract generateTemporaryReleaseValuesFilesCore to eliminate duplication; fix temp dir leaks in tests

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/b254ddda-aa95-4e2f-8dd9-1ce4c40eedb6

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: remove rendered content from debug log; extract prepareReleaseValuesEntries shared helper

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/321c6ba5-f835-4afd-be5e-ee790bc6b4a5

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: compute mergedReleaseTemplateData lazily in PrepareChartify

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/100b7974-3268-4dc8-be21-12bd82aa2dbb

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: normalize nested YAML keys in resolveReleaseValues via CastKeysToStrings

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/d0129d85-9c7d-4a31-966e-fc0b05b74867

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: use %w for error wrapping in release values resolution

Signed-off-by: yxxhero <aiopsclub@163.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
yxxhero 2026-06-15 16:31:46 +08:00 committed by GitHub
parent 6cf02956c2
commit 38e27ee439
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 657 additions and 49 deletions

View File

@ -7,12 +7,14 @@ import (
"testing"
"dario.cat/mergo"
"github.com/helmfile/vals"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
"github.com/helmfile/helmfile/pkg/environment"
"github.com/helmfile/helmfile/pkg/envvar"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/remote"
"github.com/helmfile/helmfile/pkg/testhelper"
@ -1025,3 +1027,450 @@ func TestMergeEnvironments_PreservesMergeStrategy(t *testing.T) {
})
}
}
func TestMergedReleaseTemplateData_IncludesReleaseValues(t *testing.T) {
logger := zaptest.NewLogger(t).Sugar()
testValsRuntime, err := vals.New(vals.Options{CacheSize: 32})
require.NoError(t, err)
yamlFile := "/example/path/to/helmfile.yaml"
yamlContent := []byte(`environments:
default:
values:
- env.yaml
releases:
- name: myrelease
chart: mychart
values:
- values.yaml
`)
envYamlFile := "/example/path/to/env.yaml"
envYamlContent := []byte(`envKey: envValue`)
valuesFile := "/example/path/to/values.yaml"
valuesContent := []byte(`ingress:
enabled: true
host: example.com`)
testFs := testhelper.NewTestFs(map[string]string{
envYamlFile: string(envYamlContent),
valuesFile: string(valuesContent),
})
testFs.Cwd = "/example/path/to"
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
env := environment.Environment{
Name: "default",
}
state, err := NewCreator(logger, testFs.ToFileSystem(), testValsRuntime, nil, "", "", r, false, "").
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "default", true, true, true, &env, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
release := state.Releases[0]
templateData, err := state.mergedReleaseTemplateData(&release)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ingress, ok := templateData.Values["ingress"]
if !ok {
t.Fatalf("expected .Values to contain 'ingress' key from release values")
}
ingressMap, ok := ingress.(map[string]any)
if !ok {
t.Fatalf("expected ingress to be a map, got %T", ingress)
}
if ingressMap["enabled"] != true {
t.Errorf("expected ingress.enabled to be true, got %v", ingressMap["enabled"])
}
if ingressMap["host"] != "example.com" {
t.Errorf("expected ingress.host to be 'example.com', got %v", ingressMap["host"])
}
if templateData.Values["envKey"] != "envValue" {
t.Errorf("expected envKey to be 'envValue', got %v", templateData.Values["envKey"])
}
}
func TestMergedReleaseTemplateData_ReleaseValuesOverrideEnvValues(t *testing.T) {
logger := zaptest.NewLogger(t).Sugar()
testValsRuntime, err := vals.New(vals.Options{CacheSize: 32})
require.NoError(t, err)
yamlFile := "/example/path/to/helmfile.yaml"
yamlContent := []byte(`environments:
default:
values:
- env.yaml
releases:
- name: myrelease
chart: mychart
values:
- values.yaml
`)
envYamlFile := "/example/path/to/env.yaml"
envYamlContent := []byte(`replicaCount: 1`)
valuesFile := "/example/path/to/values.yaml"
valuesContent := []byte(`replicaCount: 3`)
testFs := testhelper.NewTestFs(map[string]string{
envYamlFile: string(envYamlContent),
valuesFile: string(valuesContent),
})
testFs.Cwd = "/example/path/to"
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
env := environment.Environment{
Name: "default",
}
state, err := NewCreator(logger, testFs.ToFileSystem(), testValsRuntime, nil, "", "", r, false, "").
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "default", true, true, true, &env, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
release := state.Releases[0]
templateData, err := state.mergedReleaseTemplateData(&release)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if templateData.Values["replicaCount"] != 3 {
t.Errorf("expected replicaCount to be 3 (release value overriding env), got %v", templateData.Values["replicaCount"])
}
}
func TestMergedReleaseTemplateData_InlineValues(t *testing.T) {
logger := zaptest.NewLogger(t).Sugar()
testValsRuntime, err := vals.New(vals.Options{CacheSize: 32})
require.NoError(t, err)
yamlFile := "/example/path/to/helmfile.yaml"
yamlContent := []byte(`releases:
- name: myrelease
chart: mychart
values:
- ingress:
enabled: true
host: example.com
`)
testFs := testhelper.NewTestFs(map[string]string{})
testFs.Cwd = "/example/path/to"
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
state, err := NewCreator(logger, testFs.ToFileSystem(), testValsRuntime, nil, "", "", r, false, "").
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "default", true, true, true, nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
release := state.Releases[0]
templateData, err := state.mergedReleaseTemplateData(&release)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ingress, ok := templateData.Values["ingress"]
if !ok {
t.Fatalf("expected .Values to contain 'ingress' key from inline release values")
}
ingressMap, ok := ingress.(map[string]any)
if !ok {
t.Fatalf("expected ingress to be a map, got %T", ingress)
}
if ingressMap["enabled"] != true {
t.Errorf("expected ingress.enabled to be true, got %v", ingressMap["enabled"])
}
}
func newTestHelmStateWithFiles(t *testing.T, files map[string]string) *HelmState {
t.Helper()
const basePath = "/project"
logger := zaptest.NewLogger(t).Sugar()
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
require.NoError(t, err)
testFs := testhelper.NewTestFs(files)
testFs.Cwd = basePath
return &HelmState{
logger: logger,
fs: testFs.ToFileSystem(),
valsRuntime: valsRuntime,
basePath: basePath,
FilePath: basePath + "/helmfile.yaml",
RenderedValues: map[string]any{},
}
}
func TestResolveReleaseValues_Empty(t *testing.T) {
st := newTestHelmStateWithFiles(t, map[string]string{})
release := &ReleaseSpec{Name: "myrelease", Chart: "mychart"}
result, err := st.resolveReleaseValues(release)
require.NoError(t, err)
assert.Empty(t, result)
}
func TestResolveReleaseValues_FromFile(t *testing.T) {
valuesFile := "/project/values.yaml"
valuesContent := `replicaCount: 2
image:
repository: nginx
tag: "1.21"
`
st := newTestHelmStateWithFiles(t, map[string]string{
valuesFile: valuesContent,
})
release := &ReleaseSpec{
Name: "myrelease",
Chart: "mychart",
Values: []any{
"values.yaml",
},
}
result, err := st.resolveReleaseValues(release)
require.NoError(t, err)
assert.Equal(t, 2, result["replicaCount"])
imageMap, ok := result["image"].(map[string]any)
require.True(t, ok, "expected image to be a map")
assert.Equal(t, "nginx", imageMap["repository"])
}
func TestResolveReleaseValues_InlineMap(t *testing.T) {
st := newTestHelmStateWithFiles(t, map[string]string{})
release := &ReleaseSpec{
Name: "myrelease",
Chart: "mychart",
Values: []any{
map[string]any{
"replicaCount": 5,
"service": map[string]any{
"type": "ClusterIP",
"port": 80,
},
},
},
}
result, err := st.resolveReleaseValues(release)
require.NoError(t, err)
assert.Equal(t, 5, result["replicaCount"])
serviceMap, ok := result["service"].(map[string]any)
require.True(t, ok, "expected service to be a map")
assert.Equal(t, "ClusterIP", serviceMap["type"])
}
func TestResolveReleaseValues_MultipleSourcesMerged(t *testing.T) {
baseValuesFile := "/project/base.yaml"
baseValuesContent := `replicaCount: 1
service:
type: ClusterIP
`
overrideValuesFile := "/project/override.yaml"
overrideValuesContent := `replicaCount: 3
ingress:
enabled: true
`
st := newTestHelmStateWithFiles(t, map[string]string{
baseValuesFile: baseValuesContent,
overrideValuesFile: overrideValuesContent,
})
release := &ReleaseSpec{
Name: "myrelease",
Chart: "mychart",
Values: []any{
"base.yaml",
"override.yaml",
},
}
result, err := st.resolveReleaseValues(release)
require.NoError(t, err)
// override.yaml value wins
assert.Equal(t, 3, result["replicaCount"])
// from base.yaml
serviceMap, ok := result["service"].(map[string]any)
require.True(t, ok, "expected service to be a map")
assert.Equal(t, "ClusterIP", serviceMap["type"])
// from override.yaml
ingressMap, ok := result["ingress"].(map[string]any)
require.True(t, ok, "expected ingress to be a map")
assert.Equal(t, true, ingressMap["enabled"])
}
func TestResolveReleaseValues_FileAndInlineMerged(t *testing.T) {
valuesFile := "/project/values.yaml"
valuesContent := `replicaCount: 1
`
st := newTestHelmStateWithFiles(t, map[string]string{
valuesFile: valuesContent,
})
release := &ReleaseSpec{
Name: "myrelease",
Chart: "mychart",
Values: []any{
"values.yaml",
map[string]any{
"extraEnv": "production",
},
},
}
result, err := st.resolveReleaseValues(release)
require.NoError(t, err)
assert.Equal(t, 1, result["replicaCount"])
assert.Equal(t, "production", result["extraEnv"])
}
func TestRenderValuesFileToBytesWithData_PlainYAML(t *testing.T) {
valuesFile := "/project/values.yaml"
valuesContent := `replicaCount: 2
image:
repository: nginx
`
st := newTestHelmStateWithFiles(t, map[string]string{
valuesFile: valuesContent,
})
release := &ReleaseSpec{Name: "myrelease", Chart: "mychart"}
tmplData := st.createReleaseTemplateData(release, map[string]any{})
result, err := st.renderValuesFileToBytesWithData(valuesFile, tmplData)
require.NoError(t, err)
assert.Contains(t, string(result), "replicaCount: 2")
assert.Contains(t, string(result), "repository: nginx")
}
func TestRenderValuesFileToBytesWithData_WithValuesTemplate(t *testing.T) {
valuesFile := "/project/values.yaml.gotmpl"
valuesContent := `replicaCount: {{ .Values.replicaCount }}
enabled: {{ .Values.ingress.enabled }}
`
st := newTestHelmStateWithFiles(t, map[string]string{
valuesFile: valuesContent,
})
release := &ReleaseSpec{Name: "myrelease", Chart: "mychart"}
tmplData := st.createReleaseTemplateData(release, map[string]any{
"replicaCount": 3,
"ingress": map[string]any{
"enabled": true,
},
})
result, err := st.renderValuesFileToBytesWithData(valuesFile, tmplData)
require.NoError(t, err)
assert.Contains(t, string(result), "replicaCount: 3")
assert.Contains(t, string(result), "enabled: true")
}
func TestRenderValuesFileToBytesWithData_WithReleaseTemplate(t *testing.T) {
valuesFile := "/project/values.yaml.gotmpl"
valuesContent := `releaseName: {{ .Release.Name }}
releaseNamespace: {{ .Release.Namespace }}
`
st := newTestHelmStateWithFiles(t, map[string]string{
valuesFile: valuesContent,
})
release := &ReleaseSpec{Name: "myapp", Chart: "mychart", Namespace: "production"}
tmplData := st.createReleaseTemplateData(release, map[string]any{})
result, err := st.renderValuesFileToBytesWithData(valuesFile, tmplData)
require.NoError(t, err)
assert.Contains(t, string(result), "releaseName: myapp")
assert.Contains(t, string(result), "releaseNamespace: production")
}
func TestGenerateTemporaryReleaseValuesFilesWithData_StringPath(t *testing.T) {
t.Setenv(envvar.TempDir, t.TempDir())
patchFile := "/project/patch.yaml.gotmpl"
patchContent := `enabled: {{ .Values.ingress.enabled }}
host: {{ .Values.ingress.host }}
`
st := newTestHelmStateWithFiles(t, map[string]string{
patchFile: patchContent,
})
release := &ReleaseSpec{Name: "myrelease", Chart: "mychart"}
tmplData := st.createReleaseTemplateData(release, map[string]any{
"ingress": map[string]any{
"enabled": true,
"host": "example.com",
},
})
generatedFiles, err := st.generateTemporaryReleaseValuesFilesWithData(
release,
[]any{"patch.yaml.gotmpl"},
func() (releaseTemplateData, error) { return tmplData, nil },
)
require.NoError(t, err)
require.Len(t, generatedFiles, 1)
// The temp files are created on the real OS filesystem via os.Create, so we read them with os.ReadFile
content, err := filesystem.DefaultFileSystem().ReadFile(generatedFiles[0])
require.NoError(t, err)
assert.Contains(t, string(content), "enabled: true")
assert.Contains(t, string(content), "host: example.com")
}
func TestGenerateTemporaryReleaseValuesFilesWithData_InlineMap(t *testing.T) {
t.Setenv(envvar.TempDir, t.TempDir())
st := newTestHelmStateWithFiles(t, map[string]string{})
release := &ReleaseSpec{Name: "myrelease", Chart: "mychart"}
tmplData := st.createReleaseTemplateData(release, map[string]any{})
inlineValues := map[string]any{
"replicaCount": 5,
"service": map[string]any{
"type": "NodePort",
},
}
generatedFiles, err := st.generateTemporaryReleaseValuesFilesWithData(
release,
[]any{inlineValues},
func() (releaseTemplateData, error) { return tmplData, nil },
)
require.NoError(t, err)
require.Len(t, generatedFiles, 1)
// The temp files are created on the real OS filesystem via os.Create, so we read them with os.ReadFile
content, err := filesystem.DefaultFileSystem().ReadFile(generatedFiles[0])
require.NoError(t, err)
assert.Contains(t, string(content), "replicaCount: 5")
assert.Contains(t, string(content), "NodePort")
}
func TestGenerateTemporaryReleaseValuesFilesWithData_UnknownTypeError(t *testing.T) {
st := newTestHelmStateWithFiles(t, map[string]string{})
release := &ReleaseSpec{Name: "myrelease", Chart: "mychart"}
tmplData := st.createReleaseTemplateData(release, map[string]any{})
// Passing an unsupported type (int) should return an error
_, err := st.generateTemporaryReleaseValuesFilesWithData(
release,
[]any{42},
func() (releaseTemplateData, error) { return tmplData, nil },
)
require.Error(t, err)
assert.Contains(t, err.Error(), "unexpected type of value")
}

View File

@ -474,9 +474,28 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
shouldRun = true
}
// patchTemplateData is computed lazily on first use: only when an actual string patch/transformer
// file path is rendered. Inline-map entries don't require template data at all, so we avoid
// unnecessary I/O for releases whose patches are all inline maps or that have no string entries.
var (
cachedPatchTemplateData releaseTemplateData
cachedPatchTemplateDataErr error
cachedPatchTemplateDataSet bool
)
getPatchTemplateData := func() (releaseTemplateData, error) {
if !cachedPatchTemplateDataSet {
cachedPatchTemplateDataSet = true
cachedPatchTemplateData, cachedPatchTemplateDataErr = st.mergedReleaseTemplateData(release)
if cachedPatchTemplateDataErr != nil {
cachedPatchTemplateDataErr = fmt.Errorf("failed to compute merged release values for patch rendering: %w", cachedPatchTemplateDataErr)
}
}
return cachedPatchTemplateData, cachedPatchTemplateDataErr
}
jsonPatches := release.JSONPatches
if len(jsonPatches) > 0 {
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, jsonPatches)
generatedFiles, err := st.generateTemporaryReleaseValuesFilesWithData(release, jsonPatches, getPatchTemplateData)
if err != nil {
return nil, clean, err
}
@ -490,7 +509,7 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
strategicMergePatches := release.StrategicMergePatches
if len(strategicMergePatches) > 0 {
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, strategicMergePatches)
generatedFiles, err := st.generateTemporaryReleaseValuesFilesWithData(release, strategicMergePatches, getPatchTemplateData)
if err != nil {
return nil, clean, err
}
@ -504,7 +523,7 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
transformers := release.Transformers
if len(transformers) > 0 {
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, transformers)
generatedFiles, err := st.generateTemporaryReleaseValuesFilesWithData(release, transformers, getPatchTemplateData)
if err != nil {
return nil, clean, err
}

View File

@ -4150,15 +4150,101 @@ func (st *HelmState) newReleaseTemplateData(release *ReleaseSpec) releaseTemplat
return templateData
}
func (st *HelmState) newReleaseTemplateFuncMap(dir string) template.FuncMap {
r := tmpl.NewFileRenderer(st.fs, dir, nil)
return r.Context.CreateFuncMap()
func (st *HelmState) mergedReleaseTemplateData(release *ReleaseSpec) (releaseTemplateData, error) {
releaseValues, err := st.resolveReleaseValues(release)
if err != nil {
return releaseTemplateData{}, err
}
mergedVals := maputil.MergeMaps(st.Values(), releaseValues)
return st.createReleaseTemplateData(release, mergedVals), nil
}
func (st *HelmState) RenderReleaseValuesFileToBytes(release *ReleaseSpec, path string) ([]byte, error) {
templateData := st.newReleaseTemplateData(release)
// prepareReleaseValuesEntries normalizes release.Values path entries (applying ValuesPathPrefix)
// and evaluates any vals ref+ secrets, returning the fully-rendered values slice ready for processing.
func (st *HelmState) prepareReleaseValuesEntries(release *ReleaseSpec) ([]any, error) {
values := []any{}
for _, v := range release.Values {
switch typedValue := v.(type) {
case string:
path := st.storage().normalizePath(release.ValuesPathPrefix + typedValue)
values = append(values, path)
default:
values = append(values, typedValue)
}
}
valuesMapSecretsRendered, err := st.valsRuntime.Eval(map[string]any{"values": values})
if err != nil {
return nil, err
}
valuesSecretsRendered, ok := valuesMapSecretsRendered["values"].([]any)
if !ok {
return nil, fmt.Errorf("failed to render values in %s for release %s: type %T isn't supported", st.FilePath, release.Name, valuesMapSecretsRendered["values"])
}
return valuesSecretsRendered, nil
}
func (st *HelmState) resolveReleaseValues(release *ReleaseSpec) (map[string]any, error) {
merged := map[string]any{}
valuesSecretsRendered, err := st.prepareReleaseValuesEntries(release)
if err != nil {
return nil, err
}
for _, v := range valuesSecretsRendered {
switch typedValue := v.(type) {
case string:
paths, skip, err := st.storage().resolveFile(st.getReleaseMissingFileHandler(release), "values", typedValue, st.getReleaseMissingFileHandlerConfig(release).resolveFileOptions()...)
if err != nil {
return nil, err
}
if skip {
continue
}
if len(paths) > 1 {
return nil, fmt.Errorf("glob patterns in release values are not supported for template data resolution: value=%q, resolvedPaths=%v", typedValue, paths)
}
path := paths[0]
yamlBytes, err := st.RenderReleaseValuesFileToBytes(release, path)
if err != nil {
return nil, fmt.Errorf("failed to render values file \"%s\": %w", typedValue, err)
}
var rawVals map[string]any
if err := yaml.Unmarshal(yamlBytes, &rawVals); err != nil {
return nil, fmt.Errorf("failed to parse values file \"%s\": %w", typedValue, err)
}
// Normalize nested keys: yaml v2 may produce map[any]any for nested maps.
// CastKeysToStrings recurses through both map[any]any and map[string]any so it is
// safe to call even when yaml v3 is in use and keys are already strings.
normalizedVals, err := maputil.CastKeysToStrings(rawVals)
if err != nil {
return nil, fmt.Errorf("failed to normalize keys in values file \"%s\": %w", typedValue, err)
}
merged = maputil.MergeMaps(merged, normalizedVals)
case map[any]any:
strMap, err := maputil.CastKeysToStrings(typedValue)
if err != nil {
return nil, err
}
merged = maputil.MergeMaps(merged, strMap)
case map[string]any:
merged = maputil.MergeMaps(merged, typedValue)
default:
return nil, fmt.Errorf("unexpected type of value in release values: value=%v, type=%T", typedValue, typedValue)
}
}
return merged, nil
}
func (st *HelmState) renderValuesFileToBytesWithData(path string, templateData releaseTemplateData) ([]byte, error) {
r := tmpl.NewFileRenderer(st.fs, filepath.Dir(path), templateData)
rawBytes, err := r.RenderToBytes(path)
if err != nil {
@ -4189,6 +4275,27 @@ func (st *HelmState) RenderReleaseValuesFileToBytes(release *ReleaseSpec, path s
return rawBytes, nil
}
func (st *HelmState) generateTemporaryReleaseValuesFilesWithData(release *ReleaseSpec, values []any, getTemplateData func() (releaseTemplateData, error)) ([]string, error) {
return st.generateTemporaryReleaseValuesFilesCore(release, values, func(path string) ([]byte, error) {
templateData, err := getTemplateData()
if err != nil {
return nil, err
}
return st.renderValuesFileToBytesWithData(path, templateData)
})
}
func (st *HelmState) newReleaseTemplateFuncMap(dir string) template.FuncMap {
r := tmpl.NewFileRenderer(st.fs, dir, nil)
return r.Context.CreateFuncMap()
}
func (st *HelmState) RenderReleaseValuesFileToBytes(release *ReleaseSpec, path string) ([]byte, error) {
templateData := st.newReleaseTemplateData(release)
return st.renderValuesFileToBytesWithData(path, templateData)
}
func (st *HelmState) storage() *Storage {
return &Storage{
FilePath: st.FilePath,
@ -4314,6 +4421,14 @@ func (st *HelmState) getMissingFileHandler() *string {
}
func (st *HelmState) generateTemporaryReleaseValuesFiles(release *ReleaseSpec, values []any) ([]string, error) {
return st.generateTemporaryReleaseValuesFilesCore(release, values, func(path string) ([]byte, error) {
return st.RenderReleaseValuesFileToBytes(release, path)
})
}
// generateTemporaryReleaseValuesFilesCore is the shared implementation for generating temporary values files.
// renderStringValue is called for each string value entry after the file path has been resolved.
func (st *HelmState) generateTemporaryReleaseValuesFilesCore(release *ReleaseSpec, values []any, renderStringValue func(path string) ([]byte, error)) ([]string, error) {
generatedFiles := []string{}
for _, value := range values {
@ -4332,45 +4447,86 @@ func (st *HelmState) generateTemporaryReleaseValuesFiles(release *ReleaseSpec, v
}
path := paths[0]
yamlBytes, err := st.RenderReleaseValuesFileToBytes(release, path)
yamlBytes, err := renderStringValue(path)
if err != nil {
return generatedFiles, fmt.Errorf("failed to render values files \"%s\": %v", typedValue, err)
}
valfile, err := createTempValuesFile(release, yamlBytes)
if err := func() error {
valfile, err := createTempValuesFile(release, yamlBytes)
if err != nil {
return err
}
defer func() {
_ = valfile.Close()
}()
if _, err := valfile.Write(yamlBytes); err != nil {
return fmt.Errorf("failed to write %s: %v", valfile.Name(), err)
}
st.logger.Debugf("Successfully generated the value file from %s to %s", path, valfile.Name())
generatedFiles = append(generatedFiles, valfile.Name())
return nil
}(); err != nil {
return generatedFiles, err
}
case map[any]any:
strMap, err := maputil.CastKeysToStrings(typedValue)
if err != nil {
return generatedFiles, err
}
defer func() {
_ = valfile.Close()
}()
if err := func() error {
valfile, err := createTempValuesFile(release, strMap)
if err != nil {
return err
}
defer func() {
_ = valfile.Close()
}()
if _, err := valfile.Write(yamlBytes); err != nil {
return generatedFiles, fmt.Errorf("failed to write %s: %v", valfile.Name(), err)
}
encoder := yaml.NewEncoder(valfile)
defer func() {
_ = encoder.Close()
}()
st.logger.Debugf("Successfully generated the value file at %s. produced:\n%s", path, string(yamlBytes))
if err := encoder.Encode(strMap); err != nil {
return err
}
generatedFiles = append(generatedFiles, valfile.Name())
case map[any]any, map[string]any:
valfile, err := createTempValuesFile(release, typedValue)
if err != nil {
generatedFiles = append(generatedFiles, valfile.Name())
return nil
}(); err != nil {
return generatedFiles, err
}
defer func() {
_ = valfile.Close()
}()
case map[string]any:
if err := func() error {
valfile, err := createTempValuesFile(release, typedValue)
if err != nil {
return err
}
defer func() {
_ = valfile.Close()
}()
encoder := yaml.NewEncoder(valfile)
defer func() {
_ = encoder.Close()
}()
encoder := yaml.NewEncoder(valfile)
defer func() {
_ = encoder.Close()
}()
if err := encoder.Encode(typedValue); err != nil {
if err := encoder.Encode(typedValue); err != nil {
return err
}
generatedFiles = append(generatedFiles, valfile.Name())
return nil
}(); err != nil {
return generatedFiles, err
}
generatedFiles = append(generatedFiles, valfile.Name())
default:
return generatedFiles, fmt.Errorf("unexpected type of value: value=%v, type=%T", typedValue, typedValue)
}
@ -4379,27 +4535,11 @@ func (st *HelmState) generateTemporaryReleaseValuesFiles(release *ReleaseSpec, v
}
func (st *HelmState) generateVanillaValuesFiles(release *ReleaseSpec) ([]string, error) {
values := []any{}
for _, v := range release.Values {
switch typedValue := v.(type) {
case string:
path := st.storage().normalizePath(release.ValuesPathPrefix + typedValue)
values = append(values, path)
default:
values = append(values, v)
}
}
valuesMapSecretsRendered, err := st.valsRuntime.Eval(map[string]any{"values": values})
valuesSecretsRendered, err := st.prepareReleaseValuesEntries(release)
if err != nil {
return nil, err
}
valuesSecretsRendered, ok := valuesMapSecretsRendered["values"].([]any)
if !ok {
return nil, fmt.Errorf("Failed to render values in %s for release %s: type %T isn't supported", st.FilePath, release.Name, valuesMapSecretsRendered["values"])
}
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, valuesSecretsRendered)
if err != nil {
return nil, err