diff --git a/pkg/state/create_test.go b/pkg/state/create_test.go index aa6b704e..cd305eee 100644 --- a/pkg/state/create_test.go +++ b/pkg/state/create_test.go @@ -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" @@ -932,3 +934,450 @@ releases: }) } } + +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") +} diff --git a/pkg/state/helmx.go b/pkg/state/helmx.go index b337cd30..f259d585 100644 --- a/pkg/state/helmx.go +++ b/pkg/state/helmx.go @@ -404,9 +404,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: %v", 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 } @@ -420,7 +439,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 } @@ -434,7 +453,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 } diff --git a/pkg/state/state.go b/pkg/state/state.go index f51a0dd1..a2d45960 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -36,6 +36,7 @@ import ( "github.com/helmfile/helmfile/pkg/event" "github.com/helmfile/helmfile/pkg/filesystem" "github.com/helmfile/helmfile/pkg/helmexec" + "github.com/helmfile/helmfile/pkg/maputil" "github.com/helmfile/helmfile/pkg/remote" "github.com/helmfile/helmfile/pkg/tmpl" "github.com/helmfile/helmfile/pkg/yaml" @@ -3916,15 +3917,93 @@ 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\": %v", typedValue, err) + } + + var vals map[string]any + if err := yaml.Unmarshal(yamlBytes, &vals); err != nil { + return nil, fmt.Errorf("failed to parse values file \"%s\": %v", typedValue, err) + } + + merged = maputil.MergeMaps(merged, vals) + 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 { @@ -3955,6 +4034,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, @@ -4080,6 +4180,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 { @@ -4098,45 +4206,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) } @@ -4145,27 +4294,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