Files
helmfile/pkg/app/app_list_test.go
T
yxxhero df01afbbeb fix: helmfile list now reflects version from helmfile.lock (#2486)
* fix: helmfile list now reflects version from helmfile.lock

The list command now resolves locked dependencies before returning
release information, ensuring the version field reflects the pinned
version from helmfile.lock when present.

Fixes #1953

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

* fix: address PR review comments

- Remove redundant maps.Copy in list() - labels already merged by GetReleasesWithLabels()
- Fix default lockfile path to use basePath for multi-file mode
- Update test to expect basePath-joined lockfile path
- Add multi-file test for lockfile resolution in helmfile.d directory

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

* fix more test

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

* fix tests

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

* fix: propagate errors instead of panic in list()

When skipCharts=false, errors from list() now properly propagate instead
of causing a crash. Uses a closure variable to capture the error and
propagates it after withPreparedCharts completes.

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

* fix tests

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

* fix tests

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

* fix tests

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

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
2026-03-19 14:25:03 +08:00

462 lines
14 KiB
Go

package app
import (
"bytes"
"encoding/json"
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/helmfile/vals"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
ffs "github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/testhelper"
"github.com/helmfile/helmfile/pkg/testutil"
)
func testListWithEnvironment(t *testing.T, cfg configImpl) {
type testcase struct {
environment string
ns string
error string
selectors []string
expected string
}
check := func(t *testing.T, tc testcase, cfg configImpl) {
t.Helper()
bs := runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
t.Helper()
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Errorf("unexpected error creating vals runtime: %v", err)
}
files := map[string]string{
"/path/to/helmfile.d/helmfile_1.yaml": `
environments:
development: {}
shared: {}
---
releases:
- name: logging
chart: incubator/raw
namespace: kube-system
- name: kubernetes-external-secrets
chart: incubator/raw
namespace: kube-system
needs:
- kube-system/logging
- name: external-secrets
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- kube-system/kubernetes-external-secrets
- name: my-release
chart: incubator/raw
namespace: default
labels:
app: test
needs:
- default/external-secrets
# Disabled releases are treated as missing
- name: disabled
chart: incubator/raw
namespace: kube-system
installed: false
- name: test2
chart: incubator/raw
needs:
- kube-system/disabled
- name: test3
chart: incubator/raw
needs:
- test2
`,
"/path/to/helmfile.d/helmfile_2.yaml": `
environments:
test: {}
shared: {}
---
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: cache
namespace: my-app
chart: bitnami/redis
version: 17.0.7
labels:
app: test
- name: database
namespace: my-app
chart: bitnami/postgres
version: 11.6.22
`,
"/path/to/helmfile.d/helmfile_3.yaml": `
releases:
- name: global
chart: incubator/raw
namespace: kube-system
`,
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: tc.environment,
Logger: logger,
valsRuntime: valsRuntime,
}, files)
expectNoCallsToHelm(app)
if tc.ns != "" {
app.Namespace = tc.ns
}
if tc.selectors != nil {
app.Selectors = tc.selectors
}
var listErr error
out, err := testutil.CaptureStdout(func() {
listErr = app.ListReleases(cfg)
})
assert.NoError(t, err)
var gotErr string
if listErr != nil {
gotErr = listErr.Error()
}
if d := cmp.Diff(tc.error, gotErr); d != "" {
t.Fatalf("unexpected error: want (-), got (+): %s", d)
}
assert.Equal(t, tc.expected, out)
})
testhelper.RequireLog(t, "app_list_test", bs)
}
t.Run("default environment includes all releases", func(t *testing.T) {
check(t, testcase{
environment: "default",
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
test2 true true chart:raw,name:test2,namespace: incubator/raw
test3 true true chart:raw,name:test3,namespace: incubator/raw
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
disabled kube-system true false chart:raw,name:disabled,namespace:kube-system incubator/raw
global kube-system true true chart:raw,name:global,namespace:kube-system incubator/raw
kubernetes-external-secrets kube-system true true chart:raw,name:kubernetes-external-secrets,namespace:kube-system incubator/raw
logging kube-system true true chart:raw,name:logging,namespace:kube-system incubator/raw
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
database my-app true true chart:postgres,name:database,namespace:my-app bitnami/postgres 11.6.22
`,
}, cfg)
})
t.Run("fail on unknown environment", func(t *testing.T) {
check(t, testcase{
environment: "staging",
error: `err: no releases found that matches specified selector() and environment(staging), in any helmfile`,
}, cfg)
})
t.Run("list releases matching selector and environment", func(t *testing.T) {
check(t, testcase{
environment: "development",
selectors: []string{"app=test"},
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
`,
}, cfg)
})
t.Run("filters releases for environment used in one file only", func(t *testing.T) {
check(t, testcase{
environment: "test",
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
database my-app true true chart:postgres,name:database,namespace:my-app bitnami/postgres 11.6.22
`,
}, cfg)
})
t.Run("filters releases for environment used in multiple files", func(t *testing.T) {
check(t, testcase{
environment: "shared",
// 'global' release has no environments, so is still excluded
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
test2 true true chart:raw,name:test2,namespace: incubator/raw
test3 true true chart:raw,name:test3,namespace: incubator/raw
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
disabled kube-system true false chart:raw,name:disabled,namespace:kube-system incubator/raw
kubernetes-external-secrets kube-system true true chart:raw,name:kubernetes-external-secrets,namespace:kube-system incubator/raw
logging kube-system true true chart:raw,name:logging,namespace:kube-system incubator/raw
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
database my-app true true chart:postgres,name:database,namespace:my-app bitnami/postgres 11.6.22
`,
}, cfg)
})
}
func TestListWithEnvironment(t *testing.T) {
t.Run("with skipCharts=false", func(t *testing.T) {
testListWithEnvironment(t, configImpl{skipCharts: false})
})
t.Run("with skipCharts=true", func(t *testing.T) {
testListWithEnvironment(t, configImpl{skipCharts: true})
})
}
func testListWithJSONOutput(t *testing.T, cfg configImpl) {
cfg.output = "json"
files := map[string]string{
"/path/to/helmfile.d/first.yaml": `
environments:
default:
values:
- myrelease2:
enabled: false
---
releases:
- name: myrelease1
chart: mychart1
installed: false
labels:
id: myrelease1
- name: myrelease2
chart: mychart1
condition: myrelease2.enabled
`,
"/path/to/helmfile.d/second.yaml": `
releases:
- name: myrelease3
chart: mychart1
installed: true
- name: myrelease4
chart: mychart1
labels:
id: myrelease1
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
Namespace: "testNamespace",
}, files)
expectNoCallsToHelm(app)
out, err := testutil.CaptureStdout(func() {
err := app.ListReleases(cfg)
assert.Nil(t, err)
})
assert.NoError(t, err)
expected := `[{"name":"myrelease1","namespace":"testNamespace","enabled":true,"installed":false,"labels":"chart:mychart1,id:myrelease1,name:myrelease1,namespace:testNamespace","chart":"mychart1","version":""},{"name":"myrelease2","namespace":"testNamespace","enabled":false,"installed":true,"labels":"chart:mychart1,name:myrelease2,namespace:testNamespace","chart":"mychart1","version":""},{"name":"myrelease3","namespace":"testNamespace","enabled":true,"installed":true,"labels":"chart:mychart1,name:myrelease3,namespace:testNamespace","chart":"mychart1","version":""},{"name":"myrelease4","namespace":"testNamespace","enabled":true,"installed":true,"labels":"chart:mychart1,id:myrelease1,name:myrelease4,namespace:testNamespace","chart":"mychart1","version":""}]
`
assert.Equal(t, expected, out)
}
func TestListWithJSONOutput(t *testing.T) {
t.Run("with skipCharts=false", func(t *testing.T) {
testListWithJSONOutput(t, configImpl{skipCharts: false})
})
t.Run("with skipCharts=true", func(t *testing.T) {
testListWithJSONOutput(t, configImpl{skipCharts: true})
})
}
func TestListWithLockFileVersion(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.yaml": `
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: redis
namespace: default
chart: bitnami/redis
version: ">=1.0.0"
`,
"/path/to/helmfile.lock": `version: v0.0.0
digest: sha256:abc123
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: redis
repository: https://charts.bitnami.com/bitnami
version: 17.0.7
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
valsRuntime: valsRuntime,
}, files)
expectNoCallsToHelm(app)
out, err := testutil.CaptureStdout(func() {
err := app.ListReleases(configImpl{skipCharts: true, output: "json"})
assert.Nil(t, err)
})
assert.NoError(t, err)
var releases []HelmRelease
if err := json.Unmarshal([]byte(out), &releases); err != nil {
t.Fatalf("failed to parse JSON output: %v", err)
}
assert.Len(t, releases, 1, "expected 1 release")
assert.Equal(t, "redis", releases[0].Name)
assert.Equal(t, "bitnami/redis", releases[0].Chart)
assert.Equal(t, "17.0.7", releases[0].Version, "expected version from helmfile.lock")
}
func TestListWithLockFileVersion_MultiFile(t *testing.T) {
files := map[string]string{
"/path/to/helmfile.d/first.yaml": `
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: redis
namespace: default
chart: bitnami/redis
version: ">=1.0.0"
`,
"/path/to/helmfile.d/first.lock": `version: v0.0.0
digest: sha256:abc123
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: redis
repository: https://charts.bitnami.com/bitnami
version: 17.0.7
`,
"/path/to/helmfile.d/second.yaml": `
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: nginx
namespace: default
chart: bitnami/nginx
version: ">=1.0.0"
`,
"/path/to/helmfile.d/second.lock": `version: v0.0.0
digest: sha256:def456
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: nginx
repository: https://charts.bitnami.com/bitnami
version: 15.0.0
`,
}
stdout := os.Stdout
defer func() { os.Stdout = stdout }()
var buffer bytes.Buffer
syncWriter := testhelper.NewSyncWriter(&buffer)
logger := helmexec.NewLogger(syncWriter, "debug")
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
if err != nil {
t.Fatalf("unexpected error creating vals runtime: %v", err)
}
app := appWithFs(&App{
OverrideHelmBinary: DefaultHelmBinary,
fs: ffs.DefaultFileSystem(),
OverrideKubeContext: "default",
DisableKubeVersionAutoDetection: true,
Env: "default",
Logger: logger,
valsRuntime: valsRuntime,
}, files)
expectNoCallsToHelm(app)
out, err := testutil.CaptureStdout(func() {
err := app.ListReleases(configImpl{skipCharts: true, output: "json"})
assert.Nil(t, err)
})
assert.NoError(t, err)
var releases []HelmRelease
if err := json.Unmarshal([]byte(out), &releases); err != nil {
t.Fatalf("failed to parse JSON output: %v", err)
}
assert.Len(t, releases, 2, "expected 2 releases")
releaseMap := make(map[string]HelmRelease)
for _, r := range releases {
releaseMap[r.Name] = r
}
redis := releaseMap["redis"]
assert.Equal(t, "bitnami/redis", redis.Chart)
assert.Equal(t, "17.0.7", redis.Version, "expected redis version from first.lock")
nginx := releaseMap["nginx"]
assert.Equal(t, "bitnami/nginx", nginx.Chart)
assert.Equal(t, "15.0.0", nginx.Version, "expected nginx version from second.lock")
}