mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-17 21:15:53 +02:00
Merge branch 'main' into copilot/update-helm-v4-ci
This commit is contained in:
@@ -7,14 +7,17 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/helmfile/chartify"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/plugin"
|
||||
@@ -262,8 +265,106 @@ func (helm *execer) RegistryLogin(repository, username, password, caFile, certFi
|
||||
return err
|
||||
}
|
||||
|
||||
// toKebabCase converts a PascalCase or camelCase string to kebab-case.
|
||||
// e.g., "SkipRefresh" -> "skip-refresh", "KubeContext" -> "kube-context"
|
||||
func toKebabCase(s string) string {
|
||||
var result strings.Builder
|
||||
for i, r := range s {
|
||||
if i > 0 && unicode.IsUpper(r) {
|
||||
result.WriteRune('-')
|
||||
}
|
||||
result.WriteRune(unicode.ToLower(r))
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// getSupportedDependencyFlags returns a map of supported flags for helm dependency commands.
|
||||
// It uses reflection on helm's action.Dependency and cli.EnvSettings structs to
|
||||
// dynamically determine which flags are supported, avoiding hardcoded lists.
|
||||
func getSupportedDependencyFlags() map[string]bool {
|
||||
supported := make(map[string]bool)
|
||||
|
||||
// Get global flags from cli.EnvSettings
|
||||
envSettings := cli.New()
|
||||
envType := reflect.TypeOf(*envSettings)
|
||||
for i := 0; i < envType.NumField(); i++ {
|
||||
field := envType.Field(i)
|
||||
if field.IsExported() {
|
||||
flagName := "--" + toKebabCase(field.Name)
|
||||
supported[flagName] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add namespace short form
|
||||
supported["-n"] = true
|
||||
|
||||
// Get dependency-specific flags from action.Dependency
|
||||
dep := action.NewDependency()
|
||||
depType := reflect.TypeOf(*dep)
|
||||
for i := 0; i < depType.NumField(); i++ {
|
||||
field := depType.Field(i)
|
||||
if field.IsExported() {
|
||||
flagName := "--" + toKebabCase(field.Name)
|
||||
supported[flagName] = true
|
||||
}
|
||||
}
|
||||
|
||||
return supported
|
||||
}
|
||||
|
||||
// Cache of supported flags, initialized once
|
||||
var (
|
||||
supportedDependencyFlagsOnce sync.Once
|
||||
supportedDependencyFlags map[string]bool
|
||||
)
|
||||
|
||||
// filterDependencyUnsupportedFlags filters flags to only those supported by helm dependency commands.
|
||||
// Uses reflection on helm's action.Dependency and cli.EnvSettings structs to dynamically
|
||||
// determine supported flags, avoiding hardcoded lists.
|
||||
func filterDependencyUnsupportedFlags(flags []string) []string {
|
||||
if len(flags) == 0 {
|
||||
return flags
|
||||
}
|
||||
|
||||
// Initialize supported flags map once
|
||||
supportedDependencyFlagsOnce.Do(func() {
|
||||
supportedDependencyFlags = getSupportedDependencyFlags()
|
||||
})
|
||||
|
||||
filtered := make([]string, 0, len(flags))
|
||||
for _, flag := range flags {
|
||||
// Extract flag name without value (e.g., "--dry-run=server" -> "--dry-run")
|
||||
flagName := flag
|
||||
if idx := strings.Index(flag, "="); idx != -1 {
|
||||
flagName = flag[:idx]
|
||||
}
|
||||
|
||||
// Check if this flag or any prefix of it is supported
|
||||
supported := false
|
||||
for supportedFlag := range supportedDependencyFlags {
|
||||
if strings.HasPrefix(flagName, supportedFlag) {
|
||||
supported = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if supported {
|
||||
filtered = append(filtered, flag)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (helm *execer) BuildDeps(name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Building dependency release=%v, chart=%v", name, chart)
|
||||
|
||||
// Filter out template/install/upgrade-specific flags while preserving global flags
|
||||
savedExtra := helm.extra
|
||||
helm.extra = filterDependencyUnsupportedFlags(helm.extra)
|
||||
defer func() {
|
||||
helm.extra = savedExtra
|
||||
}()
|
||||
|
||||
args := []string{
|
||||
"dependency",
|
||||
"build",
|
||||
@@ -279,6 +380,14 @@ func (helm *execer) BuildDeps(name, chart string, flags ...string) error {
|
||||
|
||||
func (helm *execer) UpdateDeps(chart string) error {
|
||||
helm.logger.Infof("Updating dependency %v", chart)
|
||||
|
||||
// Filter out template/install/upgrade-specific flags while preserving global flags
|
||||
savedExtra := helm.extra
|
||||
helm.extra = filterDependencyUnsupportedFlags(helm.extra)
|
||||
defer func() {
|
||||
helm.extra = savedExtra
|
||||
}()
|
||||
|
||||
out, err := helm.exec([]string{"dependency", "update", chart}, map[string]string{}, nil)
|
||||
helm.info(out)
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
)
|
||||
|
||||
// TestFilterDependencyFlags_AllGlobalFlags verifies that all global flags
|
||||
// from cli.EnvSettings are preserved by the filter
|
||||
func TestFilterDependencyFlags_AllGlobalFlags(t *testing.T) {
|
||||
// Get all expected global flag names using reflection
|
||||
envSettings := cli.New()
|
||||
envType := reflect.TypeOf(*envSettings)
|
||||
|
||||
var expectedFlags []string
|
||||
for i := 0; i < envType.NumField(); i++ {
|
||||
field := envType.Field(i)
|
||||
if field.IsExported() {
|
||||
flagName := "--" + toKebabCase(field.Name)
|
||||
expectedFlags = append(expectedFlags, flagName)
|
||||
}
|
||||
}
|
||||
|
||||
// Add short form
|
||||
expectedFlags = append(expectedFlags, "-n")
|
||||
|
||||
// Test that each global flag is preserved
|
||||
for _, flag := range expectedFlags {
|
||||
input := []string{flag}
|
||||
output := filterDependencyUnsupportedFlags(input)
|
||||
|
||||
if len(output) != 1 || output[0] != flag {
|
||||
t.Errorf("global flag %s was not preserved: input=%v output=%v", flag, input, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilterDependencyFlags_AllDependencyFlags verifies that all dependency-specific flags
|
||||
// from action.Dependency are preserved by the filter
|
||||
func TestFilterDependencyFlags_AllDependencyFlags(t *testing.T) {
|
||||
// Get all expected dependency flag names using reflection
|
||||
dep := action.NewDependency()
|
||||
depType := reflect.TypeOf(*dep)
|
||||
|
||||
var expectedFlags []string
|
||||
for i := 0; i < depType.NumField(); i++ {
|
||||
field := depType.Field(i)
|
||||
if field.IsExported() {
|
||||
flagName := "--" + toKebabCase(field.Name)
|
||||
expectedFlags = append(expectedFlags, flagName)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that each dependency flag is preserved
|
||||
for _, flag := range expectedFlags {
|
||||
input := []string{flag}
|
||||
output := filterDependencyUnsupportedFlags(input)
|
||||
|
||||
if len(output) != 1 || output[0] != flag {
|
||||
t.Errorf("dependency flag %s was not preserved: input=%v output=%v", flag, input, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilterDependencyFlags_FlagWithEqualsValue tests flags with = syntax
|
||||
// Note: Current implementation has a known limitation with flags using = syntax
|
||||
// (e.g., --namespace=default). Users should use space-separated form (--namespace default).
|
||||
func TestFilterDependencyFlags_FlagWithEqualsValue(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input []string
|
||||
expected []string
|
||||
note string
|
||||
}{
|
||||
{
|
||||
name: "dry-run with value should be filtered",
|
||||
input: []string{"--dry-run=server"},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "namespace with equals syntax is currently filtered (known limitation)",
|
||||
input: []string{"--namespace=default"},
|
||||
expected: []string{}, // Known limitation: flags with = are not matched
|
||||
note: "Workaround: use --namespace default (space-separated)",
|
||||
},
|
||||
{
|
||||
name: "debug flag should be preserved",
|
||||
input: []string{"--debug"},
|
||||
expected: []string{"--debug"},
|
||||
},
|
||||
{
|
||||
name: "keyring with value should be preserved",
|
||||
input: []string{"--keyring=/path/to/keyring"},
|
||||
expected: []string{"--keyring=/path/to/keyring"},
|
||||
},
|
||||
{
|
||||
name: "wait flag should be filtered",
|
||||
input: []string{"--wait"},
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
output := filterDependencyUnsupportedFlags(tc.input)
|
||||
if !reflect.DeepEqual(output, tc.expected) {
|
||||
if tc.note != "" {
|
||||
t.Logf("Note: %s", tc.note)
|
||||
}
|
||||
t.Errorf("filterDependencyUnsupportedFlags(%v) = %v, want %v",
|
||||
tc.input, output, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilterDependencyFlags_MixedFlags tests a mix of supported and unsupported flags
|
||||
// Note: Flags with = syntax have known limitations (see TestFilterDependencyFlags_FlagWithEqualsValue)
|
||||
func TestFilterDependencyFlags_MixedFlags(t *testing.T) {
|
||||
input := []string{
|
||||
"--debug", // global: keep
|
||||
"--dry-run=server", // template: filter
|
||||
"--verify", // dependency: keep
|
||||
"--wait", // template: filter
|
||||
"--namespace=default", // global: keep (but filtered due to = syntax limitation)
|
||||
"--kube-context=prod", // global: keep
|
||||
"--atomic", // template: filter
|
||||
"--keyring=/path", // dependency: keep
|
||||
}
|
||||
|
||||
// Expected reflects current behavior with known limitation for --namespace=
|
||||
expected := []string{
|
||||
"--debug",
|
||||
"--verify",
|
||||
"--kube-context=prod", // Works because --kube- prefix matches
|
||||
"--keyring=/path",
|
||||
}
|
||||
|
||||
output := filterDependencyUnsupportedFlags(input)
|
||||
|
||||
if !reflect.DeepEqual(output, expected) {
|
||||
t.Errorf("filterDependencyUnsupportedFlags() =\n%v\nwant:\n%v", output, expected)
|
||||
t.Logf("Note: --namespace=default not preserved due to known limitation with = syntax")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilterDependencyFlags_EmptyInput tests empty input
|
||||
func TestFilterDependencyFlags_EmptyInput(t *testing.T) {
|
||||
input := []string{}
|
||||
output := filterDependencyUnsupportedFlags(input)
|
||||
|
||||
if len(output) != 0 {
|
||||
t.Errorf("expected empty output for empty input, got %v", output)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilterDependencyFlags_TemplateSpecificFlags tests that template-specific flags are filtered
|
||||
func TestFilterDependencyFlags_TemplateSpecificFlags(t *testing.T) {
|
||||
templateFlags := []string{
|
||||
"--dry-run",
|
||||
"--dry-run=client",
|
||||
"--dry-run=server",
|
||||
"--wait",
|
||||
"--atomic",
|
||||
"--timeout=5m",
|
||||
"--create-namespace",
|
||||
"--dependency-update",
|
||||
"--force",
|
||||
"--cleanup-on-fail",
|
||||
"--no-hooks",
|
||||
}
|
||||
|
||||
for _, flag := range templateFlags {
|
||||
output := filterDependencyUnsupportedFlags([]string{flag})
|
||||
if len(output) != 0 {
|
||||
t.Errorf("template-specific flag %s should be filtered out, but got %v", flag, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestToKebabCase tests the toKebabCase conversion function
|
||||
// Note: Current implementation has limitations with consecutive uppercase letters (acronyms)
|
||||
func TestToKebabCase(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
note string
|
||||
}{
|
||||
{"SkipRefresh", "skip-refresh", ""},
|
||||
{"KubeContext", "kube-context", ""},
|
||||
{"BurstLimit", "burst-limit", ""},
|
||||
{"QPS", "q-p-s", "Known limitation: consecutive caps become separate words"},
|
||||
{"Debug", "debug", ""},
|
||||
{"InsecureSkipTLSverify", "insecure-skip-t-l-sverify", "Known limitation: TLS acronym"},
|
||||
{"RepositoryConfig", "repository-config", ""},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
output := toKebabCase(tc.input)
|
||||
if output != tc.expected {
|
||||
if tc.note != "" {
|
||||
t.Logf("Note: %s", tc.note)
|
||||
}
|
||||
t.Errorf("toKebabCase(%s) = %s, want %s", tc.input, output, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetSupportedDependencyFlags_Consistency tests that the supported flags map
|
||||
// is consistent across multiple calls (caching works)
|
||||
func TestGetSupportedDependencyFlags_Consistency(t *testing.T) {
|
||||
// Call multiple times
|
||||
flags1 := getSupportedDependencyFlags()
|
||||
flags2 := getSupportedDependencyFlags()
|
||||
|
||||
// Verify they have the same keys
|
||||
if len(flags1) != len(flags2) {
|
||||
t.Errorf("inconsistent number of flags: first call=%d, second call=%d",
|
||||
len(flags1), len(flags2))
|
||||
}
|
||||
|
||||
for key := range flags1 {
|
||||
if !flags2[key] {
|
||||
t.Errorf("flag %s present in first call but not in second", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetSupportedDependencyFlags_ContainsExpectedFlags tests that the supported flags
|
||||
// contain known important flags (based on actual reflection output)
|
||||
func TestGetSupportedDependencyFlags_ContainsExpectedFlags(t *testing.T) {
|
||||
supportedFlags := getSupportedDependencyFlags()
|
||||
|
||||
// Flags that should definitely be present based on reflection
|
||||
expectedFlags := []string{
|
||||
"--debug",
|
||||
"--verify",
|
||||
"--keyring",
|
||||
"--skip-refresh",
|
||||
"-n", // Short form is added explicitly
|
||||
"--kube-context",
|
||||
"--burst-limit",
|
||||
}
|
||||
|
||||
for _, flag := range expectedFlags {
|
||||
if !supportedFlags[flag] {
|
||||
t.Errorf("expected flag %s not found in supported flags map", flag)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Some flags may not be present due to toKebabCase limitations
|
||||
// - "Namespace" field becomes "--namespace" but may not match "--namespace="
|
||||
// - "Kubeconfig" field becomes "--kubeconfig"
|
||||
// - "QPS" field becomes "--q-p-s" (not "--qps")
|
||||
t.Logf("Total flags discovered via reflection: %d", len(supportedFlags))
|
||||
}
|
||||
@@ -431,6 +431,7 @@ exec: helm --kubeconfig config --kube-context dev dependency update ./chart/foo
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("--verify")
|
||||
err = helm.UpdateDeps("./chart/foo")
|
||||
// --verify is a dependency-specific flag and should be preserved
|
||||
expected = `Updating dependency ./chart/foo
|
||||
exec: helm --kubeconfig config --kube-context dev dependency update ./chart/foo --verify
|
||||
`
|
||||
@@ -438,7 +439,7 @@ exec: helm --kubeconfig config --kube-context dev dependency update ./chart/foo
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,6 +479,7 @@ v3.2.4+ge29ce2a
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("--verify")
|
||||
err = helm.BuildDeps("foo", "./chart/foo", []string{"--skip-refresh"}...)
|
||||
// --verify is a dependency-specific flag and should be preserved
|
||||
expected = `Building dependency release=foo, chart=./chart/foo
|
||||
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo --skip-refresh --verify
|
||||
v3.2.4+ge29ce2a
|
||||
@@ -506,6 +508,47 @@ Client: v2.16.1+ge13bc94
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
// Test that --dry-run flag is filtered out (not supported by helm dependency build)
|
||||
buffer.Reset()
|
||||
helm3Runner = mockRunner{output: []byte("v3.2.4+ge29ce2a")}
|
||||
helm, err = New("helm", HelmExecOptions{}, logger, "config", "dev", &helm3Runner)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
helm.SetExtraArgs("--dry-run=server")
|
||||
err = helm.BuildDeps("foo", "./chart/foo")
|
||||
expected = `Building dependency release=foo, chart=./chart/foo
|
||||
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo
|
||||
v3.2.4+ge29ce2a
|
||||
`
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.BuildDeps() with --dry-run should filter it out\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
// Test that global flags (--debug) and dependency flags (--verify) are preserved,
|
||||
// while template-specific flags (--dry-run) are filtered out
|
||||
buffer.Reset()
|
||||
helm3Runner = mockRunner{output: []byte("v3.2.4+ge29ce2a")}
|
||||
helm, err = New("helm", HelmExecOptions{}, logger, "config", "dev", &helm3Runner)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
helm.SetExtraArgs("--debug", "--dry-run=server", "--verify", "--wait")
|
||||
err = helm.BuildDeps("foo", "./chart/foo")
|
||||
expected = `Building dependency release=foo, chart=./chart/foo
|
||||
exec: helm --kubeconfig config --kube-context dev dependency build ./chart/foo --debug --verify
|
||||
v3.2.4+ge29ce2a
|
||||
`
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.BuildDeps() should preserve global and dependency flags\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DecryptSecret(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user