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:
parent
6cf02956c2
commit
38e27ee439
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue