mirror of
https://github.com/helmfile/helmfile.git
synced 2026-09-30 14:22:56 +02:00
* 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>
462 lines
14 KiB
Go
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")
|
|
}
|