Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions controllers/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
Expand Down
41 changes: 25 additions & 16 deletions store/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
291 changes: 291 additions & 0 deletions store/helm_install_values.go
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading