Files
helmfile/pkg/state/issue_2444_test.go
T
yxxheroandClaude ad81e4231e build: update Go to 1.27.1 and modernize the codebase (#2798)
Toolchain and CI:
- Bump go directive from 1.26.8 to 1.27.1 (go.mod)
- Use golang:1.27-alpine builder images in all Dockerfiles
- Bump golangci-lint to v2.13.2 (first release with go1.27 support)
- Add CI gate: `go fix -diff` fails when outdated Go patterns are
  detected (locally: `make check-modernize`)

Note: darwin binaries now require macOS 13 or later.

Lint fixes required by golangci-lint v2.13.2:
- goconst: ignore tests (all 436 findings were test-only; goconst
  got stricter since v2.12 and this option was added for it)
- openai.go: keep deprecated MaxTokens deliberately with a nolint
  rationale (max_tokens is the only form universally supported by
  OpenAI-compatible backends like One-API, LiteLLM, Ollama shim)
- state.go: drop always-nil flags param from appendChartVersionFlags
  (renamed to chartVersionFlags, unparam)

Modernization (go fix ./..., 62 files):
- interface{} -> any, maps.Copy, strings.SplitSeq, range-over-int,
  builtin min/max, slices.Contains/ContainsFunc/Sort, WaitGroup.Go,
  reflect.Type.Fields(), new(expr)
- exit_error.go: strings.Builder + fmt.Fprintf instead of string
  concatenation and WriteString(fmt.Sprintf(...)) (QF1012)
- chart_dependency.go: strings.CutLast for OCI dependency helpers

Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-16 13:29:02 +08:00

248 lines
8.9 KiB
Go

package state
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// TestKubeconfigPassedToChartify verifies that when --kubeconfig is set,
// it is passed to chartify's internal helm template call.
// This is a regression test for issue #2444.
//
// Background: When using jsonPatches or kustomize patches with helmfile,
// chartify runs "helm template" internally to render the chart before applying patches.
// The lookup() helm function requires cluster access (--dry-run=server).
// Without --kubeconfig being passed to the internal helm template call,
// it fails to connect to the cluster when the user's kubeconfig is not in the default location.
//
// These tests exercise the real HelmState.buildChartifyTemplateArgs method (the pure
// helper that processChartification delegates to), not a duplicated copy of the logic.
func TestKubeconfigPassedToChartify(t *testing.T) {
tests := []struct {
name string
helmfileCommand string
kubeconfig string
kubeContext string
expectedFlags []string
unexpectedFlags []string
}{
{
name: "sync with kubeconfig should pass both kubeconfig and dry-run=server",
helmfileCommand: "sync",
kubeconfig: "/path/to/kubeconfig",
kubeContext: "",
expectedFlags: []string{"--kubeconfig", "/path/to/kubeconfig", "--dry-run=server"},
unexpectedFlags: []string{},
},
{
name: "sync with kubeconfig and kube-context should pass both",
helmfileCommand: "sync",
kubeconfig: "/path/to/kubeconfig",
kubeContext: "my-context",
expectedFlags: []string{"--kubeconfig", "/path/to/kubeconfig", "--kube-context", "my-context", "--dry-run=server"},
unexpectedFlags: []string{},
},
{
name: "apply with kubeconfig should pass kubeconfig",
helmfileCommand: "apply",
kubeconfig: "/custom/kubeconfig",
kubeContext: "",
expectedFlags: []string{"--kubeconfig", "/custom/kubeconfig", "--dry-run=server"},
unexpectedFlags: []string{},
},
{
name: "diff with kubeconfig should pass kubeconfig",
helmfileCommand: "diff",
kubeconfig: "/etc/kubeconfig",
kubeContext: "prod",
expectedFlags: []string{"--kubeconfig", "/etc/kubeconfig", "--kube-context", "prod", "--dry-run=server"},
unexpectedFlags: []string{},
},
{
name: "template command should not pass kubeconfig (offline command)",
helmfileCommand: "template",
kubeconfig: "/path/to/kubeconfig",
kubeContext: "",
expectedFlags: []string{},
unexpectedFlags: []string{"--kubeconfig", "--dry-run=server"},
},
{
name: "build command should not pass kubeconfig (offline command)",
helmfileCommand: "build",
kubeconfig: "/path/to/kubeconfig",
kubeContext: "",
expectedFlags: []string{},
unexpectedFlags: []string{"--kubeconfig", "--dry-run=server"},
},
{
name: "no kubeconfig should not add kubeconfig flag",
helmfileCommand: "sync",
kubeconfig: "",
kubeContext: "my-context",
expectedFlags: []string{"--kube-context", "my-context", "--dry-run=server"},
unexpectedFlags: []string{"--kubeconfig"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{kubeconfig: tt.kubeconfig}
got := st.buildChartifyTemplateArgs(tt.helmfileCommand, tt.kubeContext, false, "", "")
for _, flag := range tt.expectedFlags {
assert.Truef(t, strings.Contains(got, flag),
"buildChartifyTemplateArgs() = %q; want to contain %q", got, flag)
}
for _, flag := range tt.unexpectedFlags {
assert.Falsef(t, strings.Contains(got, flag),
"buildChartifyTemplateArgs() = %q; want NOT to contain %q", got, flag)
}
})
}
}
// TestKubeconfigNotDuplicated verifies that kubeconfig is not duplicated
// when it already exists in the template args.
func TestKubeconfigNotDuplicated(t *testing.T) {
tests := []struct {
name string
helmfileCommand string
kubeconfig string
existingArgs string
expectedCount int
expectedContains string
}{
{
name: "do not duplicate kubeconfig",
helmfileCommand: "sync",
kubeconfig: "/path/to/kubeconfig",
existingArgs: "--kubeconfig /existing/kubeconfig",
expectedCount: 1,
expectedContains: "--kubeconfig /existing/kubeconfig",
},
{
name: "add kubeconfig when not present",
helmfileCommand: "sync",
kubeconfig: "/path/to/kubeconfig",
existingArgs: "--some-flag",
expectedCount: 1,
expectedContains: "--kubeconfig /path/to/kubeconfig",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{kubeconfig: tt.kubeconfig}
got := st.buildChartifyTemplateArgs(tt.helmfileCommand, "", false, "", tt.existingArgs)
assert.Truef(t, strings.Contains(got, tt.expectedContains),
"buildChartifyTemplateArgs() = %q; want to contain %q", got, tt.expectedContains)
assert.Equalf(t, tt.expectedCount, strings.Count(got, "--kubeconfig"),
"buildChartifyTemplateArgs() has --kubeconfig %d times; want %d", strings.Count(got, "--kubeconfig"), tt.expectedCount)
})
}
}
// TestTemplateArgsDryRunTriggersKubeInjection verifies that when the user passes
// --template-args="--dry-run=server" to an offline command (e.g. `helmfile template`),
// the kubeconfig and kube-context ARE injected into chartify's internal helm template
// so lookup() can actually reach the cluster. Regression test for issue #1833.
func TestTemplateArgsDryRunTriggersKubeInjection(t *testing.T) {
st := &HelmState{kubeconfig: "/path/to/kubeconfig"}
got := st.buildChartifyTemplateArgs("template", "my-context", false, "--dry-run=server", "")
// User-provided arg is preserved
assert.Contains(t, got, "--dry-run=server")
// Cluster connection flags are injected even though "template" is offline
assert.Contains(t, got, "--kubeconfig /path/to/kubeconfig")
assert.Contains(t, got, "--kube-context my-context")
// --dry-run=server is NOT duplicated (only the user's copy is present)
assert.Equal(t, 1, strings.Count(got, "--dry-run"))
}
// TestTemplateArgsMergedBeforeInjection verifies that user-provided template args
// are merged into chartify's existing template args before the cluster-connectivity
// injection, so that duplicate --dry-run / --kubeconfig flags are deduplicated.
func TestTemplateArgsMergedBeforeInjection(t *testing.T) {
st := &HelmState{kubeconfig: "/path/to/kubeconfig"}
got := st.buildChartifyTemplateArgs(
"sync", "ctx", false,
"--dry-run=server", // user arg
"--enable-dns", // existing chartify arg
)
assert.Contains(t, got, "--enable-dns")
assert.Contains(t, got, "--dry-run=server")
// kubeconfig injected once, dry-run appears once (not duplicated)
assert.Equal(t, 1, strings.Count(got, "--dry-run"))
assert.Equal(t, 1, strings.Count(got, "--kubeconfig"))
}
// TestEffectiveTemplateArgs verifies CLI args take precedence over helmDefaults.templateArgs,
// mirroring the DiffArgs precedence (switch: CLI wins, else helmDefaults, else empty).
func TestEffectiveTemplateArgs(t *testing.T) {
tests := []struct {
name string
cliArgs string
helmDefaults []string
want string
}{
{
name: "CLI args win over helmDefaults",
cliArgs: "--dry-run=server",
helmDefaults: []string{"--enable-dns"},
want: "--dry-run=server",
},
{
name: "helmDefaults used when CLI empty",
cliArgs: "",
helmDefaults: []string{"--dry-run=server", "--enable-dns"},
want: "--dry-run=server --enable-dns",
},
{
name: "empty when neither set",
cliArgs: "",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st := &HelmState{
HelmDefaults: HelmSpec{TemplateArgs: tt.helmDefaults},
}
assert.Equal(t, tt.want, st.effectiveTemplateArgs(tt.cliArgs))
})
}
}
// TestHelmDefaultsTemplateArgsReachesChartify is a belt-and-suspenders test for the
// composition used by processChartification: it resolves the effective template args
// (CLI vs helmDefaults) and feeds them to buildChartifyTemplateArgs. Verifies that
// setting helmDefaults.templateArgs=[--dry-run=server] makes an offline `template`
// command opt into server-side templating + kube-connection injection end-to-end.
func TestHelmDefaultsTemplateArgsReachesChartify(t *testing.T) {
st := &HelmState{
kubeconfig: "/path/to/kubeconfig",
HelmDefaults: HelmSpec{TemplateArgs: []string{"--dry-run=server"}},
}
// Mirror processChartification's exact call shape.
resolved := st.effectiveTemplateArgs("")
got := st.buildChartifyTemplateArgs("template", "my-context", false, resolved, "")
// helmDefaults --dry-run=server is present
assert.Contains(t, got, "--dry-run=server")
// ...and triggered kube-connection injection for an otherwise-offline command
assert.Contains(t, got, "--kubeconfig /path/to/kubeconfig")
assert.Contains(t, got, "--kube-context my-context")
// not duplicated
assert.Equal(t, 1, strings.Count(got, "--dry-run"))
}