This commit is contained in:
Copilot 2025-12-09 06:36:32 -05:00 committed by GitHub
commit 97b4bdb8b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 332 additions and 5 deletions

2
go.mod
View File

@ -3,6 +3,7 @@ module github.com/helmfile/helmfile
go 1.25.4
require (
al.essio.dev/pkg/shellescape v1.6.0
dario.cat/mergo v1.0.2
github.com/Masterminds/semver/v3 v3.4.0
github.com/Masterminds/sprig/v3 v3.3.0
@ -110,7 +111,6 @@ require (
)
require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect

View File

@ -0,0 +1,289 @@
package state
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/helmfile/helmfile/pkg/environment"
"github.com/helmfile/helmfile/pkg/filesystem"
)
// TestProcessChartificationKubeContext tests that --kube-context flag is properly
// added to chartifyOpts.TemplateArgs when using jsonPatches with cluster-requiring commands.
// This is a regression test for the issue where helm template does not receive
// --kube-context arg when using kustomize (jsonPatches).
func TestProcessChartificationKubeContext(t *testing.T) {
tests := []struct {
name string
helmfileCommand string
kubeContext string
envKubeContext string
helmDefaults string
expectContext bool
expectDryRun bool
}{
{
name: "diff command with helmDefaults kubeContext",
helmfileCommand: "diff",
helmDefaults: "minikube",
expectContext: true,
expectDryRun: true,
},
{
name: "apply command with release kubeContext",
helmfileCommand: "apply",
kubeContext: "prod-cluster",
expectContext: true,
expectDryRun: true,
},
{
name: "sync command with env kubeContext",
helmfileCommand: "sync",
envKubeContext: "staging-cluster",
expectContext: true,
expectDryRun: true,
},
{
name: "template command should not add cluster flags",
helmfileCommand: "template",
helmDefaults: "minikube",
expectContext: false,
expectDryRun: false,
},
{
name: "build command should not add cluster flags",
helmfileCommand: "build",
helmDefaults: "minikube",
expectContext: false,
expectDryRun: false,
},
{
name: "diff command without kubeContext",
helmfileCommand: "diff",
expectContext: false,
expectDryRun: true,
},
{
name: "destroy command with kubeContext",
helmfileCommand: "destroy",
helmDefaults: "test-cluster",
expectContext: true,
expectDryRun: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a release with jsonPatches to trigger chartification
release := &ReleaseSpec{
Name: "test-release",
Namespace: "default",
Chart: "./test-chart",
JSONPatches: []interface{}{
map[string]interface{}{
"target": map[string]interface{}{
"group": "apps",
"version": "v1",
"kind": "Deployment",
"name": "test",
},
"patch": []interface{}{
map[string]interface{}{
"op": "add",
"path": "/spec/template/spec/containers/0/args/-",
"value": "test",
},
},
},
},
}
// Set kubeContext on release if provided
if tt.kubeContext != "" {
release.KubeContext = tt.kubeContext
}
// Create HelmState
state := &HelmState{
basePath: "/tmp/test",
fs: filesystem.FromFileSystem(filesystem.FileSystem{
DirectoryExistsAt: func(path string) bool {
return strings.Contains(path, "test-chart")
},
FileExistsAt: func(path string) bool {
return false
},
DeleteFile: func(path string) error {
return nil
},
Glob: func(pattern string) ([]string, error) {
return nil, nil
},
}),
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
ReleaseSetSpec: ReleaseSetSpec{
Env: environment.Environment{
Name: "default",
},
Environments: map[string]EnvironmentSpec{
"default": {
KubeContext: tt.envKubeContext,
},
},
},
}
// Set helmDefaults kubeContext if provided
if tt.helmDefaults != "" {
state.ReleaseSetSpec.HelmDefaults.KubeContext = tt.helmDefaults
}
// Prepare chartify (this generates the Chartify object with jsonPatches)
chartification, clean, err := state.PrepareChartify(nil, release, "./test-chart", 0)
require.NoError(t, err)
defer clean()
// Ensure chartification is needed (jsonPatches should trigger it)
require.NotNil(t, chartification, "Chartification should be needed when jsonPatches are present")
// Process chartification with the test command
opts := ChartPrepareOptions{}
_, _, err = state.processChartification(chartification, release, "./test-chart", opts, false, tt.helmfileCommand)
// We expect an error because we don't have an actual chart, but we can still
// check that TemplateArgs was set correctly
// The error will come from chartify.Chartify, not from our logic
// So let's check the chartification.Opts.TemplateArgs directly
if tt.expectContext {
// Determine which kubeContext should be used
expectedContext := tt.kubeContext
if expectedContext == "" {
expectedContext = tt.envKubeContext
}
if expectedContext == "" {
expectedContext = tt.helmDefaults
}
assert.Contains(t, chartification.Opts.TemplateArgs, "--kube-context",
"TemplateArgs should contain --kube-context flag")
assert.Contains(t, chartification.Opts.TemplateArgs, expectedContext,
"TemplateArgs should contain the expected kube context: %s", expectedContext)
} else if tt.helmDefaults != "" || tt.kubeContext != "" || tt.envKubeContext != "" {
// If a context is configured but not expected (offline commands)
assert.NotContains(t, chartification.Opts.TemplateArgs, "--kube-context",
"TemplateArgs should not contain --kube-context flag for offline commands")
}
if tt.expectDryRun {
assert.Contains(t, chartification.Opts.TemplateArgs, "--dry-run=server",
"TemplateArgs should contain --dry-run=server flag")
} else {
assert.NotContains(t, chartification.Opts.TemplateArgs, "--dry-run",
"TemplateArgs should not contain --dry-run flag for offline commands")
}
})
}
}
// TestProcessChartificationKubeContextPriority tests the priority order
// for kube context selection: release.KubeContext > env.KubeContext > helmDefaults.KubeContext
func TestProcessChartificationKubeContextPriority(t *testing.T) {
tests := []struct {
name string
releaseContext string
envContext string
helmDefaultsContext string
expectedContext string
}{
{
name: "release context takes priority",
releaseContext: "release-ctx",
envContext: "env-ctx",
helmDefaultsContext: "defaults-ctx",
expectedContext: "release-ctx",
},
{
name: "env context when release not set",
envContext: "env-ctx",
helmDefaultsContext: "defaults-ctx",
expectedContext: "env-ctx",
},
{
name: "helmDefaults context when others not set",
helmDefaultsContext: "defaults-ctx",
expectedContext: "defaults-ctx",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
release := &ReleaseSpec{
Name: "test-release",
Namespace: "default",
Chart: "./test-chart",
JSONPatches: []interface{}{
map[string]interface{}{
"target": map[string]interface{}{
"kind": "Deployment",
},
},
},
}
if tt.releaseContext != "" {
release.KubeContext = tt.releaseContext
}
state := &HelmState{
basePath: "/tmp/test",
fs: filesystem.FromFileSystem(filesystem.FileSystem{
DirectoryExistsAt: func(path string) bool {
return strings.Contains(path, "test-chart")
},
FileExistsAt: func(path string) bool {
return false
},
DeleteFile: func(path string) error {
return nil
},
Glob: func(pattern string) ([]string, error) {
return nil, nil
},
}),
logger: logger,
valsRuntime: valsRuntime,
RenderedValues: map[string]any{},
ReleaseSetSpec: ReleaseSetSpec{
Env: environment.Environment{
Name: "default",
},
Environments: map[string]EnvironmentSpec{
"default": {
KubeContext: tt.envContext,
},
},
HelmDefaults: HelmSpec{
KubeContext: tt.helmDefaultsContext,
},
},
}
chartification, clean, err := state.PrepareChartify(nil, release, "./test-chart", 0)
require.NoError(t, err)
defer clean()
require.NotNil(t, chartification)
opts := ChartPrepareOptions{}
_, _, _ = state.processChartification(chartification, release, "./test-chart", opts, false, "diff")
assert.Contains(t, chartification.Opts.TemplateArgs, tt.expectedContext,
"TemplateArgs should contain the expected context: %s", tt.expectedContext)
})
}
}

