mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-07 09:08:17 +02:00
* fix: eliminate os.Chdir in sequential helmfiles to fix relative path resolution The sequential code path used within() → os.Chdir() to change the process-wide working directory when processing helmfile.d files. This broke relative environment variable paths (e.g. KUBECONFIG=kubeconfig.yaml) because they resolved from the wrong directory after chdir. Replace the chdir-based approach with the same baseDir parameter pattern used by the parallel code path, passing explicit directory context through loadDesiredStateFromYamlWithBaseDir() instead of mutating global process state. Closes #2409 Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * fix: restore within() for single-file sequential to preserve chart path format The previous approach used baseDir for all sequential processing, which changed chart path format in output (e.g. from "../../../../charts/raw" to "test/integration/charts/raw"). This broke integration tests that compare chart paths in expected output. Now the sequential branch uses two strategies: - Single file: use os.Chdir via within() to preserve backward-compatible relative chart paths in output - Multiple files with --sequential-helmfiles: use baseDir parameter to avoid os.Chdir, fixing relative env var paths like KUBECONFIG (#2409) Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * fix: revert e2e snapshot outputs to match within() behavior The previous commit restored within() for single-file sequential processing, which produces relative chart paths (e.g. ../../charts/raw) and filename-only FilePath. Revert the e2e snapshot expected outputs to match main branch since single-file behavior is now identical. Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * fix: restructure integration test for multi-file sequential processing - Point -f at helmfile.d/ directly (not parent dir) so findDesiredStateFiles discovers the yaml files - Add second helmfile to trigger baseDir path (len > 1) - Inline environment config to avoid base file relative path issues - Verify both releases appear in output instead of comparing with parallel (which may differ in ordering) Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * fix: reduce cognitive complexity and improve accuracy of sequential helmfiles Replace inline visitSubHelmfiles closure with calls to the existing processNestedHelmfiles() method, matching the parallel path. This eliminates duplicated nested logic and reduces gocognit complexity below the CI threshold of 110. Also fixes help text and docs to accurately describe that single-file processing still uses within(), and adds kubeContext verification to the integration test. Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> * test: validate kubeContext resolution in sequential helmfiles integration test Restructure the integration test to replicate the exact user scenario from issue #2409: - Multiple files in helmfile.d/ using bases: with relative paths (../bases/) for environments and defaults - Environment values set kubeContext via .Environment.Values - helmDefaults.kubeContext rendered from gotmpl - Local chart references (../../../../charts/raw) from helmfile.d/ - Run diff against the minikube cluster to exercise kubeContext resolution, which would fail with "context does not exist" if os.Chdir() broke relative path resolution - Also verify template output for both releases and relative values file (values/common.yaml) resolution Fix normalizeChart() in util.go to be idempotent — skip re-prefixing when the chart path already starts with basePath. This prevents double-prefixing of local chart paths (e.g. helmfile.d/test/.../raw) when normalizeChart is called multiple times (once during chart preparation and again during diff/sync). Signed-off-by: Aditya Menon <amenon@canarytechnologies.com> --------- Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>
418 lines
12 KiB
Go
418 lines
12 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"reflect"
|
|
"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)
|
|
}
|
|
}
|