From 19b0a35c90b6408098dd114168ac33fe5e2f08c8 Mon Sep 17 00:00:00 2001 From: Dennis Witt Date: Fri, 21 Aug 2026 23:05:55 +0200 Subject: [PATCH 1/3] feat: add oci helm cart upgrade path Signed-off-by: Dennis Witt --- src/core/socketapi.go | 9 +++ src/helm/helm.go | 127 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/src/core/socketapi.go b/src/core/socketapi.go index 132a0075..f01db81b 100644 --- a/src/core/socketapi.go +++ b/src/core/socketapi.go @@ -1038,6 +1038,15 @@ func (self *socketApi) registerPatterns() { }, ) + RegisterPatternHandler( + PatternHandle{self, "cluster/helm-chart-upgrade-oci"}, + PatternConfig{}, + func(datagram structs.Datagram, request helm.HelmChartOciInstallUpgradeRequest) (string, error) { + res, err := helm.HelmOciUpgrade(request) + return store.AddToAuditLog(datagram, self.logger, res, err, nil, nil) + }, + ) + RegisterPatternHandler( PatternHandle{self, "cluster/helm-chart-show"}, PatternConfig{}, diff --git a/src/helm/helm.go b/src/helm/helm.go index 8d7dc303..4f325498 100644 --- a/src/helm/helm.go +++ b/src/helm/helm.go @@ -1037,6 +1037,133 @@ func HelmOciInstall(data HelmChartOciInstallUpgradeRequest) (result string, err return installStatus(*re), nil } +func HelmOciUpgrade(data HelmChartOciInstallUpgradeRequest) (result string, err error) { + defer invalidateReleaseListCache() // the release set changed + // See HelmOciInstall for the rationale on no longer wiping logs on error. + cleanReleaseLogs(data.Namespace, data.Release) + + settings := NewCli() + settings.Debug = false + settings.SetNamespace(data.Namespace) + helmLogger.Info("Setting up Helm OCI upgrade...", "releaseName", data.Release, "namespace", data.Namespace) + + actionConfig := new(action.Configuration) + if err := actionConfig.Init(settings.RESTClientGetter(), data.Namespace, ""); err != nil { + helmLogger.Error("HelmOCIUpgrade Init", + "releaseName", data.Release, + "namespace", data.Namespace, + "error", err.Error(), + ) + return "", err + } + + if !registry.IsOCI(data.OCIChartUrl) { + return "", fmt.Errorf("non-OCI charts are not supported in OCI upgrade") + } + + // Parse the values string into a map + valuesMap := map[string]any{} + if err := yaml.Unmarshal([]byte(data.Values), &valuesMap); err != nil { + helmLogger.Error("failed to Unmarshal HelmOCIUpgrade Values", + "releaseName", data.Release, + "namespace", data.Namespace, + "error", err.Error(), + ) + return "", err + } + + upgrade := action.NewUpgrade(actionConfig) + if data.DryRun { + upgrade.DryRunStrategy = action.DryRunServer + } + // See HelmReleaseUpgrade for the rationale behind these flags. + upgrade.WaitStrategy = kube.HookOnlyStrategy + upgrade.Namespace = data.Namespace + upgrade.Version = data.Version + upgrade.Devel = true + upgrade.ForceConflicts = true + upgrade.ServerSideApply = "true" + + // Create registry client for OCI + registryClient, err := newRegistryClient(settings, false) + if err != nil { + return "", fmt.Errorf("failed to create OCI registry client: %w", err) + } + if (data.Username != "" || data.Password != "") && data.AuthHost != "" { + err = registryClient.Login( + data.AuthHost, + registry.LoginOptBasicAuth(data.Username, data.Password), + ) + if err != nil { + return "", fmt.Errorf("failed to login to OCI registry: %w", err) + } + } else { + helmLogger.Info("No OCI registry credentials provided, attempting anonymous access", "releaseName", data.Release, "namespace", data.Namespace) + } + upgrade.SetRegistryClient(registryClient) + + helmLogger.Info("Locating chart ...", "releaseName", data.Release, "namespace", data.Namespace) + chartPath, err := upgrade.LocateChart(data.OCIChartUrl, settings) + if err != nil { + return "", fmt.Errorf("failed to locate chart: %w", err) + } + + helmLogger.Info("Loading pulled OCI chart ...", "releaseName", data.Release, "namespace", data.Namespace) + chartRequested, err := loader.Load(chartPath) + if err != nil { + helmLogger.Error("HelmOCIUpgrade Load", + "releaseName", data.Release, + "namespace", data.Namespace, + "error", err.Error(), + ) + return "", err + } + + // Ownership preflight: see HelmReleaseUpgrade. + if !data.DryRun { + needsTakeOwnership, perr := CheckOwnershipAndLog( + actionConfig, chartRequested, valuesMap, + data.Release, data.Namespace, data.Version, + ) + if perr != nil { + helmLogger.Error("HelmOCIUpgrade ownership preflight failed", + "releaseName", data.Release, + "namespace", data.Namespace, + "error", perr.Error(), + ) + return "", perr + } + upgrade.TakeOwnership = needsTakeOwnership + } + + helmLogger.Info("Upgrading OCI chart ...", "releaseName", data.Release, "namespace", data.Namespace) + re, err := upgrade.Run(data.Release, chartRequested, valuesMap) + if err != nil { + helmLogger.Error("HelmOCIUpgrade Run failed", + "releaseName", data.Release, + "namespace", data.Namespace, + "error", err.Error(), + ) + logReleaseFailureDiagnostics(settings, data.Namespace, data.Release) + return "", err + } + if re == nil { + return "", fmt.Errorf("HelmOCIUpgrade Error: Release not found") + } + + err = SaveRepoNameToValkey(data.Namespace, data.Release, data.OCIChartUrl) + if err != nil { + helmLogger.Error("failed to SaveRepoNameToValkey", + "releaseName", data.Release, + "namespace", data.Namespace, + "error", err.Error(), + ) + return "", err + } + + return installStatus(re), nil +} + // HelmOciChartVersion lists the available tags for an OCI chart, newest first // (registry.Client.Tags returns them semver-sorted descending), so the caller // can offer a version picker pre-filled with the latest the same way the From b3e68e8bc3e2ac58fe336dbc29dc6c405da4c2c1 Mon Sep 17 00:00:00 2001 From: Dennis Witt Date: Fri, 21 Aug 2026 23:55:01 +0200 Subject: [PATCH 2/3] feat: show set values for oci helm releases Signed-off-by: Dennis Witt --- src/helm/helm.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/helm/helm.go b/src/helm/helm.go index 4f325498..946d93d4 100644 --- a/src/helm/helm.go +++ b/src/helm/helm.go @@ -275,6 +275,10 @@ type HelmChartShowRequest struct { Chart string `json:"chart" validate:"required"` ShowFormat action.ShowOutputFormat `json:"format" validate:"required"` // "all" "chart" "values" "readme" "crds" Version string `json:"version,omitempty"` // optional, if not set, the latest version will be used + // OCI specific fields — only used when Chart is an "oci://" reference. + AuthHost string `json:"authHost,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` } type HelmChartVersionRequest struct { @@ -843,17 +847,33 @@ func HelmChartShow(data HelmChartShowRequest) (string, error) { return "", err } + // action.Show embeds ChartPathOptions, so LocateChart/SetRegistryClient are + // both called on the same show object — matching how HelmOciInstall/HelmOciUpgrade + // wire OCI support into action.Install/action.Upgrade, which embed it the same way. + show := action.NewShow(data.ShowFormat, actionConfig) + show.Version = data.Version + + if registry.IsOCI(data.Chart) { + registryClient, err := newRegistryClient(settings, false) + if err != nil { + return "", fmt.Errorf("failed to create OCI registry client: %w", err) + } + if (data.Username != "" || data.Password != "") && data.AuthHost != "" { + if err := registryClient.Login(data.AuthHost, registry.LoginOptBasicAuth(data.Username, data.Password)); err != nil { + return "", fmt.Errorf("failed to login to OCI registry: %w", err) + } + } + show.SetRegistryClient(registryClient) + } + // Fetch the chart - chartPathOptions := action.ChartPathOptions{} - chartPathOptions.Version = data.Version - chartPath, err := chartPathOptions.LocateChart(data.Chart, settings) + chartPath, err := show.LocateChart(data.Chart, settings) if err != nil { helmLogger.Error("HelmShow LocateChart", "error", err.Error()) return "", err } // Show the chart - show := action.NewShow(data.ShowFormat, actionConfig) result, err := show.Run(chartPath) if err != nil { helmLogger.Error("HelmShow Run", "error", err.Error()) From 79594817c0513bbe3857d4154877b0fb1ea4ed89 Mon Sep 17 00:00:00 2001 From: Dennis Witt Date: Sat, 22 Aug 2026 00:22:43 +0200 Subject: [PATCH 3/3] refactor: handle oci upgrade the same as release upgrade Signed-off-by: Dennis Witt --- src/core/socketapi.go | 9 --- src/helm/helm.go | 171 +++++++++--------------------------------- 2 files changed, 37 insertions(+), 143 deletions(-) diff --git a/src/core/socketapi.go b/src/core/socketapi.go index f01db81b..132a0075 100644 --- a/src/core/socketapi.go +++ b/src/core/socketapi.go @@ -1038,15 +1038,6 @@ func (self *socketApi) registerPatterns() { }, ) - RegisterPatternHandler( - PatternHandle{self, "cluster/helm-chart-upgrade-oci"}, - PatternConfig{}, - func(datagram structs.Datagram, request helm.HelmChartOciInstallUpgradeRequest) (string, error) { - res, err := helm.HelmOciUpgrade(request) - return store.AddToAuditLog(datagram, self.logger, res, err, nil, nil) - }, - ) - RegisterPatternHandler( PatternHandle{self, "cluster/helm-chart-show"}, PatternConfig{}, diff --git a/src/helm/helm.go b/src/helm/helm.go index 946d93d4..b7a0ff1d 100644 --- a/src/helm/helm.go +++ b/src/helm/helm.go @@ -248,12 +248,18 @@ type HelmChartSearchRequest struct { type HelmChartInstallUpgradeRequest struct { Namespace string `json:"namespace" validate:"required"` - Chart string `json:"chart" validate:"required"` - Release string `json:"release" validate:"required"` + // Chart is either a repo-relative reference ("repoName/chartName") or a full + // "oci://..." reference — HelmReleaseUpgrade dispatches on registry.IsOCI(Chart). + Chart string `json:"chart" validate:"required"` + Release string `json:"release" validate:"required"` // Optional fields Version string `json:"version,omitempty"` Values string `json:"values,omitempty"` DryRun bool `json:"dryRun,omitempty"` + // OCI specific fields — only used when Chart is an "oci://" reference. + AuthHost string `json:"authHost,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` } type HelmChartOciInstallUpgradeRequest struct { @@ -848,8 +854,9 @@ func HelmChartShow(data HelmChartShowRequest) (string, error) { } // action.Show embeds ChartPathOptions, so LocateChart/SetRegistryClient are - // both called on the same show object — matching how HelmOciInstall/HelmOciUpgrade - // wire OCI support into action.Install/action.Upgrade, which embed it the same way. + // both called on the same show object — matching how HelmOciInstall and + // HelmReleaseUpgrade's OCI branch wire OCI support into action.Install and + // action.Upgrade, which embed it the same way. show := action.NewShow(data.ShowFormat, actionConfig) show.Version = data.Version @@ -1057,133 +1064,6 @@ func HelmOciInstall(data HelmChartOciInstallUpgradeRequest) (result string, err return installStatus(*re), nil } -func HelmOciUpgrade(data HelmChartOciInstallUpgradeRequest) (result string, err error) { - defer invalidateReleaseListCache() // the release set changed - // See HelmOciInstall for the rationale on no longer wiping logs on error. - cleanReleaseLogs(data.Namespace, data.Release) - - settings := NewCli() - settings.Debug = false - settings.SetNamespace(data.Namespace) - helmLogger.Info("Setting up Helm OCI upgrade...", "releaseName", data.Release, "namespace", data.Namespace) - - actionConfig := new(action.Configuration) - if err := actionConfig.Init(settings.RESTClientGetter(), data.Namespace, ""); err != nil { - helmLogger.Error("HelmOCIUpgrade Init", - "releaseName", data.Release, - "namespace", data.Namespace, - "error", err.Error(), - ) - return "", err - } - - if !registry.IsOCI(data.OCIChartUrl) { - return "", fmt.Errorf("non-OCI charts are not supported in OCI upgrade") - } - - // Parse the values string into a map - valuesMap := map[string]any{} - if err := yaml.Unmarshal([]byte(data.Values), &valuesMap); err != nil { - helmLogger.Error("failed to Unmarshal HelmOCIUpgrade Values", - "releaseName", data.Release, - "namespace", data.Namespace, - "error", err.Error(), - ) - return "", err - } - - upgrade := action.NewUpgrade(actionConfig) - if data.DryRun { - upgrade.DryRunStrategy = action.DryRunServer - } - // See HelmReleaseUpgrade for the rationale behind these flags. - upgrade.WaitStrategy = kube.HookOnlyStrategy - upgrade.Namespace = data.Namespace - upgrade.Version = data.Version - upgrade.Devel = true - upgrade.ForceConflicts = true - upgrade.ServerSideApply = "true" - - // Create registry client for OCI - registryClient, err := newRegistryClient(settings, false) - if err != nil { - return "", fmt.Errorf("failed to create OCI registry client: %w", err) - } - if (data.Username != "" || data.Password != "") && data.AuthHost != "" { - err = registryClient.Login( - data.AuthHost, - registry.LoginOptBasicAuth(data.Username, data.Password), - ) - if err != nil { - return "", fmt.Errorf("failed to login to OCI registry: %w", err) - } - } else { - helmLogger.Info("No OCI registry credentials provided, attempting anonymous access", "releaseName", data.Release, "namespace", data.Namespace) - } - upgrade.SetRegistryClient(registryClient) - - helmLogger.Info("Locating chart ...", "releaseName", data.Release, "namespace", data.Namespace) - chartPath, err := upgrade.LocateChart(data.OCIChartUrl, settings) - if err != nil { - return "", fmt.Errorf("failed to locate chart: %w", err) - } - - helmLogger.Info("Loading pulled OCI chart ...", "releaseName", data.Release, "namespace", data.Namespace) - chartRequested, err := loader.Load(chartPath) - if err != nil { - helmLogger.Error("HelmOCIUpgrade Load", - "releaseName", data.Release, - "namespace", data.Namespace, - "error", err.Error(), - ) - return "", err - } - - // Ownership preflight: see HelmReleaseUpgrade. - if !data.DryRun { - needsTakeOwnership, perr := CheckOwnershipAndLog( - actionConfig, chartRequested, valuesMap, - data.Release, data.Namespace, data.Version, - ) - if perr != nil { - helmLogger.Error("HelmOCIUpgrade ownership preflight failed", - "releaseName", data.Release, - "namespace", data.Namespace, - "error", perr.Error(), - ) - return "", perr - } - upgrade.TakeOwnership = needsTakeOwnership - } - - helmLogger.Info("Upgrading OCI chart ...", "releaseName", data.Release, "namespace", data.Namespace) - re, err := upgrade.Run(data.Release, chartRequested, valuesMap) - if err != nil { - helmLogger.Error("HelmOCIUpgrade Run failed", - "releaseName", data.Release, - "namespace", data.Namespace, - "error", err.Error(), - ) - logReleaseFailureDiagnostics(settings, data.Namespace, data.Release) - return "", err - } - if re == nil { - return "", fmt.Errorf("HelmOCIUpgrade Error: Release not found") - } - - err = SaveRepoNameToValkey(data.Namespace, data.Release, data.OCIChartUrl) - if err != nil { - helmLogger.Error("failed to SaveRepoNameToValkey", - "releaseName", data.Release, - "namespace", data.Namespace, - "error", err.Error(), - ) - return "", err - } - - return installStatus(re), nil -} - // HelmOciChartVersion lists the available tags for an OCI chart, newest first // (registry.Client.Tags returns them semver-sorted descending), so the caller // can offer a version picker pre-filled with the latest the same way the @@ -1387,9 +1267,15 @@ func HelmReleaseUpgrade(data HelmChartInstallUpgradeRequest) (result string, err settings := NewCli() settings.SetNamespace(data.Namespace) - helmLogger.Info("Updating repo index ...", "releaseName", data.Release, "namespace", data.Namespace) - if err := helmRepoUpdateForChart(data.Chart); err != nil { - helmLogger.Error("failed to update helm repository", "chart", data.Chart, "error", err.Error()) + isOCI := registry.IsOCI(data.Chart) + + // helmRepoUpdateForChart refreshes a classic repo index — meaningless for an + // OCI reference, which resolves directly against the registry below instead. + if !isOCI { + helmLogger.Info("Updating repo index ...", "releaseName", data.Release, "namespace", data.Namespace) + if err := helmRepoUpdateForChart(data.Chart); err != nil { + helmLogger.Error("failed to update helm repository", "chart", data.Chart, "error", err.Error()) + } } actionConfig := new(action.Configuration) @@ -1419,6 +1305,23 @@ func HelmReleaseUpgrade(data HelmChartInstallUpgradeRequest) (result string, err // resolve to SSA=false, making ForceConflicts invalid. upgrade.ServerSideApply = "true" + // action.Upgrade embeds ChartPathOptions, so SetRegistryClient/LocateChart are + // both called on the same upgrade object — same OCI wiring as HelmOciInstall. + if isOCI { + registryClient, err := newRegistryClient(settings, false) + if err != nil { + return "", fmt.Errorf("failed to create OCI registry client: %w", err) + } + if (data.Username != "" || data.Password != "") && data.AuthHost != "" { + if err := registryClient.Login(data.AuthHost, registry.LoginOptBasicAuth(data.Username, data.Password)); err != nil { + return "", fmt.Errorf("failed to login to OCI registry: %w", err) + } + } else { + helmLogger.Info("No OCI registry credentials provided, attempting anonymous access", "releaseName", data.Release, "namespace", data.Namespace) + } + upgrade.SetRegistryClient(registryClient) + } + helmLogger.Info("Locating chart ...", "releaseName", data.Release, "namespace", data.Namespace) chartPath, err := upgrade.LocateChart(data.Chart, settings) if err != nil {