2019-02-09 08:02:57 +08:00
/ *
Copyright The Helm Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package action
import (
"bytes"
"fmt"
2019-09-24 06:13:02 +08:00
"strings"
2019-10-17 10:01:52 +08:00
"time"
2019-02-09 08:02:57 +08:00
"github.com/pkg/errors"
2025-02-26 04:20:44 +08:00
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
2025-02-26 22:04:32 +08:00
release "helm.sh/helm/v4/pkg/release/v1"
2024-12-27 05:33:51 +08:00
helmtime "helm.sh/helm/v4/pkg/time"
2019-02-09 08:02:57 +08:00
)
// Rollback is the action for rolling back to a given release.
//
// It provides the implementation of 'helm rollback'.
type Rollback struct {
cfg * Configuration
2019-09-24 06:13:02 +08:00
Version int
2019-10-17 10:01:52 +08:00
Timeout time . Duration
2019-09-24 06:13:02 +08:00
Wait bool
2020-11-03 19:48:29 +08:00
WaitForJobs bool
2019-09-24 06:13:02 +08:00
DisableHooks bool
DryRun bool
Recreate bool // will (if true) recreate pods after a rollback.
Force bool // will (if true) force resource upgrade through uninstall/recreate if needed
CleanupOnFail bool
2020-07-09 17:40:36 +08:00
MaxHistory int // MaxHistory limits the maximum number of revisions saved per release
2019-02-09 08:02:57 +08:00
}
// NewRollback creates a new Rollback object with the given configuration.
func NewRollback ( cfg * Configuration ) * Rollback {
return & Rollback {
cfg : cfg ,
}
}
// Run executes 'helm rollback' against the given release.
2019-08-20 01:22:27 +08:00
func ( r * Rollback ) Run ( name string ) error {
2019-08-26 05:46:12 +08:00
if err := r . cfg . KubeClient . IsReachable ( ) ; err != nil {
return err
}
2020-07-09 17:40:36 +08:00
r . cfg . Releases . MaxHistory = r . MaxHistory
2019-02-09 08:02:57 +08:00
r . cfg . Log ( "preparing rollback of %s" , name )
currentRelease , targetRelease , err := r . prepareRollback ( name )
if err != nil {
2019-08-20 01:22:27 +08:00
return err
2019-02-09 08:02:57 +08:00
}
if ! r . DryRun {
r . cfg . Log ( "creating rolled back release for %s" , name )
if err := r . cfg . Releases . Create ( targetRelease ) ; err != nil {
2019-08-20 01:22:27 +08:00
return err
2019-02-09 08:02:57 +08:00
}
}
2019-09-24 06:13:02 +08:00
2019-02-09 08:02:57 +08:00
r . cfg . Log ( "performing rollback of %s" , name )
2019-08-20 01:22:27 +08:00
if _ , err := r . performRollback ( currentRelease , targetRelease ) ; err != nil {
return err
2019-02-09 08:02:57 +08:00
}
if ! r . DryRun {
r . cfg . Log ( "updating status for rolled back release for %s" , name )
if err := r . cfg . Releases . Update ( targetRelease ) ; err != nil {
2019-08-20 01:22:27 +08:00
return err
2019-02-09 08:02:57 +08:00
}
}
2019-08-20 01:22:27 +08:00
return nil
2019-02-09 08:02:57 +08:00
}
// prepareRollback finds the previous release and prepares a new release object with
// the previous release's configuration
func ( r * Rollback ) prepareRollback ( name string ) ( * release . Release , * release . Release , error ) {
2020-09-10 03:30:30 +08:00
if err := chartutil . ValidateReleaseName ( name ) ; err != nil {
2019-02-09 08:02:57 +08:00
return nil , nil , errors . Errorf ( "prepareRollback: Release name is invalid: %s" , name )
}
if r . Version < 0 {
return nil , nil , errInvalidRevision
}
currentRelease , err := r . cfg . Releases . Last ( name )
if err != nil {
return nil , nil , err
}
previousVersion := r . Version
if r . Version == 0 {
previousVersion = currentRelease . Version - 1
}
2023-08-10 10:22:57 +08:00
historyReleases , err := r . cfg . Releases . History ( name )
if err != nil {
return nil , nil , err
}
// Check if the history version to be rolled back exists
previousVersionExist := false
for _ , historyRelease := range historyReleases {
version := historyRelease . Version
if previousVersion == version {
previousVersionExist = true
break
}
}
if ! previousVersionExist {
return nil , nil , errors . Errorf ( "release has no %d version" , previousVersion )
}
2019-02-09 08:02:57 +08:00
r . cfg . Log ( "rolling back %s (current: v%d, target: v%d)" , name , currentRelease . Version , previousVersion )
previousRelease , err := r . cfg . Releases . Get ( name , previousVersion )
if err != nil {
return nil , nil , err
}
// Store a new release object with previous release's configuration
targetRelease := & release . Release {
Name : name ,
Namespace : currentRelease . Namespace ,
Chart : previousRelease . Chart ,
Config : previousRelease . Config ,
Info : & release . Info {
FirstDeployed : currentRelease . Info . FirstDeployed ,
2019-10-17 10:01:52 +08:00
LastDeployed : helmtime . Now ( ) ,
2019-02-09 08:02:57 +08:00
Status : release . StatusPendingRollback ,
Notes : previousRelease . Info . Notes ,
// Because we lose the reference to previous version elsewhere, we set the
// message here, and only override it later if we experience failure.
Description : fmt . Sprintf ( "Rollback to %d" , previousVersion ) ,
} ,
Version : currentRelease . Version + 1 ,
2023-10-27 00:24:40 +08:00
Labels : previousRelease . Labels ,
2019-02-09 08:02:57 +08:00
Manifest : previousRelease . Manifest ,
Hooks : previousRelease . Hooks ,
}
return currentRelease , targetRelease , nil
}
func ( r * Rollback ) performRollback ( currentRelease , targetRelease * release . Release ) ( * release . Release , error ) {
if r . DryRun {
r . cfg . Log ( "dry run for %s" , targetRelease . Name )
return targetRelease , nil
}
2019-10-09 03:55:19 +08:00
current , err := r . cfg . KubeClient . Build ( bytes . NewBufferString ( currentRelease . Manifest ) , false )
2019-07-25 04:24:32 +08:00
if err != nil {
return targetRelease , errors . Wrap ( err , "unable to build kubernetes objects from current release manifest" )
}
2019-10-09 03:55:19 +08:00
target , err := r . cfg . KubeClient . Build ( bytes . NewBufferString ( targetRelease . Manifest ) , false )
2019-07-25 04:24:32 +08:00
if err != nil {
return targetRelease , errors . Wrap ( err , "unable to build kubernetes objects from new release manifest" )
}
2019-02-09 08:02:57 +08:00
// pre-rollback hooks
if ! r . DisableHooks {
2019-07-26 02:45:03 +08:00
if err := r . cfg . execHook ( targetRelease , release . HookPreRollback , r . Timeout ) ; err != nil {
2019-02-09 08:02:57 +08:00
return targetRelease , err
}
} else {
r . cfg . Log ( "rollback hooks disabled for %s" , targetRelease . Name )
}
2021-05-30 02:55:00 +08:00
// It is safe to use "force" here because these are resources currently rendered by the chart.
err = target . Visit ( setMetadataVisitor ( targetRelease . Name , targetRelease . Namespace , true ) )
if err != nil {
return targetRelease , errors . Wrap ( err , "unable to set metadata visitor from target release" )
}
2019-07-25 04:24:32 +08:00
results , err := r . cfg . KubeClient . Update ( current , target , r . Force )
2019-07-04 03:05:47 +08:00
2019-07-25 04:24:32 +08:00
if err != nil {
2019-02-09 08:02:57 +08:00
msg := fmt . Sprintf ( "Rollback %q failed: %s" , targetRelease . Name , err )
r . cfg . Log ( "warning: %s" , msg )
currentRelease . Info . Status = release . StatusSuperseded
targetRelease . Info . Status = release . StatusFailed
targetRelease . Info . Description = msg
2019-03-27 02:11:27 +08:00
r . cfg . recordRelease ( currentRelease )
r . cfg . recordRelease ( targetRelease )
2019-09-24 06:13:02 +08:00
if r . CleanupOnFail {
r . cfg . Log ( "Cleanup on fail set, cleaning up %d resources" , len ( results . Created ) )
_ , errs := r . cfg . KubeClient . Delete ( results . Created )
if errs != nil {
var errorList [ ] string
for _ , e := range errs {
errorList = append ( errorList , e . Error ( ) )
}
return targetRelease , errors . Wrapf ( fmt . Errorf ( "unable to cleanup resources: %s" , strings . Join ( errorList , ", " ) ) , "an error occurred while cleaning up resources. original rollback error: %s" , err )
}
r . cfg . Log ( "Resource cleanup complete" )
}
2019-02-09 08:02:57 +08:00
return targetRelease , err
}
2019-07-25 04:24:32 +08:00
if r . Recreate {
// NOTE: Because this is not critical for a release to succeed, we just
// log if an error occurs and continue onward. If we ever introduce log
// levels, we should make these error level logs so users are notified
// that they'll need to go do the cleanup on their own
2019-07-31 01:48:32 +08:00
if err := recreate ( r . cfg , results . Updated ) ; err != nil {
2019-07-25 04:24:32 +08:00
r . cfg . Log ( err . Error ( ) )
}
}
2019-07-04 03:05:47 +08:00
if r . Wait {
2020-11-05 17:13:15 +08:00
if r . WaitForJobs {
if err := r . cfg . KubeClient . WaitWithJobs ( target , r . Timeout ) ; err != nil {
targetRelease . SetStatus ( release . StatusFailed , fmt . Sprintf ( "Release %q failed: %s" , targetRelease . Name , err . Error ( ) ) )
r . cfg . recordRelease ( currentRelease )
r . cfg . recordRelease ( targetRelease )
return targetRelease , errors . Wrapf ( err , "release %s failed" , targetRelease . Name )
}
} else {
if err := r . cfg . KubeClient . Wait ( target , r . Timeout ) ; err != nil {
targetRelease . SetStatus ( release . StatusFailed , fmt . Sprintf ( "Release %q failed: %s" , targetRelease . Name , err . Error ( ) ) )
r . cfg . recordRelease ( currentRelease )
r . cfg . recordRelease ( targetRelease )
return targetRelease , errors . Wrapf ( err , "release %s failed" , targetRelease . Name )
}
2019-07-04 03:05:47 +08:00
}
}
2019-02-09 08:02:57 +08:00
// post-rollback hooks
if ! r . DisableHooks {
2019-07-26 02:45:03 +08:00
if err := r . cfg . execHook ( targetRelease , release . HookPostRollback , r . Timeout ) ; err != nil {
2019-02-09 08:02:57 +08:00
return targetRelease , err
}
}
deployed , err := r . cfg . Releases . DeployedAll ( currentRelease . Name )
2020-02-22 01:22:42 +08:00
if err != nil && ! strings . Contains ( err . Error ( ) , "has no deployed releases" ) {
2019-02-09 08:02:57 +08:00
return nil , err
}
// Supersede all previous deployments, see issue #2941.
for _ , rel := range deployed {
r . cfg . Log ( "superseding previous deployment %d" , rel . Version )
rel . Info . Status = release . StatusSuperseded
2019-03-27 02:11:27 +08:00
r . cfg . recordRelease ( rel )
2019-02-09 08:02:57 +08:00
}
targetRelease . Info . Status = release . StatusDeployed
return targetRelease , nil
}