View File

@ -20,6 +20,7 @@ import (
"text/template"
"time"
shellescape "al.essio.dev/pkg/shellescape"
"dario.cat/mergo"
"github.com/Masterminds/semver/v3"
"github.com/gofrs/flock"
@ -1421,10 +1422,47 @@ func (st *HelmState) processChartification(chartification *Chartify, release *Re
// for all cluster-requiring operations (diff, apply, sync, etc.) but not for offline
// commands (template, lint, build, etc.)
if requiresCluster {
if chartifyOpts.TemplateArgs == "" {
chartifyOpts.TemplateArgs = "--dry-run=server"
} else if !strings.Contains(chartifyOpts.TemplateArgs, "--dry-run") {
chartifyOpts.TemplateArgs += " --dry-run=server"
// Add --kube-context when a kube context is configured
// This ensures chartify's helm template command uses the correct cluster
var kubeContext string
if release.KubeContext != "" {
kubeContext = release.KubeContext
} else if st.Environments[st.Env.Name].KubeContext != "" {
kubeContext = st.Environments[st.Env.Name].KubeContext
} else if st.HelmDefaults.KubeContext != "" {
kubeContext = st.HelmDefaults.KubeContext
}
if kubeContext != "" {
// Build the template args with proper quoting for the kubeContext value
// Use shellescape to safely quote the context name in case it contains special characters
quotedContext := shellescape.Quote(kubeContext)
if chartifyOpts.TemplateArgs == "" {
chartifyOpts.TemplateArgs = fmt.Sprintf("--kube-context %s --dry-run=server", quotedContext)
} else {
// Only add --kube-context if not already present
// Check for the flag at word boundaries to avoid false matches
if !strings.Contains(chartifyOpts.TemplateArgs, "--kube-context ") &&
!strings.HasPrefix(chartifyOpts.TemplateArgs, "--kube-context=") &&
!strings.Contains(chartifyOpts.TemplateArgs, " --kube-context=") {
chartifyOpts.TemplateArgs = fmt.Sprintf("--kube-context %s %s", quotedContext, chartifyOpts.TemplateArgs)
}
// Add --dry-run if not already present
// Check for the flag at word boundaries to avoid false matches
if !strings.Contains(chartifyOpts.TemplateArgs, "--dry-run ") &&
!strings.HasPrefix(chartifyOpts.TemplateArgs, "--dry-run=") &&
!strings.Contains(chartifyOpts.TemplateArgs, " --dry-run=") {
chartifyOpts.TemplateArgs += " --dry-run=server"
}
}
} else {
if chartifyOpts.TemplateArgs == "" {
chartifyOpts.TemplateArgs = "--dry-run=server"
} else if !strings.Contains(chartifyOpts.TemplateArgs, "--dry-run ") &&
!strings.HasPrefix(chartifyOpts.TemplateArgs, "--dry-run=") &&
!strings.Contains(chartifyOpts.TemplateArgs, " --dry-run=") {
chartifyOpts.TemplateArgs += " --dry-run=server"
}
}
}