503 lines
14 KiB
Go
503 lines
14 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/helmfile/vals"
|
|
|
|
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"
|
|
)
|
|
|
|
// TestSequentialHelmfilesNoChdirCalled verifies that sequential processing
|
|
// does NOT call os.Chdir(), which was the root cause of issue #2409.
|
|
func TestSequentialHelmfilesNoChdirCalled(t *testing.T) {
|
|
files := map[string]string{
|
|
"/path/to/helmfile.d/01-first.yaml": `
|
|
releases:
|
|
- name: first-release
|
|
chart: stable/chart-a
|
|
namespace: default
|
|
`,
|
|
"/path/to/helmfile.d/02-second.yaml": `
|
|
releases:
|
|
- name: second-release
|
|
chart: stable/chart-b
|
|
namespace: default
|
|
`,
|
|
}
|
|
|
|
testFs := testhelper.NewTestFs(files)
|
|
|
|
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error creating vals runtime: %v", err)
|
|
}
|
|
|
|
app := &App{
|
|
OverrideHelmBinary: DefaultHelmBinary,
|
|
OverrideKubeContext: "default",
|
|
DisableKubeVersionAutoDetection: true,
|
|
Env: "default",
|
|
Logger: newAppTestLogger(),
|
|
valsRuntime: valsRuntime,
|
|
FileOrDir: "/path/to/helmfile.d",
|
|
SequentialHelmfiles: true,
|
|
}
|
|
|
|
app = injectFs(app, testFs)
|
|
expectNoCallsToHelm(app)
|
|
|
|
err = app.ForEachState(
|
|
Noop,
|
|
false,
|
|
SetFilter(true),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if testFs.ChdirCalls != 0 {
|
|
t.Errorf("expected 0 Chdir calls in sequential mode, got %d", testFs.ChdirCalls)
|
|
}
|
|
}
|
|
|
|
// TestSequentialHelmfilesProcessesAllFiles verifies all files in helmfile.d
|
|
// are processed when using sequential mode.
|
|
func TestSequentialHelmfilesProcessesAllFiles(t *testing.T) {
|
|
files := map[string]string{
|
|
"/path/to/helmfile.d/001-app.yaml": `
|
|
releases:
|
|
- name: app1
|
|
chart: stable/app1
|
|
namespace: default
|
|
`,
|
|
"/path/to/helmfile.d/002-db.yaml": `
|
|
releases:
|
|
- name: db1
|
|
chart: stable/postgresql
|
|
namespace: default
|
|
`,
|
|
"/path/to/helmfile.d/003-cache.yaml": `
|
|
releases:
|
|
- name: cache1
|
|
chart: stable/redis
|
|
namespace: default
|
|
`,
|
|
}
|
|
|
|
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,
|
|
FileOrDir: "/path/to/helmfile.d",
|
|
SequentialHelmfiles: true,
|
|
}, files)
|
|
|
|
expectNoCallsToHelm(app)
|
|
|
|
out, err := testutil.CaptureStdout(func() {
|
|
err := app.ListReleases(configImpl{
|
|
skipCharts: false,
|
|
output: "json",
|
|
})
|
|
if err != nil {
|
|
t.Logf("ListReleases error: %v", err)
|
|
}
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error capturing output: %v", err)
|
|
}
|
|
|
|
if !bytes.Contains([]byte(out), []byte("app1")) {
|
|
t.Errorf("app1 release not found in output:\n%s", out)
|
|
}
|
|
if !bytes.Contains([]byte(out), []byte("db1")) {
|
|
t.Errorf("db1 release not found in output:\n%s", out)
|
|
}
|
|
if !bytes.Contains([]byte(out), []byte("cache1")) {
|
|
t.Errorf("cache1 release not found in output:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// TestSequentialHelmfilesAlphabeticalOrder verifies sequential mode processes
|
|
// files in alphabetical order.
|
|
func TestSequentialHelmfilesAlphabeticalOrder(t *testing.T) {
|
|
files := map[string]string{
|
|
"/path/to/helmfile.d/z-last.yaml": `
|
|
releases:
|
|
- name: zulu-release
|
|
chart: stable/chart-z
|
|
namespace: ns-z
|
|
`,
|
|
"/path/to/helmfile.d/a-first.yaml": `
|
|
releases:
|
|
- name: alpha-release
|
|
chart: stable/chart-a
|
|
namespace: ns-a
|
|
`,
|
|
"/path/to/helmfile.d/m-middle.yaml": `
|
|
releases:
|
|
- name: mike-release
|
|
chart: stable/chart-m
|
|
namespace: ns-m
|
|
`,
|
|
}
|
|
|
|
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: newAppTestLogger(),
|
|
valsRuntime: valsRuntime,
|
|
FileOrDir: "/path/to/helmfile.d",
|
|
SequentialHelmfiles: true,
|
|
}, files)
|
|
|
|
expectNoCallsToHelm(app)
|
|
|
|
var actualOrder []string
|
|
noop := func(run *Run) (bool, []error) {
|
|
actualOrder = append(actualOrder, run.state.FilePath)
|
|
return false, []error{}
|
|
}
|
|
|
|
err = app.ForEachState(
|
|
noop,
|
|
false,
|
|
SetFilter(true),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
expectedOrder := []string{"/path/to/helmfile.d/a-first.yaml", "/path/to/helmfile.d/m-middle.yaml", "/path/to/helmfile.d/z-last.yaml"}
|
|
if !reflect.DeepEqual(actualOrder, expectedOrder) {
|
|
t.Errorf("unexpected order of processed state files: expected=%v, actual=%v", expectedOrder, actualOrder)
|
|
}
|
|
}
|
|
|
|
// TestSequentialHelmfilesMatchesParallelResults verifies that sequential and
|
|
// parallel modes produce the same set of releases.
|
|
func TestSequentialHelmfilesMatchesParallelResults(t *testing.T) {
|
|
files := map[string]string{
|
|
"/path/to/helmfile.d/01-app.yaml": `
|
|
releases:
|
|
- name: app-release
|
|
chart: stable/app
|
|
namespace: default
|
|
`,
|
|
"/path/to/helmfile.d/02-db.yaml": `
|
|
releases:
|
|
- name: db-release
|
|
chart: stable/postgresql
|
|
namespace: default
|
|
`,
|
|
}
|
|
|
|
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error creating vals runtime: %v", err)
|
|
}
|
|
|
|
// Run in parallel mode (default)
|
|
parallelOut, err := testutil.CaptureStdout(func() {
|
|
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,
|
|
valsRuntime: valsRuntime,
|
|
FileOrDir: "/path/to/helmfile.d",
|
|
SequentialHelmfiles: false,
|
|
}, files)
|
|
expectNoCallsToHelm(app)
|
|
|
|
if err := app.ListReleases(configImpl{skipCharts: false, output: "json"}); err != nil {
|
|
t.Logf("parallel ListReleases error: %v", err)
|
|
}
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error capturing parallel output: %v", err)
|
|
}
|
|
|
|
// Run in sequential mode
|
|
sequentialOut, err := testutil.CaptureStdout(func() {
|
|
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,
|
|
valsRuntime: valsRuntime,
|
|
FileOrDir: "/path/to/helmfile.d",
|
|
SequentialHelmfiles: true,
|
|
}, files)
|
|
expectNoCallsToHelm(app)
|
|
|
|
if err := app.ListReleases(configImpl{skipCharts: false, output: "json"}); err != nil {
|
|
t.Logf("sequential ListReleases error: %v", err)
|
|
}
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error capturing sequential output: %v", err)
|
|
}
|
|
|
|
// Both modes should contain the same releases
|
|
for _, name := range []string{"app-release", "db-release"} {
|
|
if !bytes.Contains([]byte(parallelOut), []byte(name)) {
|
|
t.Errorf("parallel output missing release %q:\n%s", name, parallelOut)
|
|
}
|
|
if !bytes.Contains([]byte(sequentialOut), []byte(name)) {
|
|
t.Errorf("sequential output missing release %q:\n%s", name, sequentialOut)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSequentialHelmfilesWithUndefinedEnv verifies that files with undefined
|
|
// environments are skipped gracefully in sequential mode.
|
|
func TestSequentialHelmfilesWithUndefinedEnv(t *testing.T) {
|
|
files := map[string]string{
|
|
"/path/to/helmfile.d/01-has-prod.yaml": `
|
|
environments:
|
|
prod: {}
|
|
---
|
|
releases:
|
|
- name: prod-release
|
|
chart: stable/prod
|
|
namespace: default
|
|
`,
|
|
"/path/to/helmfile.d/02-no-prod.yaml": `
|
|
environments:
|
|
staging: {}
|
|
---
|
|
releases:
|
|
- name: staging-release
|
|
chart: stable/staging
|
|
namespace: default
|
|
`,
|
|
}
|
|
|
|
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: "prod",
|
|
Logger: newAppTestLogger(),
|
|
valsRuntime: valsRuntime,
|
|
FileOrDir: "/path/to/helmfile.d",
|
|
SequentialHelmfiles: true,
|
|
}, files)
|
|
|
|
expectNoCallsToHelm(app)
|
|
|
|
out, err := testutil.CaptureStdout(func() {
|
|
err := app.ListReleases(configImpl{
|
|
skipCharts: false,
|
|
output: "json",
|
|
})
|
|
if err != nil {
|
|
t.Logf("ListReleases error: %v", err)
|
|
}
|
|
})
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error capturing output: %v", err)
|
|
}
|
|
|
|
// The prod-release should be present
|
|
if !bytes.Contains([]byte(out), []byte("prod-release")) {
|
|
t.Errorf("prod-release not found in output:\n%s", out)
|
|
}
|
|
|
|
// The staging-release should NOT be present (env "prod" not defined in that file)
|
|
if bytes.Contains([]byte(out), []byte("staging-release")) {
|
|
t.Errorf("staging-release should have been skipped but was found in output:\n%s", out)
|
|
}
|
|
}
|
|
|
|
// TestSequentialHelmfilesConvergeErrorPropagated verifies that errors returned
|
|
// from the converge function are properly propagated in sequential mode.
|
|
func TestSequentialHelmfilesConvergeErrorPropagated(t *testing.T) {
|
|
files := map[string]string{
|
|
"/path/to/helmfile.d/01-first.yaml": `
|
|
releases:
|
|
- name: first-release
|
|
chart: stable/chart-a
|
|
namespace: default
|
|
`,
|
|
"/path/to/helmfile.d/02-second.yaml": `
|
|
releases:
|
|
- name: second-release
|
|
chart: stable/chart-b
|
|
namespace: default
|
|
`,
|
|
}
|
|
|
|
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: newAppTestLogger(),
|
|
valsRuntime: valsRuntime,
|
|
FileOrDir: "/path/to/helmfile.d",
|
|
SequentialHelmfiles: true,
|
|
}, files)
|
|
|
|
expectNoCallsToHelm(app)
|
|
|
|
convergeErr := fmt.Errorf("simulated converge failure")
|
|
failingConverge := func(_ *Run) (bool, []error) {
|
|
return false, []error{convergeErr}
|
|
}
|
|
|
|
err = app.ForEachState(
|
|
failingConverge,
|
|
false,
|
|
SetFilter(true),
|
|
)
|
|
|
|
if err == nil {
|
|
t.Fatal("expected error from ForEachState, got nil")
|
|
}
|
|
|
|
if !bytes.Contains([]byte(err.Error()), []byte("simulated converge failure")) {
|
|
t.Errorf("expected error to contain 'simulated converge failure', got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestSequentialHelmfilesValuesFileResolution verifies that relative values file
|
|
// paths (e.g., ../config/myapp/values.yaml) are resolved correctly in sequential
|
|
// mode. This is a regression test for issue #2424 where a relative baseDir caused
|
|
// values paths to be resolved incorrectly.
|
|
func TestSequentialHelmfilesValuesFileResolution(t *testing.T) {
|
|
files := map[string]string{
|
|
"/path/to/helmfile.d/01-app.yaml": `
|
|
releases:
|
|
- name: myapp
|
|
chart: stable/myapp
|
|
namespace: default
|
|
values:
|
|
- ../config/myapp/values.yaml
|
|
`,
|
|
"/path/to/helmfile.d/02-other.yaml": `
|
|
releases:
|
|
- name: other
|
|
chart: stable/other
|
|
namespace: default
|
|
`,
|
|
"/path/to/config/myapp/values.yaml": `
|
|
replicaCount: 3
|
|
`,
|
|
}
|
|
|
|
testFs := testhelper.NewTestFs(files)
|
|
|
|
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error creating vals runtime: %v", err)
|
|
}
|
|
|
|
app := &App{
|
|
OverrideHelmBinary: DefaultHelmBinary,
|
|
OverrideKubeContext: "default",
|
|
DisableKubeVersionAutoDetection: true,
|
|
Env: "default",
|
|
Logger: newAppTestLogger(),
|
|
valsRuntime: valsRuntime,
|
|
FileOrDir: "/path/to/helmfile.d",
|
|
SequentialHelmfiles: true,
|
|
}
|
|
|
|
app = injectFs(app, testFs)
|
|
expectNoCallsToHelm(app)
|
|
|
|
var stateFilePaths []string
|
|
captureState := func(run *Run) (bool, []error) {
|
|
stateFilePaths = append(stateFilePaths, run.state.FilePath)
|
|
return false, []error{}
|
|
}
|
|
|
|
err = app.ForEachState(
|
|
captureState,
|
|
false,
|
|
SetFilter(true),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify that state FilePaths are absolute (which means basePath is also
|
|
// absolute, ensuring relative values paths like ../config/myapp/values.yaml
|
|
// resolve correctly). Before the fix, a relative baseDir was passed causing
|
|
// values path resolution to fail.
|
|
for _, fp := range stateFilePaths {
|
|
if !strings.HasPrefix(fp, "/") {
|
|
t.Errorf("expected absolute FilePath, got relative: %s", fp)
|
|
}
|
|
}
|
|
|
|
// Verify the values file was found and can be read (proving correct path resolution).
|
|
// With an absolute basePath, ../config/myapp/values.yaml resolves to
|
|
// /path/to/config/myapp/values.yaml which exists in our test filesystem.
|
|
valuesPath := "/path/to/config/myapp/values.yaml"
|
|
content, err := testFs.ToFileSystem().ReadFile(valuesPath)
|
|
if err != nil {
|
|
t.Fatalf("values file should be readable at %s: %v", valuesPath, err)
|
|
}
|
|
if !strings.Contains(string(content), "replicaCount") {
|
|
t.Errorf("values file content mismatch: %s", string(content))
|
|
}
|
|
}
|