Merge e5ba174314 into 0d7309f2b7
This commit is contained in:
commit
97b4bdb8b5
2
go.mod
2
go.mod
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue