mirror of
https://github.com/helmfile/helmfile.git
synced 2026-09-30 12:34:23 +02:00
* 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>
1477 lines
42 KiB
Go
1477 lines
42 KiB
Go
package state
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"reflect"
|
|
"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"
|
|
)
|
|
|
|
func createFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) {
|
|
c := &StateCreator{
|
|
logger: logger,
|
|
fs: filesystem.DefaultFileSystem(),
|
|
Strict: true,
|
|
}
|
|
return c.ParseAndLoad(content, filepath.Dir(file), file, env, false, true, true, nil, nil)
|
|
}
|
|
|
|
func TestReadFromYaml(t *testing.T) {
|
|
yamlFile := "example/path/to/yaml/file"
|
|
yamlContent := []byte(`releases:
|
|
- name: myrelease
|
|
namespace: mynamespace
|
|
chart: mychart
|
|
`)
|
|
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
if state.Releases[0].Name != "myrelease" {
|
|
t.Errorf("unexpected release name: expected=myrelease actual=%s", state.Releases[0].Name)
|
|
}
|
|
if state.Releases[0].Namespace != "mynamespace" {
|
|
t.Errorf("unexpected chart namespace: expected=mynamespace actual=%s", state.Releases[0].Chart)
|
|
}
|
|
if state.Releases[0].Chart != "mychart" {
|
|
t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart)
|
|
}
|
|
}
|
|
|
|
func TestReadFromYaml_NonexistentEnv(t *testing.T) {
|
|
yamlFile := "example/path/to/yaml/file"
|
|
yamlContent := []byte(`releases:
|
|
- name: myrelease
|
|
namespace: mynamespace
|
|
chart: mychart
|
|
`)
|
|
_, err := createFromYaml(yamlContent, yamlFile, "production", logger)
|
|
// This does not produce an error because the environment existence check if done
|
|
// outside of the ParseAndLoad function since
|
|
// https://github.com/helmfile/helmfile/pull/885
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
type stateTestEnv struct {
|
|
Files map[string]string
|
|
WorkDir string
|
|
}
|
|
|
|
func (testEnv stateTestEnv) MustLoadState(t *testing.T, file, envName string) *HelmState {
|
|
return testEnv.MustLoadStateWithEnableLiveOutput(t, file, envName, false)
|
|
}
|
|
|
|
func (testEnv stateTestEnv) MustLoadStateWithEnableLiveOutput(t *testing.T, file, envName string, enableLiveOutput bool) *HelmState {
|
|
t.Helper()
|
|
|
|
testFs := testhelper.NewTestFs(testEnv.Files)
|
|
|
|
if testFs.Cwd == "" {
|
|
testFs.Cwd = "/"
|
|
}
|
|
|
|
yamlContent, ok := testEnv.Files[file]
|
|
if !ok {
|
|
t.Fatalf("no file named %q registered", file)
|
|
}
|
|
|
|
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
|
|
state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", "", r, enableLiveOutput, "").
|
|
ParseAndLoad([]byte(yamlContent), filepath.Dir(file), file, envName, true, true, true, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
return state
|
|
}
|
|
|
|
func TestReadFromYaml_NonDefaultEnv(t *testing.T) {
|
|
yamlFile := "/example/path/to/helmfile.yaml"
|
|
yamlContent := []byte(`environments:
|
|
production:
|
|
values:
|
|
- foo.yaml
|
|
- bar.yaml.gotmpl
|
|
|
|
releases:
|
|
- name: myrelease
|
|
namespace: mynamespace
|
|
chart: mychart
|
|
values:
|
|
- values.yaml.gotmpl
|
|
`)
|
|
|
|
fooYamlFile := "/example/path/to/foo.yaml"
|
|
fooYamlContent := []byte(`foo: foo
|
|
# As this file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
|
|
baz: "{{ readFile \"baz.txt\" }}"`)
|
|
|
|
barYamlFile := "/example/path/to/bar.yaml.gotmpl"
|
|
barYamlContent := []byte(`foo: FOO
|
|
bar: {{ readFile "bar.txt" }}
|
|
env: {{ .Environment.Name }}
|
|
`)
|
|
|
|
barTextFile := "/example/path/to/bar.txt"
|
|
barTextContent := []byte("BAR")
|
|
|
|
expected := map[string]any{
|
|
"foo": "FOO",
|
|
"bar": "BAR",
|
|
// As the file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
|
|
"baz": "{{ readFile \"baz.txt\" }}",
|
|
"env": "production",
|
|
}
|
|
|
|
valuesFile := "/example/path/to/values.yaml.gotmpl"
|
|
valuesContent := []byte(`env: {{ .Environment.Name }}
|
|
releaseName: {{ .Release.Name }}
|
|
releaseNamespace: {{ .Release.Namespace }}
|
|
`)
|
|
|
|
expectedValues := `env: production
|
|
releaseName: myrelease
|
|
releaseNamespace: mynamespace
|
|
`
|
|
|
|
testFs := testhelper.NewTestFs(map[string]string{
|
|
fooYamlFile: string(fooYamlContent),
|
|
barYamlFile: string(barYamlContent),
|
|
barTextFile: string(barTextContent),
|
|
valuesFile: string(valuesContent),
|
|
})
|
|
testFs.Cwd = "/example/path/to"
|
|
|
|
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
|
|
env := environment.Environment{
|
|
Name: "production",
|
|
}
|
|
state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", "", r, false, "").
|
|
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, true, true, &env, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
actual := state.Env.Values
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
|
|
}
|
|
|
|
release := state.Releases[0]
|
|
|
|
state.ApplyOverrides(&release)
|
|
|
|
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, valuesFile)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
actualValues := string(actualValuesData)
|
|
|
|
if !reflect.DeepEqual(expectedValues, actualValues) {
|
|
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
|
|
}
|
|
}
|
|
|
|
func TestReadFromYaml_OverrideNamespace(t *testing.T) {
|
|
yamlFile := "/example/path/to/helmfile.yaml"
|
|
yamlContent := []byte(`environments:
|
|
production:
|
|
values:
|
|
- foo.yaml
|
|
- bar.yaml.gotmpl
|
|
|
|
# A.k.a helmfile apply --namespace myns
|
|
namespace: myns
|
|
|
|
releases:
|
|
- name: myrelease
|
|
namespace: mynamespace
|
|
chart: mychart
|
|
values:
|
|
- values.yaml.gotmpl
|
|
`)
|
|
|
|
fooYamlFile := "/example/path/to/foo.yaml"
|
|
fooYamlContent := []byte(`foo: foo
|
|
# As this file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
|
|
baz: "{{ readFile \"baz.txt\" }}"`)
|
|
|
|
barYamlFile := "/example/path/to/bar.yaml.gotmpl"
|
|
barYamlContent := []byte(`foo: FOO
|
|
bar: {{ readFile "bar.txt" }}
|
|
`)
|
|
|
|
barTextFile := "/example/path/to/bar.txt"
|
|
barTextContent := []byte("BAR")
|
|
|
|
expected := map[string]any{
|
|
"foo": "FOO",
|
|
"bar": "BAR",
|
|
// As the file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
|
|
"baz": "{{ readFile \"baz.txt\" }}",
|
|
}
|
|
|
|
valuesFile := "/example/path/to/values.yaml.gotmpl"
|
|
valuesContent := []byte(`env: {{ .Environment.Name }}
|
|
releaseName: {{ .Release.Name }}
|
|
releaseNamespace: {{ .Release.Namespace }}
|
|
overrideNamespace: {{ .Namespace }}
|
|
`)
|
|
|
|
expectedValues := `env: production
|
|
releaseName: myrelease
|
|
releaseNamespace: myns
|
|
overrideNamespace: myns
|
|
`
|
|
|
|
testFs := testhelper.NewTestFs(map[string]string{
|
|
fooYamlFile: string(fooYamlContent),
|
|
barYamlFile: string(barYamlContent),
|
|
barTextFile: string(barTextContent),
|
|
valuesFile: string(valuesContent),
|
|
})
|
|
testFs.Cwd = "/example/path/to"
|
|
|
|
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
|
|
state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", "", r, false, "").
|
|
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, true, true, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
actual := state.Env.Values
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
|
|
}
|
|
|
|
release := state.Releases[0]
|
|
|
|
state.ApplyOverrides(&release)
|
|
|
|
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, valuesFile)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
actualValues := string(actualValuesData)
|
|
|
|
if !reflect.DeepEqual(expectedValues, actualValues) {
|
|
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
|
|
}
|
|
}
|
|
|
|
func TestReadFromYaml_StrictUnmarshalling(t *testing.T) {
|
|
yamlFile := "example/path/to/yaml/file"
|
|
yamlContent := []byte(`releases:
|
|
- name: myrelease
|
|
namespace: mynamespace
|
|
releases: mychart
|
|
`)
|
|
_, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
|
if err == nil {
|
|
t.Error("expected an error for wrong key 'releases' which is not in struct")
|
|
}
|
|
}
|
|
|
|
func TestReadFromYaml_ConflictingReleasesConfig(t *testing.T) {
|
|
yamlFile := "example/path/to/yaml/file"
|
|
yamlContent := []byte(`charts:
|
|
- name: myrelease1
|
|
chart: mychart1
|
|
releases:
|
|
- name: myrelease2
|
|
chart: mychart2
|
|
`)
|
|
_, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
|
if err == nil {
|
|
t.Error("expected error")
|
|
}
|
|
}
|
|
|
|
func TestReadFromYaml_FilterReleasesOnLabels(t *testing.T) {
|
|
yamlFile := "example/path/to/yaml/file"
|
|
yamlContent := []byte(`releases:
|
|
- name: myrelease1
|
|
chart: mychart1
|
|
labels:
|
|
tier: frontend
|
|
foo: bar
|
|
- name: myrelease2
|
|
chart: mychart2
|
|
labels:
|
|
tier: frontend
|
|
- name: myrelease3
|
|
chart: mychart3
|
|
labels:
|
|
tier: backend
|
|
`)
|
|
cases := []struct {
|
|
filter LabelFilter
|
|
results []bool
|
|
}{
|
|
{LabelFilter{positiveLabels: [][]string{{"tier", "frontend"}}},
|
|
[]bool{true, true, false}},
|
|
{LabelFilter{positiveLabels: [][]string{{"tier", "frontend"}, {"foo", "bar"}}},
|
|
[]bool{true, false, false}},
|
|
{LabelFilter{negativeLabels: [][]string{{"tier", "frontend"}}},
|
|
[]bool{false, false, true}},
|
|
{LabelFilter{positiveLabels: [][]string{{"tier", "frontend"}}, negativeLabels: [][]string{{"foo", "bar"}}},
|
|
[]bool{false, true, false}},
|
|
}
|
|
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
for idx, c := range cases {
|
|
for idx2, expected := range c.results {
|
|
if f := c.filter.Match(state.Releases[idx2]); f != expected {
|
|
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadFromYaml_FilterNegatives(t *testing.T) {
|
|
yamlFile := "example/path/to/yaml/file"
|
|
yamlContent := []byte(`releases:
|
|
- name: myrelease1
|
|
chart: mychart1
|
|
labels:
|
|
stage: pre
|
|
foo: bar
|
|
- name: myrelease2
|
|
chart: mychart2
|
|
labels:
|
|
stage: post
|
|
- name: myrelease3
|
|
chart: mychart3
|
|
`)
|
|
cases := []struct {
|
|
filter LabelFilter
|
|
results []bool
|
|
}{
|
|
{LabelFilter{positiveLabels: [][]string{{"stage", "pre"}}},
|
|
[]bool{true, false, false}},
|
|
{LabelFilter{positiveLabels: [][]string{{"stage", "post"}}},
|
|
[]bool{false, true, false}},
|
|
{LabelFilter{negativeLabels: [][]string{{"stage", "pre"}, {"stage", "post"}}},
|
|
[]bool{false, false, true}},
|
|
{LabelFilter{negativeLabels: [][]string{{"foo", "bar"}}},
|
|
[]bool{false, true, true}},
|
|
}
|
|
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
for idx, c := range cases {
|
|
for idx2, expected := range c.results {
|
|
if f := c.filter.Match(state.Releases[idx2]); f != expected {
|
|
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadFromYaml_Helmfiles_Selectors(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
content []byte
|
|
wantErr bool
|
|
helmfiles []SubHelmfileSpec
|
|
}{
|
|
{
|
|
path: "working/selector",
|
|
content: []byte(`helmfiles:
|
|
- simple/helmfile.yaml
|
|
- path: path/prefix/selector.yaml
|
|
selectors:
|
|
- name=zorba
|
|
- foo=bar
|
|
- path: path/prefix/empty/selector.yaml
|
|
selectors: []
|
|
- path: path/prefix/inherits/selector.yaml
|
|
selectorsInherited: true
|
|
`),
|
|
wantErr: false,
|
|
helmfiles: []SubHelmfileSpec{{Path: "simple/helmfile.yaml", Selectors: nil, SelectorsInherited: false},
|
|
{Path: "path/prefix/selector.yaml", Selectors: []string{"name=zorba", "foo=bar"}, SelectorsInherited: false},
|
|
{Path: "path/prefix/empty/selector.yaml", Selectors: []string{}, SelectorsInherited: false},
|
|
{Path: "path/prefix/inherits/selector.yaml", Selectors: nil, SelectorsInherited: true},
|
|
},
|
|
},
|
|
{
|
|
path: "failing2/selector",
|
|
content: []byte(`helmfiles:
|
|
- path: failing2/helmfile.yaml
|
|
wrongkey:
|
|
`),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
path: "failing3/selector",
|
|
content: []byte(`helmfiles:
|
|
- path: failing3/helmfile.yaml
|
|
selectors: foo
|
|
`),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
path: "failing4/selector",
|
|
content: []byte(`helmfiles:
|
|
- path: failing4/helmfile.yaml
|
|
selectors:
|
|
`),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
path: "failing4/selector",
|
|
content: []byte(`helmfiles:
|
|
- path: failing4/helmfile.yaml
|
|
selectors:
|
|
- colon: not-authorized
|
|
`),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
path: "failing6/selector",
|
|
content: []byte(`helmfiles:
|
|
- selectors:
|
|
- whatever
|
|
`),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
path: "failing7/selector",
|
|
content: []byte(`helmfiles:
|
|
- path: foo/bar
|
|
selectors:
|
|
- foo=bar
|
|
selectorsInherited: true
|
|
`),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
st, err := createFromYaml(test.content, test.path, DefaultEnv, logger)
|
|
if err != nil {
|
|
if test.wantErr {
|
|
continue
|
|
} else {
|
|
t.Error("unexpected error:", err)
|
|
}
|
|
}
|
|
require.Equalf(t, test.helmfiles, st.Helmfiles, "for path %s", test.path)
|
|
}
|
|
}
|
|
|
|
func TestReadFromYaml_EnvironmentContext(t *testing.T) {
|
|
yamlFile := "/example/path/to/helmfile.yaml"
|
|
yamlContent := []byte(`environments:
|
|
production:
|
|
values: []
|
|
kubeContext: myCtx
|
|
|
|
releases:
|
|
- name: myrelease
|
|
namespace: mynamespace
|
|
chart: mychart
|
|
values:
|
|
- values.yaml.gotmpl
|
|
`)
|
|
|
|
valuesFile := "/example/path/to/values.yaml.gotmpl"
|
|
valuesContent := []byte(`envName: {{ .Environment.Name }}
|
|
envContext: {{ .Environment.KubeContext }}
|
|
releaseName: {{ .Release.Name }}
|
|
releaseContext: {{ .Release.KubeContext }}
|
|
`)
|
|
|
|
expectedValues := `envName: production
|
|
envContext: myCtx
|
|
releaseName: myrelease
|
|
releaseContext:
|
|
`
|
|
|
|
testFs := testhelper.NewTestFs(map[string]string{
|
|
valuesFile: string(valuesContent),
|
|
})
|
|
testFs.Cwd = "/example/path/to"
|
|
|
|
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
|
|
state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", "", r, false, "").
|
|
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, true, true, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
release := state.Releases[0]
|
|
|
|
state.ApplyOverrides(&release)
|
|
|
|
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, valuesFile)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
actualValues := string(actualValuesData)
|
|
|
|
if !reflect.DeepEqual(expectedValues, actualValues) {
|
|
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
|
|
}
|
|
}
|
|
|
|
// TestHelmBinaryInBases tests that helmBinary and kustomizeBinary settings
|
|
// from bases are properly merged with later values overriding earlier ones
|
|
func TestHelmBinaryInBases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
mainFile string
|
|
expectedHelmBinary string
|
|
expectedKustomizeBinary string
|
|
}{
|
|
{
|
|
name: "helmBinary in second base should be used",
|
|
files: map[string]string{
|
|
"/path/to/helmfile.yaml": `bases:
|
|
- ./bases/env.yaml
|
|
---
|
|
bases:
|
|
- ./bases/repos.yaml
|
|
---
|
|
bases:
|
|
- ./bases/releases.yaml
|
|
`,
|
|
"/path/to/bases/env.yaml": `environments:
|
|
default:
|
|
values:
|
|
- key: value1
|
|
`,
|
|
"/path/to/bases/repos.yaml": `repositories:
|
|
- name: stable
|
|
url: https://charts.helm.sh/stable
|
|
helmBinary: /path/to/custom/helm
|
|
`,
|
|
"/path/to/bases/releases.yaml": `releases:
|
|
- name: myapp
|
|
chart: stable/nginx
|
|
`,
|
|
},
|
|
mainFile: "/path/to/helmfile.yaml",
|
|
expectedHelmBinary: "/path/to/custom/helm",
|
|
expectedKustomizeBinary: DefaultKustomizeBinary,
|
|
},
|
|
{
|
|
name: "helmBinary in main file after bases should override",
|
|
files: map[string]string{
|
|
"/path/to/helmfile.yaml": `bases:
|
|
- ./bases/env.yaml
|
|
---
|
|
bases:
|
|
- ./bases/repos.yaml
|
|
---
|
|
bases:
|
|
- ./bases/releases.yaml
|
|
helmBinary: /path/to/main/helm
|
|
`,
|
|
"/path/to/bases/env.yaml": `environments:
|
|
default:
|
|
values:
|
|
- key: value1
|
|
`,
|
|
"/path/to/bases/repos.yaml": `repositories:
|
|
- name: stable
|
|
url: https://charts.helm.sh/stable
|
|
helmBinary: /path/to/base/helm
|
|
`,
|
|
"/path/to/bases/releases.yaml": `releases:
|
|
- name: myapp
|
|
chart: stable/nginx
|
|
`,
|
|
},
|
|
mainFile: "/path/to/helmfile.yaml",
|
|
expectedHelmBinary: "/path/to/main/helm",
|
|
expectedKustomizeBinary: DefaultKustomizeBinary,
|
|
},
|
|
{
|
|
name: "helmBinary in main file between bases should override earlier bases",
|
|
files: map[string]string{
|
|
"/path/to/helmfile.yaml": `bases:
|
|
- ./bases/env.yaml
|
|
---
|
|
bases:
|
|
- ./bases/repos.yaml
|
|
helmBinary: /path/to/middle/helm
|
|
---
|
|
bases:
|
|
- ./bases/releases.yaml
|
|
`,
|
|
"/path/to/bases/env.yaml": `environments:
|
|
default:
|
|
values:
|
|
- key: value1
|
|
`,
|
|
"/path/to/bases/repos.yaml": `repositories:
|
|
- name: stable
|
|
url: https://charts.helm.sh/stable
|
|
helmBinary: /path/to/base/helm
|
|
`,
|
|
"/path/to/bases/releases.yaml": `releases:
|
|
- name: myapp
|
|
chart: stable/nginx
|
|
`,
|
|
},
|
|
mainFile: "/path/to/helmfile.yaml",
|
|
expectedHelmBinary: "/path/to/middle/helm",
|
|
expectedKustomizeBinary: DefaultKustomizeBinary,
|
|
},
|
|
{
|
|
name: "kustomizeBinary in base should be used",
|
|
files: map[string]string{
|
|
"/path/to/helmfile.yaml": `bases:
|
|
- ./bases/base.yaml
|
|
`,
|
|
"/path/to/bases/base.yaml": `kustomizeBinary: /path/to/custom/kustomize
|
|
releases:
|
|
- name: myapp
|
|
chart: mychart
|
|
`,
|
|
},
|
|
mainFile: "/path/to/helmfile.yaml",
|
|
expectedHelmBinary: DefaultHelmBinary,
|
|
expectedKustomizeBinary: "/path/to/custom/kustomize",
|
|
},
|
|
{
|
|
name: "both helmBinary and kustomizeBinary in different bases",
|
|
files: map[string]string{
|
|
"/path/to/helmfile.yaml": `bases:
|
|
- ./bases/helm.yaml
|
|
---
|
|
bases:
|
|
- ./bases/kustomize.yaml
|
|
`,
|
|
"/path/to/bases/helm.yaml": `helmBinary: /path/to/custom/helm
|
|
`,
|
|
"/path/to/bases/kustomize.yaml": `kustomizeBinary: /path/to/custom/kustomize
|
|
`,
|
|
},
|
|
mainFile: "/path/to/helmfile.yaml",
|
|
expectedHelmBinary: "/path/to/custom/helm",
|
|
expectedKustomizeBinary: "/path/to/custom/kustomize",
|
|
},
|
|
{
|
|
name: "later base overrides earlier base for helmBinary",
|
|
files: map[string]string{
|
|
"/path/to/helmfile.yaml": `bases:
|
|
- ./bases/first.yaml
|
|
---
|
|
bases:
|
|
- ./bases/second.yaml
|
|
`,
|
|
"/path/to/bases/first.yaml": `helmBinary: /path/to/first/helm
|
|
`,
|
|
"/path/to/bases/second.yaml": `helmBinary: /path/to/second/helm
|
|
`,
|
|
},
|
|
mainFile: "/path/to/helmfile.yaml",
|
|
expectedHelmBinary: "/path/to/second/helm",
|
|
expectedKustomizeBinary: DefaultKustomizeBinary,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
testFs := testhelper.NewTestFs(tt.files)
|
|
if testFs.Cwd == "" {
|
|
testFs.Cwd = "/"
|
|
}
|
|
|
|
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
|
|
creator := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", "", r, false, "")
|
|
|
|
// Set up LoadFile for recursive base loading
|
|
creator.LoadFile = func(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error) {
|
|
path := filepath.Join(baseDir, file)
|
|
content, ok := tt.files[path]
|
|
if !ok {
|
|
return nil, fmt.Errorf("file not found: %s", path)
|
|
}
|
|
// When loading base files, we don't apply defaults - they should only
|
|
// be applied to the main file after all parts/bases are merged.
|
|
// So applyDefaults = evaluateBases (true for main, false for bases).
|
|
applyDefaults := evaluateBases
|
|
return creator.ParseAndLoad([]byte(content), filepath.Dir(path), path, DefaultEnv, true, evaluateBases, applyDefaults, inheritedEnv, overrodeEnv)
|
|
}
|
|
|
|
yamlContent, ok := tt.files[tt.mainFile]
|
|
if !ok {
|
|
t.Fatalf("no file named %q registered", tt.mainFile)
|
|
}
|
|
|
|
state, err := creator.ParseAndLoad([]byte(yamlContent), filepath.Dir(tt.mainFile), tt.mainFile, DefaultEnv, true, true, true, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if state.DefaultHelmBinary != tt.expectedHelmBinary {
|
|
t.Errorf("helmBinary mismatch: expected=%s, actual=%s",
|
|
tt.expectedHelmBinary, state.DefaultHelmBinary)
|
|
}
|
|
|
|
if state.DefaultKustomizeBinary != tt.expectedKustomizeBinary {
|
|
t.Errorf("kustomizeBinary mismatch: expected=%s, actual=%s",
|
|
tt.expectedKustomizeBinary, state.DefaultKustomizeBinary)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHelmBinaryInMultiDocumentYAML tests that helmBinary is preserved when
|
|
// processing multi-document YAML files (files with --- separators).
|
|
// This is a regression test for issue #2319.
|
|
func TestHelmBinaryInMultiDocumentYAML(t *testing.T) {
|
|
logger := zaptest.NewLogger(t).Sugar()
|
|
testFs := testhelper.NewTestFs(map[string]string{})
|
|
testFs.Cwd = "/"
|
|
|
|
r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem())
|
|
creator := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", "", r, false, "")
|
|
|
|
// Simulate a multi-document YAML where the first document sets helmBinary
|
|
// and the second document has no helmBinary
|
|
doc1 := `helmBinary: /custom/helm
|
|
helmDefaults:
|
|
timeout: 300
|
|
`
|
|
doc2 := `releases:
|
|
- name: myapp
|
|
chart: stable/nginx
|
|
`
|
|
|
|
// Simulate what load() in desired_state_file_loader.go does:
|
|
// 1. Process each part with applyDefaults=false
|
|
state1, err := creator.ParseAndLoad([]byte(doc1), "/", "/helmfile.yaml", DefaultEnv, true, true, false, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
state2, err := creator.ParseAndLoad([]byte(doc2), "/", "/helmfile.yaml", DefaultEnv, true, true, false, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
// 2. Merge parts (simulating what mergo.Merge does with mergo.WithOverride)
|
|
// Since state2 has empty DefaultHelmBinary, mergo.WithOverride should preserve state1's value
|
|
require.NoError(t, mergo.Merge(state1, state2, mergo.WithAppendSlice, mergo.WithOverride))
|
|
|
|
// 3. Apply defaults after merge
|
|
creator.ApplyDefaultsAndOverrides(state1)
|
|
|
|
// Verify that helmBinary from first document is preserved
|
|
assert.Equal(t, "/custom/helm", state1.DefaultHelmBinary,
|
|
"helmBinary from first document should be preserved after merge and applyDefaults")
|
|
}
|
|
|
|
// TestEnvironmentMergingWithBases tests that environment values from multiple bases
|
|
// are properly merged rather than replaced. This is a regression test for issue #2273.
|
|
func TestEnvironmentMergingWithBases(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files map[string]string
|
|
mainFile string
|
|
environment string
|
|
expectedError bool
|
|
checkValues func(t *testing.T, state *HelmState)
|
|
}{
|
|
{
|
|
name: "environment values should merge from multiple bases",
|
|
files: map[string]string{
|
|
"/path/one.yaml": `environments:
|
|
sandbox:
|
|
values:
|
|
- example:
|
|
enabled: true
|
|
`,
|
|
"/path/two.yaml": `environments:
|
|
sandbox: {}
|
|
`,
|
|
"/path/helmfile.yaml": `bases:
|
|
- one.yaml
|
|
- two.yaml
|
|
---
|
|
repositories:
|
|
- name: examples
|
|
url: https://helm.github.io/examples
|
|
releases:
|
|
- name: example
|
|
chart: examples/hello-world
|
|
`,
|
|
},
|
|
mainFile: "/path/helmfile.yaml",
|
|
environment: "sandbox",
|
|
checkValues: func(t *testing.T, state *HelmState) {
|
|
// Check that the environment has the values from the first base
|
|
envSpec, ok := state.Environments["sandbox"]
|
|
require.True(t, ok, "sandbox environment should exist")
|
|
require.NotNil(t, envSpec.Values, "environment values should not be nil")
|
|
require.Greater(t, len(envSpec.Values), 0, "environment should have values from first base")
|
|
|
|
// Check that RenderedValues has the example.enabled value
|
|
require.NotNil(t, state.RenderedValues, "rendered values should not be nil")
|
|
exampleVal, ok := state.RenderedValues["example"]
|
|
require.True(t, ok, "example key should exist in rendered values")
|
|
exampleMap, ok := exampleVal.(map[string]any)
|
|
require.True(t, ok, "example should be a map")
|
|
enabled, ok := exampleMap["enabled"]
|
|
require.True(t, ok, "enabled key should exist")
|
|
require.Equal(t, true, enabled, "enabled should be true")
|
|
},
|
|
},
|
|
{
|
|
name: "environment values should merge when second base adds values",
|
|
files: map[string]string{
|
|
"/path/one.yaml": `environments:
|
|
sandbox:
|
|
values:
|
|
- example:
|
|
enabled: true
|
|
`,
|
|
"/path/two.yaml": `environments:
|
|
sandbox:
|
|
values:
|
|
- another:
|
|
setting: value
|
|
`,
|
|
"/path/helmfile.yaml": `bases:
|
|
- one.yaml
|
|
- two.yaml
|
|
---
|
|
repositories:
|
|
- name: examples
|
|
url: https://helm.github.io/examples
|
|
releases:
|
|
- name: example
|
|
chart: examples/hello-world
|
|
`,
|
|
},
|
|
mainFile: "/path/helmfile.yaml",
|
|
environment: "sandbox",
|
|
checkValues: func(t *testing.T, state *HelmState) {
|
|
// Check that both values from both bases are present
|
|
require.NotNil(t, state.RenderedValues, "rendered values should not be nil")
|
|
|
|
exampleVal, ok := state.RenderedValues["example"]
|
|
require.True(t, ok, "example key should exist in rendered values")
|
|
exampleMap, ok := exampleVal.(map[string]any)
|
|
require.True(t, ok, "example should be a map")
|
|
enabled, ok := exampleMap["enabled"]
|
|
require.True(t, ok, "enabled key should exist")
|
|
require.Equal(t, true, enabled, "enabled should be true")
|
|
|
|
anotherVal, ok := state.RenderedValues["another"]
|
|
require.True(t, ok, "another key should exist in rendered values")
|
|
anotherMap, ok := anotherVal.(map[string]any)
|
|
require.True(t, ok, "another should be a map")
|
|
setting, ok := anotherMap["setting"]
|
|
require.True(t, ok, "setting key should exist")
|
|
require.Equal(t, "value", setting, "setting should be 'value'")
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
creator := &StateCreator{
|
|
logger: logger,
|
|
fs: &filesystem.FileSystem{
|
|
ReadFile: func(filename string) ([]byte, error) {
|
|
content, ok := tt.files[filename]
|
|
if !ok {
|
|
return nil, fmt.Errorf("file not found: %s", filename)
|
|
}
|
|
return []byte(content), nil
|
|
},
|
|
},
|
|
valsRuntime: valsRuntime,
|
|
Strict: true,
|
|
}
|
|
creator.LoadFile = func(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error) {
|
|
path := filepath.Join(baseDir, file)
|
|
content, ok := tt.files[path]
|
|
if !ok {
|
|
return nil, fmt.Errorf("file not found: %s", path)
|
|
}
|
|
// When loading base files, we don't apply defaults - they should only
|
|
// be applied to the main file after all parts/bases are merged.
|
|
// So applyDefaults = evaluateBases (true for main, false for bases).
|
|
applyDefaults := evaluateBases
|
|
return creator.ParseAndLoad([]byte(content), filepath.Dir(path), path, tt.environment, true, evaluateBases, applyDefaults, inheritedEnv, overrodeEnv)
|
|
}
|
|
|
|
yamlContent, ok := tt.files[tt.mainFile]
|
|
if !ok {
|
|
t.Fatalf("no file named %q registered", tt.mainFile)
|
|
}
|
|
|
|
state, err := creator.ParseAndLoad([]byte(yamlContent), filepath.Dir(tt.mainFile), tt.mainFile, tt.environment, true, true, true, nil, nil)
|
|
if tt.expectedError {
|
|
require.Error(t, err, "expected an error but got none")
|
|
return
|
|
}
|
|
require.NoError(t, err, "unexpected error: %v", err)
|
|
|
|
if tt.checkValues != nil {
|
|
tt.checkValues(t, state)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// mergeEnvironments must keep the established "later layer wins over earlier
|
|
// layer" semantics even when the resolved strategy is fallback. Under override
|
|
// the natural append order (dst then src) achieves that because last-wins
|
|
// chooses src; under fallback we have to prepend src so that first-wins still
|
|
// resolves to src. Without this adjustment, a base helmfile's values would
|
|
// silently override values declared in the main helmfile that includes it.
|
|
func TestMergeEnvironments_LaterLayerWinsRegardlessOfStrategy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
strategy string
|
|
wantOrdering []any
|
|
}{
|
|
{
|
|
name: "override appends src after dst (last wins)",
|
|
strategy: MergeStrategyOverride,
|
|
wantOrdering: []any{"base.yaml", "main.yaml"},
|
|
},
|
|
{
|
|
name: "empty strategy defaults to override semantics",
|
|
strategy: "",
|
|
wantOrdering: []any{"base.yaml", "main.yaml"},
|
|
},
|
|
{
|
|
name: "fallback prepends src (first wins) so main still beats base",
|
|
strategy: MergeStrategyFallback,
|
|
wantOrdering: []any{"main.yaml", "base.yaml"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dst := map[string]EnvironmentSpec{
|
|
"prod": {Values: []any{"base.yaml"}, MergeStrategy: tt.strategy},
|
|
}
|
|
src := map[string]EnvironmentSpec{
|
|
"prod": {Values: []any{"main.yaml"}, MergeStrategy: tt.strategy},
|
|
}
|
|
mergeEnvironments(dst, src)
|
|
got := dst["prod"].Values
|
|
if !reflect.DeepEqual(got, tt.wantOrdering) {
|
|
t.Errorf("Values ordering: want %v, got %v", tt.wantOrdering, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// mergeEnvironments must preserve MergeStrategy when layering multiple
|
|
// helmfiles (bases). Without this, an environment that opted into
|
|
// fallback in a base helmfile would silently fall back to the default
|
|
// override behavior in any helmfile that re-declares the environment.
|
|
func TestMergeEnvironments_PreservesMergeStrategy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dst EnvironmentSpec
|
|
src EnvironmentSpec
|
|
expected string
|
|
}{
|
|
{
|
|
name: "src declares fallback, dst empty",
|
|
dst: EnvironmentSpec{},
|
|
src: EnvironmentSpec{MergeStrategy: MergeStrategyFallback},
|
|
expected: MergeStrategyFallback,
|
|
},
|
|
{
|
|
name: "dst declares fallback, src empty preserves dst",
|
|
dst: EnvironmentSpec{MergeStrategy: MergeStrategyFallback},
|
|
src: EnvironmentSpec{},
|
|
expected: MergeStrategyFallback,
|
|
},
|
|
{
|
|
name: "src override wins over dst fallback",
|
|
dst: EnvironmentSpec{MergeStrategy: MergeStrategyFallback},
|
|
src: EnvironmentSpec{MergeStrategy: MergeStrategyOverride},
|
|
expected: MergeStrategyOverride,
|
|
},
|
|
{
|
|
name: "both empty stays empty",
|
|
dst: EnvironmentSpec{},
|
|
src: EnvironmentSpec{},
|
|
expected: "",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dst := map[string]EnvironmentSpec{"prod": tt.dst}
|
|
src := map[string]EnvironmentSpec{"prod": tt.src}
|
|
mergeEnvironments(dst, src)
|
|
if got := dst["prod"].MergeStrategy; got != tt.expected {
|
|
t.Errorf("MergeStrategy after merge: want %q, got %q", tt.expected, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|