From cb49b79aa60fa2d0b02f01caa214427deea2dc4c Mon Sep 17 00:00:00 2001 From: bugkeep <1921817430@qq.com> Date: Sun, 26 Jul 2026 06:17:50 +0800 Subject: [PATCH] fix: harden app store install defaults --- controllers/helm.go | 4 +- store/helm.go | 41 +++-- store/helm_install_values.go | 291 +++++++++++++++++++++++++++++++++++ 3 files changed, 318 insertions(+), 18 deletions(-) create mode 100644 store/helm_install_values.go diff --git a/controllers/helm.go b/controllers/helm.go index 88e8a92..273452f 100644 --- a/controllers/helm.go +++ b/controllers/helm.go @@ -144,7 +144,7 @@ func (c *ApiController) GetRepoCharts() { // ---------- Chart values (via store/Helm SDK) ---------- -// GetHelmChartValues fetches the default values.yaml for a chart. +// GetHelmChartValues fetches the values.yaml shown in the App Store install dialog. // @router /api/get-helm-chart-values [get] func (c *ApiController) GetHelmChartValues() { if c.RequireSignedIn() { @@ -157,7 +157,7 @@ func (c *ApiController) GetHelmChartValues() { c.ResponseError("chart and repo are required") return } - values, err := store.GetHelmChartDefaultValues(chartName, repoURL, version) + values, err := store.GetHelmChartInstallValues(chartName, repoURL, version) if err != nil { c.ResponseError(err.Error()) return diff --git a/store/helm.go b/store/helm.go index 9227444..b7a70f8 100644 --- a/store/helm.go +++ b/store/helm.go @@ -1217,6 +1217,13 @@ func InstallHelmChart(cfg *rest.Config, releaseName, namespace, chartName, repoU if err != nil { return err } + vals, fallbackApplied, err := prepareHelmInstallValues(ch, repoURL, vals) + if err != nil { + return err + } + if fallbackApplied { + logrus.Warn(bitnamiLegacyImageWarning()) + } attachHelmCapabilities(actionConfig, cfg, namespace, helmWarningLog) if err := validateHelmChartCompatibility(context.Background(), cfg, actionConfig, releaseName, namespace, ch, vals); err != nil { @@ -1310,6 +1317,17 @@ func InstallHelmChartStream(ctx context.Context, lifecycle HelmInstallLifecycle, } return } + vals, fallbackApplied, err := prepareHelmInstallValues(helmChart, repoURL, vals) + if err != nil { + send("ERROR: " + err.Error()) + if finishErr := lifecycle.Finish(err); finishErr != nil { + logrus.Errorf("failed to finish Helm operation after values preparation error: %v", finishErr) + } + return + } + if fallbackApplied { + send("WARNING: " + bitnamiLegacyImageWarning()) + } attachHelmCapabilities(actionConfig, cfg, namespace, logFn) if err := validateHelmChartCompatibility(installCtx, cfg, actionConfig, releaseName, namespace, helmChart, vals); err != nil { send("ERROR: " + err.Error()) @@ -1373,6 +1391,13 @@ func UpgradeHelmRelease(cfg *rest.Config, releaseName, namespace, chartName, rep if err != nil { return err } + vals, fallbackApplied, err := prepareHelmInstallValues(ch, repoURL, vals) + if err != nil { + return err + } + if fallbackApplied { + logrus.Warn(bitnamiLegacyImageWarning()) + } attachHelmCapabilities(actionConfig, cfg, namespace, helmWarningLog) if err := validateHelmReleaseCompatibility(context.Background(), actionConfig, releaseName, namespace, ch, vals); err != nil { @@ -1460,19 +1485,3 @@ func GetHelmReleaseHistory(cfg *rest.Config, releaseName, namespace string) ([]H } return result, nil } - -// GetHelmChartDefaultValues downloads a chart and returns its default values.yaml content. -func GetHelmChartDefaultValues(chartName, repoURL, version string) (string, error) { - ch, err := loadChart(chartName, repoURL, version) - if err != nil { - return "", err - } - if ch.Values == nil { - return "", nil - } - data, err := yaml.Marshal(ch.Values) - if err != nil { - return "", err - } - return string(data), nil -} diff --git a/store/helm_install_values.go b/store/helm_install_values.go new file mode 100644 index 0000000..2a0cbed --- /dev/null +++ b/store/helm_install_values.go @@ -0,0 +1,291 @@ +package store + +import ( + "fmt" + "reflect" + "strings" + + "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" +) + +const ( + bitnamiChartRepoURL = "https://charts.bitnami.com/bitnami" + bitnamiOCIChartRepoPrefix = "oci://registry-1.docker.io/bitnamicharts/" + bitnamiDockerOCIChartPrefix = "oci://docker.io/bitnamicharts/" + bitnamiLegacyRegistry = "docker.io" +) + +func bitnamiLegacyImageWarning() string { + return fmt.Sprintf("Bitnami legacy image fallback is enabled: images use %s/bitnamilegacy from the frozen snapshot and global.security.allowInsecureImages=true; review these values before installation", bitnamiLegacyRegistry) +} + +// GetHelmChartInstallValues returns chart defaults with only the compatibility +// overrides that the install path will also enforce. +func GetHelmChartInstallValues(chartName, repoURL, version string) (string, error) { + ch, err := loadChart(chartName, repoURL, version) + if err != nil { + return "", err + } + return renderHelmChartInstallValues(ch, repoURL) +} + +func renderHelmChartInstallValues(ch *chart.Chart, repoURL string) (string, error) { + values, fallbackApplied, err := buildHelmChartInstallValues(ch, repoURL) + if err != nil { + return "", err + } + data, err := yaml.Marshal(values) + if err != nil { + return "", err + } + if fallbackApplied { + return "# WARNING: " + bitnamiLegacyImageWarning() + "\n" + string(data), nil + } + return string(data), nil +} + +func buildHelmChartInstallValues(ch *chart.Chart, repoURL string) (map[string]interface{}, bool, error) { + if ch == nil { + return map[string]interface{}{}, false, nil + } + overrides, fallbackApplied, err := prepareHelmInstallValues(ch, repoURL, map[string]interface{}{}) + if err != nil { + return nil, false, err + } + values := cloneHelmValues(ch.Values) + if err := mergeHelmValueOverrides(values, overrides, nil); err != nil { + return nil, false, err + } + return values, fallbackApplied, nil +} + +// prepareHelmInstallValues computes compatibility changes from Helm's fully +// coalesced values, then merges only changed paths into the caller's values. +func prepareHelmInstallValues(ch *chart.Chart, repoURL string, input map[string]interface{}) (map[string]interface{}, bool, error) { + values := cloneHelmValues(input) + if ch == nil || !isBitnamiCommunityChartRepo(repoURL) { + return values, false, nil + } + + dependencyValues := cloneHelmValues(input) + if err := chartutil.ProcessDependenciesWithMerge(ch, dependencyValues); err != nil { + return nil, false, fmt.Errorf("process Helm chart dependencies: %w", err) + } + coalesced, err := chartutil.CoalesceValues(ch, dependencyValues) + if err != nil { + return nil, false, fmt.Errorf("coalesce Helm install values: %w", err) + } + before := map[string]interface{}(coalesced) + patched := cloneHelmValues(before) + fallbackApplied := rewriteBitnamiLegacyImageRepositories(patched, false) + installDefaultsApplied := applyBitnamiInstallabilityDefaults(patched, input) + if !fallbackApplied && !installDefaultsApplied { + return values, false, nil + } + + if fallbackApplied { + security, err := ensureHelmValuesPath(patched, "global", "security") + if err != nil { + return nil, false, err + } + security["allowInsecureImages"] = true + } + + if err := mergeHelmValueOverrides(values, changedHelmValues(before, patched), nil); err != nil { + return nil, false, err + } + return values, fallbackApplied, nil +} + +func applyBitnamiInstallabilityDefaults(values, explicitValues map[string]interface{}) bool { + // Capability keys apply only when the chart declares them and the caller + // did not override them, avoiding a catalog of chart-name special cases. + changed := false + for key, wanted := range map[string]bool{ + "tomcatInstallDefaultWebapps": true, + } { + if _, explicitlySet := explicitValues[key]; explicitlySet { + continue + } + current, exists := values[key] + currentBool, isBool := current.(bool) + if exists && isBool && currentBool != wanted { + values[key] = wanted + changed = true + } + } + return changed +} + +func isBitnamiCommunityChartRepo(repoURL string) bool { + normalized := strings.ToLower(strings.TrimRight(strings.TrimSpace(repoURL), "/")) + return normalized == bitnamiChartRepoURL || + strings.HasPrefix(normalized+"/", bitnamiOCIChartRepoPrefix) || + strings.HasPrefix(normalized+"/", bitnamiDockerOCIChartPrefix) +} + +func rewriteBitnamiLegacyImageRepositories(value interface{}, imageValues bool) bool { + changed := false + switch typed := value.(type) { + case map[string]interface{}: + if imageValues && rewriteBitnamiLegacyImageRepository(typed) { + changed = true + } + for key, child := range typed { + if rewriteBitnamiLegacyImageRepositories(child, isHelmImageValuesKey(key)) { + changed = true + } + } + case []interface{}: + for _, child := range typed { + if rewriteBitnamiLegacyImageRepositories(child, imageValues) { + changed = true + } + } + } + return changed +} + +func isHelmImageValuesKey(key string) bool { + return strings.HasSuffix(strings.ToLower(strings.TrimSpace(key)), "image") +} + +func rewriteBitnamiLegacyImageRepository(values map[string]interface{}) bool { + repository, ok := values["repository"].(string) + if !ok || !hasBitnamiImageVersion(values) { + return false + } + registry, _ := values["registry"].(string) + registry = strings.ToLower(strings.TrimSpace(registry)) + preferredRegistry := bitnamiLegacyRegistry + if registry != "" && registry != "docker.io" && registry != "registry-1.docker.io" { + return false + } + + var imageName string + switch { + case strings.HasPrefix(repository, "bitnami/"): + imageName = strings.TrimPrefix(repository, "bitnami/") + case strings.HasPrefix(repository, "docker.io/bitnami/"): + imageName = strings.TrimPrefix(repository, "docker.io/bitnami/") + case strings.HasPrefix(repository, "registry-1.docker.io/bitnami/"): + imageName = strings.TrimPrefix(repository, "registry-1.docker.io/bitnami/") + case strings.HasPrefix(repository, "bitnamilegacy/"): + imageName = strings.TrimPrefix(repository, "bitnamilegacy/") + case strings.HasPrefix(repository, "docker.io/bitnamilegacy/"): + imageName = strings.TrimPrefix(repository, "docker.io/bitnamilegacy/") + case strings.HasPrefix(repository, "registry-1.docker.io/bitnamilegacy/"): + imageName = strings.TrimPrefix(repository, "registry-1.docker.io/bitnamilegacy/") + default: + return false + } + wantedRepository := "bitnamilegacy/" + imageName + if registry == preferredRegistry && repository == wantedRepository { + return false + } + values["registry"] = preferredRegistry + values["repository"] = wantedRepository + return true +} + +func hasBitnamiImageVersion(values map[string]interface{}) bool { + if digest, ok := values["digest"].(string); ok && strings.TrimSpace(digest) != "" { + return true + } + tag, ok := values["tag"].(string) + if !ok { + return false + } + return strings.TrimSpace(tag) != "" +} + +func ensureHelmValuesPath(root map[string]interface{}, path ...string) (map[string]interface{}, error) { + current := root + for index, key := range path { + existing, exists := current[key] + if !exists || existing == nil { + next := map[string]interface{}{} + current[key] = next + current = next + continue + } + next, ok := existing.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("%s must be a map to apply Bitnami image compatibility", strings.Join(path[:index+1], ".")) + } + current = next + } + return current, nil +} + +func changedHelmValues(before, after map[string]interface{}) map[string]interface{} { + changes := map[string]interface{}{} + for key, afterValue := range after { + beforeValue, exists := before[key] + if !exists { + changes[key] = cloneHelmValue(afterValue) + continue + } + beforeMap, beforeIsMap := beforeValue.(map[string]interface{}) + afterMap, afterIsMap := afterValue.(map[string]interface{}) + if beforeIsMap && afterIsMap { + if nested := changedHelmValues(beforeMap, afterMap); len(nested) != 0 { + changes[key] = nested + } + continue + } + if !reflect.DeepEqual(beforeValue, afterValue) { + changes[key] = cloneHelmValue(afterValue) + } + } + return changes +} + +func mergeHelmValueOverrides(target, overrides map[string]interface{}, path []string) error { + for key, override := range overrides { + currentPath := append(path, key) + overrideMap, isMap := override.(map[string]interface{}) + if !isMap { + target[key] = cloneHelmValue(override) + continue + } + existing, exists := target[key] + if !exists || existing == nil { + existing = map[string]interface{}{} + target[key] = existing + } + targetMap, ok := existing.(map[string]interface{}) + if !ok { + return fmt.Errorf("%s must be a map to apply Bitnami image compatibility", strings.Join(currentPath, ".")) + } + if err := mergeHelmValueOverrides(targetMap, overrideMap, currentPath); err != nil { + return err + } + } + return nil +} + +func cloneHelmValues(values map[string]interface{}) map[string]interface{} { + cloned := make(map[string]interface{}, len(values)) + for key, value := range values { + cloned[key] = cloneHelmValue(value) + } + return cloned +} + +func cloneHelmValue(value interface{}) interface{} { + switch typed := value.(type) { + case map[string]interface{}: + return cloneHelmValues(typed) + case []interface{}: + cloned := make([]interface{}, len(typed)) + for index, child := range typed { + cloned[index] = cloneHelmValue(child) + } + return cloned + default: + return typed + } +}