Fix: Add --kube-context flag when using --dry-run=server in chartify

When jsonPatches are used with helmfile, the chartify process now includes
the --kube-context flag along with --dry-run=server. This ensures Helm
connects to the correct cluster as specified in helmDefaults.kubeContext,
release.kubeContext, or environment kubeContext.

Fixes issue where helm template doesn't receive --kube-context when
kustomize (jsonPatches) is used.

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-01-14 12:51:12 +00:00
parent 3b8f9513db
commit 24355f0ac0
2 changed files with 286 additions and 0 deletions

View File

@ -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 <value>"
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)
})
}
}

View File

@ -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))