@ -18,6 +18,7 @@ package action
import (
"bytes"
"context"
"errors"
"fmt"
"time"
@ -28,6 +29,7 @@ import (
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
"helm.sh/helm/v4/pkg/storage/driver"
)
@ -92,7 +94,7 @@ func (r *Rollback) Run(name string) error {
}
r . cfg . Logger ( ) . Debug ( "performing rollback" , "name" , name )
if _ , err := r . performRollback ( c urrentRelease, targetRelease , serverSideApply ) ; err != nil {
if _ , err := r . performRollback ( c ontext. Background ( ) , c urrentRelease, targetRelease , serverSideApply ) ; err != nil {
return err
}
@ -184,22 +186,28 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
// message here, and only override it later if we experience failure.
Description : fmt . Sprintf ( "Rollback to %d" , previousVersion ) ,
} ,
Version : currentRelease . Version + 1 ,
Labels : previousRelease . Labels ,
Manifest : previousRelease . Manifest ,
Hooks : previousRelease . Hooks ,
ApplyMethod : string ( determineReleaseSSApplyMethod ( serverSideApply ) ) ,
Version : currentRelease . Version + 1 ,
Labels : previousRelease . Labels ,
Manifest : previousRelease . Manifest ,
Hooks : previousRelease . Hooks ,
ApplyMethod : string ( determineReleaseSSApplyMethod ( serverSideApply ) ) ,
SequencingInfo : previousRelease . SequencingInfo ,
}
return currentRelease , targetRelease , serverSideApply , nil
}
func ( r * Rollback ) performRollback ( c urrentRelease, targetRelease * release . Release , serverSideApply bool ) ( * release . Release , error ) {
func ( r * Rollback ) performRollback ( c tx context . Context , c urrentRelease, targetRelease * release . Release , serverSideApply bool ) ( * release . Release , error ) {
if isDryRun ( r . DryRunStrategy ) {
r . cfg . Logger ( ) . Debug ( "dry run" , "name" , targetRelease . Name )
return targetRelease , nil
}
// Use sequenced rollback when the target revision was originally deployed with sequencing.
if targetRelease . SequencingInfo != nil && targetRelease . SequencingInfo . Enabled {
return r . performSequencedRollback ( ctx , currentRelease , targetRelease , serverSideApply )
}
current , err := r . cfg . KubeClient . Build ( bytes . NewBufferString ( currentRelease . Manifest ) , false )
if err != nil {
return targetRelease , fmt . Errorf ( "unable to build kubernetes objects from current release manifest: %w" , err )
@ -304,3 +312,109 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
return targetRelease , nil
}
// performSequencedRollback deploys the target revision's resources in DAG-ordered batches
// when the target revision was originally deployed with --wait=ordered.
func ( r * Rollback ) performSequencedRollback ( ctx context . Context , currentRelease , targetRelease * release . Release , serverSideApply bool ) ( * release . Release , error ) {
// Build the full set of current (deployed) resources for diff matching.
current , err := r . cfg . KubeClient . Build ( bytes . NewBufferString ( currentRelease . Manifest ) , false )
if err != nil {
return targetRelease , fmt . Errorf ( "unable to build kubernetes objects from current release manifest: %w" , err )
}
// Build the full set of target resources for computing deletions.
target , err := r . cfg . KubeClient . Build ( bytes . NewBufferString ( targetRelease . Manifest ) , false )
if err != nil {
return targetRelease , fmt . Errorf ( "unable to build kubernetes objects from target release manifest: %w" , err )
}
// Parse the target manifest into sorted manifests for DAG ordering.
rawManifests := releaseutil . SplitManifests ( targetRelease . Manifest )
_ , sortedManifests , err := releaseutil . SortManifests ( rawManifests , nil , releaseutil . InstallOrder )
if err != nil {
return targetRelease , fmt . Errorf ( "parsing target release manifest for sequenced rollback: %w" , err )
}
// pre-rollback hooks
if ! r . DisableHooks {
if err := r . cfg . execHook ( targetRelease , release . HookPreRollback , r . WaitStrategy , r . WaitOptions , r . Timeout , serverSideApply ) ; err != nil {
return targetRelease , err
}
} else {
r . cfg . Logger ( ) . Debug ( "rollback hooks disabled" , "name" , targetRelease . Name )
}
readinessTimeout := r . Timeout / 2 // sensible default: half the overall timeout
if readinessTimeout <= 0 {
readinessTimeout = time . Minute
}
sd := & sequencedDeployment {
cfg : r . cfg ,
releaseName : targetRelease . Name ,
releaseNamespace : targetRelease . Namespace ,
serverSideApply : serverSideApply ,
forceConflicts : r . ForceConflicts ,
forceReplace : r . ForceReplace ,
waitStrategy : r . WaitStrategy ,
waitOptions : r . WaitOptions ,
waitForJobs : r . WaitForJobs ,
timeout : r . Timeout ,
readinessTimeout : readinessTimeout ,
deadline : time . Now ( ) . Add ( r . Timeout ) ,
upgradeMode : true ,
currentResources : current ,
// upgradeCSAFieldManager: always true for rollback (same as existing performRollback)
upgradeCSAFieldManager : true ,
}
chrt := targetRelease . Chart
if err := sd . deployChartLevel ( ctx , chrt , sortedManifests ) ; err != nil {
targetRelease . SetStatus ( common . StatusFailed , fmt . Sprintf ( "Rollback %q failed: %s" , targetRelease . Name , err ) )
r . cfg . recordRelease ( currentRelease )
r . cfg . recordRelease ( targetRelease )
return targetRelease , err
}
// Delete resources that were removed in the rollback target (in current but not in target).
allTargetKeys := make ( map [ string ] bool , len ( target ) )
for _ , res := range target {
allTargetKeys [ objectKey ( res ) ] = true
}
var toBeDeleted kube . ResourceList
for _ , res := range current {
if ! allTargetKeys [ objectKey ( res ) ] {
toBeDeleted = append ( toBeDeleted , res )
}
}
if len ( toBeDeleted ) > 0 {
if _ , errs := r . cfg . KubeClient . Delete ( toBeDeleted , metav1 . DeletePropagationBackground ) ; errs != nil {
return targetRelease , fmt . Errorf ( "deleting removed resources during rollback: %w" , joinErrors ( errs , ", " ) )
}
}
// post-rollback hooks
if ! r . DisableHooks {
if err := r . cfg . execHook ( targetRelease , release . HookPostRollback , r . WaitStrategy , r . WaitOptions , r . Timeout , serverSideApply ) ; err != nil {
return targetRelease , err
}
}
deployed , err := r . cfg . Releases . DeployedAll ( currentRelease . Name )
if err != nil && ! errors . Is ( err , driver . ErrNoDeployedReleases ) {
return nil , err
}
for _ , reli := range deployed {
rel , err := releaserToV1Release ( reli )
if err != nil {
return nil , err
}
r . cfg . Logger ( ) . Debug ( "superseding previous deployment" , "version" , rel . Version )
rel . Info . Status = common . StatusSuperseded
r . cfg . recordRelease ( rel )
}
targetRelease . Info . Status = common . StatusDeployed
return targetRelease , nil
}