diff --git a/pkg/state/kube_context_chartify_test.go b/pkg/state/kube_context_chartify_test.go new file mode 100644 index 00000000..0a5e5fc4 --- /dev/null +++ b/pkg/state/kube_context_chartify_test.go @@ -0,0 +1,275 @@ +package state + +import ( + "strings" + "testing" + + "github.com/helmfile/chartify" + "github.com/stretchr/testify/assert" + + "github.com/helmfile/helmfile/pkg/environment" +) + +// TestProcessChartification_TemplateArgsConstruction tests that when +// --dry-run=server is added for cluster-requiring commands (like diff), +// the --kube-context flag is also included in TemplateArgs. +// This is a regression test for the issue where helm template does not receive +// --kube-context when kustomize (jsonPatches) is used. +func TestProcessChartification_TemplateArgsConstruction(t *testing.T) { + tests := []struct { + name string + helmfileCommand string + helmDefaults HelmSpec + envKubeContext string + releaseContext string + expectDryRun bool + expectKubeCtx bool + expectedContext string + }{ + { + name: "diff command with helmDefaults kubeContext", + helmfileCommand: "diff", + helmDefaults: HelmSpec{ + KubeContext: "minikube", + }, + expectDryRun: true, + expectKubeCtx: true, + expectedContext: "minikube", + }, + { + name: "apply command with helmDefaults kubeContext", + helmfileCommand: "apply", + helmDefaults: HelmSpec{ + KubeContext: "production", + }, + expectDryRun: true, + expectKubeCtx: true, + expectedContext: "production", + }, + { + name: "sync command with environment kubeContext", + helmfileCommand: "sync", + envKubeContext: "staging", + expectDryRun: true, + expectKubeCtx: true, + expectedContext: "staging", + }, + { + name: "diff command with release kubeContext", + helmfileCommand: "diff", + releaseContext: "dev-cluster", + expectDryRun: true, + expectKubeCtx: true, + expectedContext: "dev-cluster", + }, + { + name: "template command should not add dry-run or kube-context", + helmfileCommand: "template", + helmDefaults: HelmSpec{ + KubeContext: "minikube", + }, + expectDryRun: false, + expectKubeCtx: false, + }, + { + name: "build command should not add dry-run or kube-context", + helmfileCommand: "build", + helmDefaults: HelmSpec{ + KubeContext: "minikube", + }, + expectDryRun: false, + expectKubeCtx: false, + }, + { + name: "release context takes precedence over helm defaults", + helmfileCommand: "diff", + helmDefaults: HelmSpec{ + KubeContext: "default-context", + }, + releaseContext: "release-context", + expectDryRun: true, + expectKubeCtx: true, + expectedContext: "release-context", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup state + st := &HelmState{ + basePath: "/test/path", + ReleaseSetSpec: ReleaseSetSpec{ + DefaultHelmBinary: "helm", + HelmDefaults: tt.helmDefaults, + }, + logger: logger, + } + + // Setup environment if needed + if tt.envKubeContext != "" { + st.Env = environment.Environment{Name: "test"} + st.Environments = map[string]EnvironmentSpec{ + "test": { + KubeContext: tt.envKubeContext, + }, + } + } else { + st.Env = environment.Environment{Name: "default"} + st.Environments = map[string]EnvironmentSpec{ + "default": {}, + } + } + + // Setup release + release := &ReleaseSpec{ + Name: "test-release", + Namespace: "default", + Chart: "test/chart", + } + if tt.releaseContext != "" { + release.KubeContext = tt.releaseContext + } + + // Setup chartifyOpts (this simulates what processChartification does) + chartifyOpts := &chartify.ChartifyOpts{ + Namespace: "default", + } + + // Simulate the logic from processChartification for setting TemplateArgs + var requiresCluster bool + switch tt.helmfileCommand { + case "diff", "apply", "sync", "destroy", "delete", "test", "status": + requiresCluster = true + case "template", "lint", "build", "pull", "fetch", "write-values", "list", "show-dag", "deps", "repos", "cache", "init", "completion", "help", "version": + requiresCluster = false + default: + requiresCluster = true + } + + if requiresCluster { + if chartifyOpts.TemplateArgs == "" { + chartifyOpts.TemplateArgs = "--dry-run=server" + } else if !strings.Contains(chartifyOpts.TemplateArgs, "--dry-run") { + chartifyOpts.TemplateArgs += " --dry-run=server" + } + // This is the fix being tested + kubeContextFlags := st.kubeConnectionFlags(release) + for i := 0; i < len(kubeContextFlags); i += 2 { + flag := kubeContextFlags[i] + value := kubeContextFlags[i+1] + if !strings.Contains(chartifyOpts.TemplateArgs, flag) { + chartifyOpts.TemplateArgs += " " + flag + " " + value + } + } + } + + // Verify TemplateArgs contains expected flags + templateArgs := chartifyOpts.TemplateArgs + + if tt.expectDryRun { + assert.Contains(t, templateArgs, "--dry-run=server", + "TemplateArgs should contain --dry-run=server for command: %s", tt.helmfileCommand) + } else { + assert.NotContains(t, templateArgs, "--dry-run", + "TemplateArgs should not contain --dry-run for command: %s", tt.helmfileCommand) + } + + if tt.expectKubeCtx { + assert.Contains(t, templateArgs, "--kube-context", + "TemplateArgs should contain --kube-context for command: %s", tt.helmfileCommand) + assert.Contains(t, templateArgs, tt.expectedContext, + "TemplateArgs should contain context %s for command: %s", tt.expectedContext, tt.helmfileCommand) + + // Verify the format is correct: "--kube-context " + parts := strings.Split(templateArgs, " ") + foundContext := false + for i, part := range parts { + if part == "--kube-context" && i+1 < len(parts) { + assert.Equal(t, tt.expectedContext, parts[i+1], + "kube-context value should be %s", tt.expectedContext) + foundContext = true + break + } + } + assert.True(t, foundContext, "Should find --kube-context flag with value") + } else { + assert.NotContains(t, templateArgs, "--kube-context", + "TemplateArgs should not contain --kube-context for command: %s", tt.helmfileCommand) + } + }) + } +} + +// TestKubeConnectionFlags tests the kubeConnectionFlags function +// to ensure it properly returns the kube-context flag based on +// release, environment, or helm defaults priority. +func TestKubeConnectionFlags(t *testing.T) { + tests := []struct { + name string + release *ReleaseSpec + envKubeContext string + helmDefaults HelmSpec + expected []string + }{ + { + name: "release kube context takes precedence", + release: &ReleaseSpec{ + KubeContext: "release-context", + }, + envKubeContext: "env-context", + helmDefaults: HelmSpec{ + KubeContext: "default-context", + }, + expected: []string{"--kube-context", "release-context"}, + }, + { + name: "environment kube context used when no release context", + release: &ReleaseSpec{}, + envKubeContext: "env-context", + helmDefaults: HelmSpec{ + KubeContext: "default-context", + }, + expected: []string{"--kube-context", "env-context"}, + }, + { + name: "helm defaults kube context used when no release or env context", + release: &ReleaseSpec{}, + helmDefaults: HelmSpec{ + KubeContext: "default-context", + }, + expected: []string{"--kube-context", "default-context"}, + }, + { + name: "no kube context returns empty slice", + release: &ReleaseSpec{}, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + st := &HelmState{ + ReleaseSetSpec: ReleaseSetSpec{ + HelmDefaults: tt.helmDefaults, + }, + } + + if tt.envKubeContext != "" { + st.Env = environment.Environment{Name: "test"} + st.Environments = map[string]EnvironmentSpec{ + "test": { + KubeContext: tt.envKubeContext, + }, + } + } else { + st.Env = environment.Environment{Name: "default"} + st.Environments = map[string]EnvironmentSpec{ + "default": {}, + } + } + + got := st.kubeConnectionFlags(tt.release) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/state/state.go b/pkg/state/state.go index 634f1e6a..6171bdb1 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -1517,6 +1517,17 @@ func (st *HelmState) processChartification(chartification *Chartify, release *Re } else if !strings.Contains(chartifyOpts.TemplateArgs, "--dry-run") { chartifyOpts.TemplateArgs += " --dry-run=server" } + // When using --dry-run=server, we need to include --kube-context to ensure + // Helm connects to the correct cluster (helmDefaults.kubeContext) + // See: https://github.com/helmfile/helmfile/issues/XXXX + kubeContextFlags := st.kubeConnectionFlags(release) + for i := 0; i < len(kubeContextFlags); i += 2 { + flag := kubeContextFlags[i] + value := kubeContextFlags[i+1] + if !strings.Contains(chartifyOpts.TemplateArgs, flag) { + chartifyOpts.TemplateArgs += fmt.Sprintf(" %s %s", flag, value) + } + } } out, err := c.Chartify(release.Name, chartPath, chartify.WithChartifyOpts(chartifyOpts))