package exectest import ( "errors" "fmt" "io" "os" "os/exec" "strings" "sync" "github.com/Masterminds/semver/v3" chart "helm.sh/helm/v4/pkg/chart/v2" "github.com/helmfile/helmfile/pkg/helmexec" ) type ListKey struct { Filter string Flags string } func (k ListKey) String() string { return fmt.Sprintf("listkey(filter=%s,flags=%s)", k.Filter, k.Flags) } type DiffKey struct { Name string Chart string Flags string } type Helm struct { Charts []string Repo []string RegistryLoginHost string // Captures the host passed to RegistryLogin Releases []Release Deleted []Release Linted []Release Templated []Release Lists map[ListKey]string Diffs map[DiffKey]error Diffed []Release FailOnUnexpectedDiff bool FailOnUnexpectedList bool Version *semver.Version UpdateDepsCallbacks map[string]func(string) error DiffMutex *sync.Mutex ChartsMutex *sync.Mutex ReleasesMutex *sync.Mutex Helm3 bool Helm4 bool } type Release struct { Name string Flags []string } type Affected struct { Upgraded []*Release Reinstalled []*Release Deleted []*Release Failed []*Release } func (helm *Helm) UpdateDeps(chart string) error { if strings.Contains(chart, "error") { return fmt.Errorf("simulated UpdateDeps failure for chart: %s", chart) } helm.Charts = append(helm.Charts, chart) if helm.UpdateDepsCallbacks != nil { callback, exists := helm.UpdateDepsCallbacks[chart] if exists { if err := callback(chart); err != nil { return err } } } return nil } func (helm *Helm) BuildDeps(name, chart string, flags ...string) error { if strings.Contains(chart, "error") { return errors.New("error") } helm.Charts = append(helm.Charts, chart) return nil } func (helm *Helm) SetExtraArgs(args ...string) { } func (helm *Helm) SetHelmBinary(bin string) { } func (helm *Helm) SetEnableLiveOutput(enableLiveOutput bool) { } func (helm *Helm) SetDisableForceUpdate(forceUpdate bool) { } func (helm *Helm) SkipSchemaValidation(skipSchemaValidation bool) { } func (helm *Helm) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials, skipTLSVerify bool) error { helm.Repo = []string{name, repository, cafile, certfile, keyfile, username, password, managed, fmt.Sprintf("%v", passCredentials), fmt.Sprintf("%v", skipTLSVerify)} return nil } func (helm *Helm) UpdateRepo() error { return nil } func (helm *Helm) RegistryLogin(name, username, password, caFile, certFile, keyFile string, skipTLSVerify bool) error { helm.RegistryLoginHost = name return nil } func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart, namespace string, flags ...string) error { if strings.Contains(name, "forbidden") { releaseExists := false for _, release := range helm.Releases { if release.Name == name { releaseExists = true } } releaseDeleted := false for _, release := range helm.Deleted { if release.Name == name { releaseDeleted = true } } // Only fail if the release is present in the helm.Releases to simulate a forbidden update if it exists if releaseExists && !releaseDeleted { return fmt.Errorf("cannot patch %q with kind StatefulSet: StatefulSet.apps %q is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden", name, name) } } else if strings.Contains(name, "error") { return errors.New("error") } helm.sync(helm.ReleasesMutex, func() { helm.Releases = append(helm.Releases, Release{Name: name, Flags: flags}) }) helm.sync(helm.ChartsMutex, func() { helm.Charts = append(helm.Charts, chart) }) return nil } func (helm *Helm) DiffRelease(context helmexec.HelmContext, name, chart, namespace string, suppressDiff bool, flags ...string) error { if helm.DiffMutex != nil { helm.DiffMutex.Lock() } helm.Diffed = append(helm.Diffed, Release{Name: name, Flags: flags}) if helm.DiffMutex != nil { helm.DiffMutex.Unlock() } if helm.Diffs == nil { return nil } key := DiffKey{Name: name, Chart: chart, Flags: strings.Join(flags, " ")} err, ok := helm.Diffs[key] if !ok && helm.FailOnUnexpectedDiff { return fmt.Errorf("unexpected diff with key: %v", key) } return err } func (helm *Helm) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error { if strings.Contains(release, "notFound") { return errors.New("Error: release: not found") } if strings.Contains(release, "error") { return errors.New("error") } helm.Releases = append(helm.Releases, Release{Name: release, Flags: flags}) return nil } func (helm *Helm) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error { if strings.Contains(name, "error") { return errors.New("error") } helm.Deleted = append(helm.Deleted, Release{Name: name, Flags: flags}) return nil } func (helm *Helm) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) { key := ListKey{Filter: filter, Flags: strings.Join(flags, " ")} if helm.Lists == nil { return "dummy non-empty helm-list output", nil } res, ok := helm.Lists[key] if !ok && helm.FailOnUnexpectedList { var keys []string for k := range helm.Lists { keys = append(keys, k.String()) } return "", fmt.Errorf("unexpected list key: %v not found in %v", key, strings.Join(keys, ", ")) } return res, nil } func (helm *Helm) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) { return "", nil } func (helm *Helm) TestRelease(context helmexec.HelmContext, name string, flags ...string) error { if strings.Contains(name, "error") { return errors.New("error") } helm.Releases = append(helm.Releases, Release{Name: name, Flags: flags}) return nil } func (helm *Helm) Fetch(chart string, flags ...string) error { return nil } func (helm *Helm) Lint(name, chart string, flags ...string) error { if strings.Contains(name, "error") { return errors.New("error") } helm.Linted = append(helm.Linted, Release{Name: name, Flags: flags}) return nil } func (helm *Helm) TemplateRelease(name, chart string, flags ...string) error { if strings.Contains(name, "error") { return errors.New("error") } helm.Templated = append(helm.Templated, Release{Name: name, Flags: flags}) return nil } func (helm *Helm) ChartPull(chart string, path string, flags ...string) error { return nil } func (helm *Helm) ChartExport(chart string, path string) error { return nil } func (helm *Helm) IsHelm3() bool { // Priority order: // 1. If Version is explicitly set, use that if helm.Version != nil { return helm.Version.Major() == 3 } // 2. Check explicit struct field settings (for unit tests) if helm.Helm3 { return true } if helm.Helm4 { return false } // 3. Check environment variable (for CI matrix testing) if IsHelm4Enabled() { return false } // 4. Default to Helm 4 (newer version) return false } func (helm *Helm) IsHelm4() bool { // Priority order: // 1. If Version is explicitly set, use that if helm.Version != nil { return helm.Version.Major() == 4 } // 2. Check explicit struct field settings (for unit tests) if helm.Helm4 { return true } if helm.Helm3 { return false } // 3. Check environment variable (for CI matrix testing) if IsHelm4Enabled() { return true } // 4. Default to Helm 4 (newer version) return true } func (helm *Helm) GetVersion() helmexec.Version { return helmexec.Version{ Major: int(helm.Version.Major()), Minor: int(helm.Version.Minor()), Patch: int(helm.Version.Patch()), } } func (helm *Helm) IsVersionAtLeast(versionStr string) bool { if helm.Version == nil { return false } ver := semver.MustParse(versionStr) return helm.Version.Equal(ver) || helm.Version.GreaterThan(ver) } func (helm *Helm) sync(m *sync.Mutex, f func()) { if m != nil { m.Lock() defer m.Unlock() } f() } func (helm *Helm) ShowChart(chartPath string) (chart.Metadata, error) { switch chartPath { case "../../foo-bar": return chart.Metadata{Version: "3.2.0"}, nil default: return chart.Metadata{}, errors.New("fake test error") } } // IsHelm4Enabled detects the installed Helm version by executing the helm binary. // It returns true if Helm 4.x is installed, false for Helm 3.x or earlier. // Falls back to environment variable HELMFILE_HELM4 if helm binary is not available. func IsHelm4Enabled() bool { // First try to detect actual Helm version helmBinary := os.Getenv("HELM_BIN") if helmBinary == "" { helmBinary = "helm" } // Create a simple runner for executing helm version runner := &simpleRunner{} version, err := helmexec.GetHelmVersion(helmBinary, runner) if err == nil && version != nil { return version.Major() == 4 } // Fallback to environment variable for CI/testing scenarios where helm might not be available return os.Getenv("HELMFILE_HELM4") == "1" } // simpleRunner is a minimal implementation of helmexec.Runner for version detection type simpleRunner struct{} func (r *simpleRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) { command := exec.Command(cmd, args...) if env != nil { command.Env = append(os.Environ(), mapToEnv(env)...) } return command.CombinedOutput() } func (r *simpleRunner) ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) { command := exec.Command(cmd, args...) if env != nil { command.Env = append(os.Environ(), mapToEnv(env)...) } command.Stdin = stdin return command.CombinedOutput() } func mapToEnv(m map[string]string) []string { var env []string for k, v := range m { env = append(env, k+"="+v) } return env }