@@ -26,6 +26,7 @@ import (
2626 "github.com/skyhook-io/radar/pkg/helmhistory"
2727
2828 "helm.sh/helm/v3/pkg/action"
29+ "helm.sh/helm/v3/pkg/chart"
2930 "helm.sh/helm/v3/pkg/chart/loader"
3031 "helm.sh/helm/v3/pkg/cli"
3132 "helm.sh/helm/v3/pkg/registry"
@@ -2315,6 +2316,32 @@ func (c *Client) UpgradeAsUser(namespace, name, targetVersion, repositoryName st
23152316 return c .upgradeWith (actionConfig , name , targetVersion , repositoryName , noop )
23162317}
23172318
2319+ // UpgradeWithValuesProgress upgrades a release to a target version applying the
2320+ // supplied user values (WYSIWYG), reporting progress via a channel.
2321+ func (c * Client ) UpgradeWithValuesProgress (namespace , name , targetVersion , repositoryName string , newValues map [string ]any , progressCh chan <- InstallProgress ) error {
2322+ sendProgress := progressSender (progressCh )
2323+ sendProgress ("preparing" , fmt .Sprintf ("Getting current release %s..." , name ), "" )
2324+
2325+ actionConfig , err := c .getActionConfig (namespace )
2326+ if err != nil {
2327+ return err
2328+ }
2329+ return c .upgradeWithValues (actionConfig , name , targetVersion , repositoryName , newValues , sendProgress )
2330+ }
2331+
2332+ // UpgradeWithValuesProgressAsUser upgrades a release to a target version applying
2333+ // the supplied user values with K8s impersonation and progress reporting.
2334+ func (c * Client ) UpgradeWithValuesProgressAsUser (namespace , name , targetVersion , repositoryName string , newValues map [string ]any , username string , groups []string , progressCh chan <- InstallProgress ) error {
2335+ sendProgress := progressSender (progressCh )
2336+ sendProgress ("preparing" , fmt .Sprintf ("Getting current release %s..." , name ), "" )
2337+
2338+ actionConfig , err := c .getActionConfigForUser (namespace , username , groups )
2339+ if err != nil {
2340+ return err
2341+ }
2342+ return c .upgradeWithValues (actionConfig , name , targetVersion , repositoryName , newValues , sendProgress )
2343+ }
2344+
23182345func progressSender (progressCh chan <- InstallProgress ) func (phase , message , detail string ) {
23192346 return func (phase , message , detail string ) {
23202347 if progressCh == nil {
@@ -2335,16 +2362,11 @@ func (c *Client) upgradeWith(actionConfig *action.Configuration, name, targetVer
23352362 return fmt .Errorf ("failed to get current release: %w" , err )
23362363 }
23372364
2338- chartName := rel .Chart .Metadata .Name
2339- sendProgress ("resolving" , fmt .Sprintf ("Finding %s version %s in repositories..." , chartName , targetVersion ), "" )
2340-
2341- chartPath , resolvedRepo , err := c .resolveUpgradeChartPath (chartName , targetVersion , repositoryName , chartSourceHosts (rel .Chart .Metadata .Home , rel .Chart .Metadata .Sources ))
2365+ targetChart , err := c .chartForUpgradeTarget (actionConfig , rel , targetVersion , repositoryName , sendProgress )
23422366 if err != nil {
23432367 return err
23442368 }
23452369
2346- sendProgress ("downloading" , fmt .Sprintf ("Downloading %s-%s from %s..." , chartName , targetVersion , resolvedRepo ), chartPath )
2347-
23482370 // Create upgrade action — don't use Wait=true because Radar already
23492371 // shows real-time resource status via SSE. Waiting blocks the dialog
23502372 // for minutes with zero feedback; users can monitor the rollout in the UI.
@@ -2357,49 +2379,105 @@ func (c *Client) upgradeWith(actionConfig *action.Configuration, name, targetVer
23572379 // for keys a newer chart added (a cross-version upgrade footgun).
23582380 upgradeAction .ResetThenReuseValues = true
23592381
2382+ sendProgress ("upgrading" , fmt .Sprintf ("Applying %s %s..." , rel .Chart .Metadata .Name , targetVersion ), "" )
2383+
2384+ // Run the upgrade
2385+ _ , err = upgradeAction .Run (name , targetChart , rel .Config )
2386+ if err != nil {
2387+ return fmt .Errorf ("upgrade failed: %w" , err )
2388+ }
2389+
2390+ sendProgress ("complete" , fmt .Sprintf ("Successfully upgraded %s to %s" , name , targetVersion ), "" )
2391+ return nil
2392+ }
2393+
2394+ // upgradeWithValues upgrades a release to a target chart version applying exactly
2395+ // the supplied user values (WYSIWYG — ResetValues, no merge with prior overrides).
2396+ // The plain upgradeWith path keeps ResetThenReuseValues for the blind carry-over case.
2397+ func (c * Client ) upgradeWithValues (actionConfig * action.Configuration , name , targetVersion , repositoryName string , newValues map [string ]any , sendProgress func (phase , message , detail string )) error {
2398+ getAction := action .NewGet (actionConfig )
2399+ rel , err := getAction .Run (name )
2400+ if err != nil {
2401+ return fmt .Errorf ("failed to get current release: %w" , err )
2402+ }
2403+
2404+ targetChart , err := c .chartForUpgradeTarget (actionConfig , rel , targetVersion , repositoryName , sendProgress )
2405+ if err != nil {
2406+ return err
2407+ }
2408+
2409+ upgradeAction := action .NewUpgrade (actionConfig )
2410+ upgradeAction .Namespace = rel .Namespace
2411+ upgradeAction .Timeout = 120 * time .Second
2412+ upgradeAction .ResetValues = true // WYSIWYG: apply only the edited values
2413+
2414+ sendProgress ("upgrading" , fmt .Sprintf ("Applying %s %s..." , rel .Chart .Metadata .Name , targetVersion ), "" )
2415+
2416+ _ , err = upgradeAction .Run (name , targetChart , newValues )
2417+ if err != nil {
2418+ return fmt .Errorf ("upgrade failed: %w" , err )
2419+ }
2420+
2421+ sendProgress ("complete" , fmt .Sprintf ("Successfully upgraded %s to %s" , name , targetVersion ), "" )
2422+ return nil
2423+ }
2424+
2425+ func (c * Client ) chartForUpgradeTarget (actionConfig * action.Configuration , rel * release.Release , targetVersion , repositoryName string , sendProgress func (phase , message , detail string )) (* chart.Chart , error ) {
2426+ if targetVersion == "" || targetVersion == rel .Chart .Metadata .Version {
2427+ return rel .Chart , nil
2428+ }
2429+ return c .loadTargetChart (actionConfig , rel , targetVersion , repositoryName , sendProgress )
2430+ }
2431+
2432+ // loadTargetChart resolves, downloads and loads the chart for a target upgrade
2433+ // version, refusing a silent chart-swap (the resolved source must publish the
2434+ // same chart the release runs). Shared by the upgrade and preview paths so they
2435+ // resolve the target version identically.
2436+ func (c * Client ) loadTargetChart (actionConfig * action.Configuration , rel * release.Release , targetVersion , repositoryName string , sendProgress func (phase , message , detail string )) (* chart.Chart , error ) {
2437+ chartName := rel .Chart .Metadata .Name
2438+ sendProgress ("resolving" , fmt .Sprintf ("Finding %s version %s in repositories..." , chartName , targetVersion ), "" )
2439+
2440+ chartPath , resolvedRepo , err := c .resolveUpgradeChartPath (chartName , targetVersion , repositoryName , chartSourceHosts (rel .Chart .Metadata .Home , rel .Chart .Metadata .Sources ))
2441+ if err != nil {
2442+ return nil , err
2443+ }
2444+
2445+ sendProgress ("downloading" , fmt .Sprintf ("Downloading %s-%s from %s..." , chartName , targetVersion , resolvedRepo ), chartPath )
2446+
23602447 // Use ChartPathOptions to locate/download the chart
2361- client := action .NewInstall (actionConfig )
2362- client .Version = targetVersion
2448+ locate := action .NewInstall (actionConfig )
2449+ locate .Version = targetVersion
23632450
23642451 // OCI pulls need a registry client on the action; Radar's action config
23652452 // doesn't carry one by default. Wire it from the user's helm registry login.
23662453 if registry .IsOCI (chartPath ) {
23672454 rc , err := c .newRegistryClientConcrete ()
23682455 if err != nil {
2369- return fmt .Errorf ("failed to build OCI registry client: %w" , err )
2456+ return nil , fmt .Errorf ("failed to build OCI registry client: %w" , err )
23702457 }
2371- client .SetRegistryClient (rc )
2458+ locate .SetRegistryClient (rc )
23722459 }
23732460
2374- cp , err := client .ChartPathOptions .LocateChart (chartPath , c .settings )
2461+ cp , err := locate .ChartPathOptions .LocateChart (chartPath , c .settings )
23752462 if err != nil {
2376- return fmt .Errorf ("failed to locate chart: %w" , err )
2463+ return nil , fmt .Errorf ("failed to locate chart: %w" , err )
23772464 }
23782465
23792466 sendProgress ("loading" , "Loading chart..." , cp )
23802467
2381- chart , err := loader .Load (cp )
2468+ targetChart , err := loader .Load (cp )
23822469 if err != nil {
2383- return fmt .Errorf ("failed to load chart: %w" , err )
2470+ return nil , fmt .Errorf ("failed to load chart: %w" , err )
23842471 }
23852472
23862473 // Refuse a silent chart-swap: the resolved source must publish the SAME chart
23872474 // the release runs, not merely a chart at the same version. Matters most for
23882475 // OCI prefix probing, where "<prefix>/<chartName>" is derived, not asserted.
2389- if chart .Metadata != nil && chart .Metadata .Name != chartName {
2390- return fmt .Errorf ("resolved chart is %q but release %q runs chart %q — refusing to swap charts" , chart .Metadata .Name , name , chartName )
2391- }
2392-
2393- sendProgress ("upgrading" , fmt .Sprintf ("Applying %s %s..." , chartName , targetVersion ), "" )
2394-
2395- // Run the upgrade
2396- _ , err = upgradeAction .Run (name , chart , rel .Config )
2397- if err != nil {
2398- return fmt .Errorf ("upgrade failed: %w" , err )
2476+ if targetChart .Metadata != nil && targetChart .Metadata .Name != chartName {
2477+ return nil , fmt .Errorf ("resolved chart is %q but release %q runs chart %q — refusing to swap charts" , targetChart .Metadata .Name , rel .Name , chartName )
23992478 }
24002479
2401- sendProgress ("complete" , fmt .Sprintf ("Successfully upgraded %s to %s" , name , targetVersion ), "" )
2402- return nil
2480+ return targetChart , nil
24032481}
24042482
24052483type chartPathCandidate struct {
@@ -2445,7 +2523,7 @@ func (c *Client) resolveUpgradeChartPathWithOCIResolver(chartName, targetVersion
24452523 continue
24462524 }
24472525 path := entry .URLs [0 ]
2448- if ! strings . HasPrefix (path , "http://" ) && ! strings . HasPrefix ( path , "https://" ) {
2526+ if ! isAbsoluteChartURL (path ) {
24492527 path = strings .TrimSuffix (r .URL , "/" ) + "/" + path
24502528 }
24512529 candidates = append (candidates , chartPathCandidate {repoName : r .Name , repoURL : r .URL , chartPath : path })
@@ -2492,6 +2570,10 @@ func (c *Client) resolveUpgradeChartPathWithOCIResolver(chartName, targetVersion
24922570 return "" , "" , fmt .Errorf ("chart %s version %s not found in configured repositories or registered OCI sources" , chartName , targetVersion )
24932571}
24942572
2573+ func isAbsoluteChartURL (path string ) bool {
2574+ return strings .HasPrefix (path , "http://" ) || strings .HasPrefix (path , "https://" ) || registry .IsOCI (path )
2575+ }
2576+
24952577// BatchCheckUpgrades checks for upgrades for all releases at once (more efficient)
24962578func (c * Client ) BatchCheckUpgrades (namespace string ) (* BatchUpgradeInfo , error ) {
24972579 return c .batchCheckUpgrades (namespace , "" , nil )
@@ -2689,13 +2771,26 @@ func (c *Client) batchCheckUpgrades(namespace, username string, groups []string)
26892771 return result , nil
26902772}
26912773
2692- // PreviewValuesChange previews the effect of new values on a release via dry-run
2693- func (c * Client ) PreviewValuesChange (namespace , name string , newValues map [string ]any ) (* ValuesPreviewResponse , error ) {
2774+ // PreviewValuesChange previews the effect of new values on a release via dry-run.
2775+ // When targetVersion is non-empty and differs from the running chart version, the
2776+ // dry-run renders against that target chart instead of the current chart.
2777+ func (c * Client ) PreviewValuesChange (namespace , name string , newValues map [string ]any , targetVersion , repositoryName string ) (* ValuesPreviewResponse , error ) {
26942778 actionConfig , err := c .getActionConfig (namespace )
26952779 if err != nil {
26962780 return nil , err
26972781 }
2782+ return c .previewValuesChangeWith (actionConfig , name , newValues , targetVersion , repositoryName )
2783+ }
2784+
2785+ func (c * Client ) PreviewValuesChangeAsUser (namespace , name string , newValues map [string ]any , targetVersion , repositoryName string , username string , groups []string ) (* ValuesPreviewResponse , error ) {
2786+ actionConfig , err := c .getActionConfigForUser (namespace , username , groups )
2787+ if err != nil {
2788+ return nil , err
2789+ }
2790+ return c .previewValuesChangeWith (actionConfig , name , newValues , targetVersion , repositoryName )
2791+ }
26982792
2793+ func (c * Client ) previewValuesChangeWith (actionConfig * action.Configuration , name string , newValues map [string ]any , targetVersion , repositoryName string ) (* ValuesPreviewResponse , error ) {
26992794 // Get the current release
27002795 getAction := action .NewGet (actionConfig )
27012796 rel , err := getAction .Run (name )
@@ -2713,6 +2808,12 @@ func (c *Client) PreviewValuesChange(namespace, name string, newValues map[strin
27132808 // Get current manifest
27142809 currentManifest := rel .Manifest
27152810
2811+ noop := func (phase , message , detail string ) {}
2812+ previewChart , err := c .chartForUpgradeTarget (actionConfig , rel , targetVersion , repositoryName , noop )
2813+ if err != nil {
2814+ return nil , err
2815+ }
2816+
27162817 // Perform a dry-run upgrade with the new values
27172818 upgradeAction := action .NewUpgrade (actionConfig )
27182819 upgradeAction .Namespace = rel .Namespace
@@ -2721,7 +2822,7 @@ func (c *Client) PreviewValuesChange(namespace, name string, newValues map[strin
27212822 upgradeAction .ResetValues = true // Use only the provided values, don't merge
27222823
27232824 // Run the dry-run upgrade
2724- newRel , err := upgradeAction .Run (name , rel . Chart , newValues )
2825+ newRel , err := upgradeAction .Run (name , previewChart , newValues )
27252826 if err != nil {
27262827 return nil , fmt .Errorf ("failed to preview values change: %w" , err )
27272828 }
0 commit comments