...

Source file src/helm.sh/helm/v3/pkg/action/rollback.go

Documentation: helm.sh/helm/v3/pkg/action

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package action
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/pkg/errors"
    26  
    27  	"helm.sh/helm/v3/pkg/chartutil"
    28  	"helm.sh/helm/v3/pkg/release"
    29  	helmtime "helm.sh/helm/v3/pkg/time"
    30  )
    31  
    32  // Rollback is the action for rolling back to a given release.
    33  //
    34  // It provides the implementation of 'helm rollback'.
    35  type Rollback struct {
    36  	cfg *Configuration
    37  
    38  	Version       int
    39  	Timeout       time.Duration
    40  	Wait          bool
    41  	WaitForJobs   bool
    42  	DisableHooks  bool
    43  	DryRun        bool
    44  	Recreate      bool // will (if true) recreate pods after a rollback.
    45  	Force         bool // will (if true) force resource upgrade through uninstall/recreate if needed
    46  	CleanupOnFail bool
    47  	MaxHistory    int // MaxHistory limits the maximum number of revisions saved per release
    48  }
    49  
    50  // NewRollback creates a new Rollback object with the given configuration.
    51  func NewRollback(cfg *Configuration) *Rollback {
    52  	return &Rollback{
    53  		cfg: cfg,
    54  	}
    55  }
    56  
    57  // Run executes 'helm rollback' against the given release.
    58  func (r *Rollback) Run(name string) error {
    59  	if err := r.cfg.KubeClient.IsReachable(); err != nil {
    60  		return err
    61  	}
    62  
    63  	r.cfg.Releases.MaxHistory = r.MaxHistory
    64  
    65  	r.cfg.Log("preparing rollback of %s", name)
    66  	currentRelease, targetRelease, err := r.prepareRollback(name)
    67  	if err != nil {
    68  		return err
    69  	}
    70  
    71  	if !r.DryRun {
    72  		r.cfg.Log("creating rolled back release for %s", name)
    73  		if err := r.cfg.Releases.Create(targetRelease); err != nil {
    74  			return err
    75  		}
    76  	}
    77  
    78  	r.cfg.Log("performing rollback of %s", name)
    79  	if _, err := r.performRollback(currentRelease, targetRelease); err != nil {
    80  		return err
    81  	}
    82  
    83  	if !r.DryRun {
    84  		r.cfg.Log("updating status for rolled back release for %s", name)
    85  		if err := r.cfg.Releases.Update(targetRelease); err != nil {
    86  			return err
    87  		}
    88  	}
    89  	return nil
    90  }
    91  
    92  // prepareRollback finds the previous release and prepares a new release object with
    93  // the previous release's configuration
    94  func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Release, error) {
    95  	if err := chartutil.ValidateReleaseName(name); err != nil {
    96  		return nil, nil, errors.Errorf("prepareRollback: Release name is invalid: %s", name)
    97  	}
    98  
    99  	if r.Version < 0 {
   100  		return nil, nil, errInvalidRevision
   101  	}
   102  
   103  	currentRelease, err := r.cfg.Releases.Last(name)
   104  	if err != nil {
   105  		return nil, nil, err
   106  	}
   107  
   108  	previousVersion := r.Version
   109  	if r.Version == 0 {
   110  		previousVersion = currentRelease.Version - 1
   111  	}
   112  
   113  	historyReleases, err := r.cfg.Releases.History(name)
   114  	if err != nil {
   115  		return nil, nil, err
   116  	}
   117  
   118  	// Check if the history version to be rolled back exists
   119  	previousVersionExist := false
   120  	for _, historyRelease := range historyReleases {
   121  		version := historyRelease.Version
   122  		if previousVersion == version {
   123  			previousVersionExist = true
   124  			break
   125  		}
   126  	}
   127  	if !previousVersionExist {
   128  		return nil, nil, errors.Errorf("release has no %d version", previousVersion)
   129  	}
   130  
   131  	r.cfg.Log("rolling back %s (current: v%d, target: v%d)", name, currentRelease.Version, previousVersion)
   132  
   133  	previousRelease, err := r.cfg.Releases.Get(name, previousVersion)
   134  	if err != nil {
   135  		return nil, nil, err
   136  	}
   137  
   138  	// Store a new release object with previous release's configuration
   139  	targetRelease := &release.Release{
   140  		Name:      name,
   141  		Namespace: currentRelease.Namespace,
   142  		Chart:     previousRelease.Chart,
   143  		Config:    previousRelease.Config,
   144  		Info: &release.Info{
   145  			FirstDeployed: currentRelease.Info.FirstDeployed,
   146  			LastDeployed:  helmtime.Now(),
   147  			Status:        release.StatusPendingRollback,
   148  			Notes:         previousRelease.Info.Notes,
   149  			// Because we lose the reference to previous version elsewhere, we set the
   150  			// message here, and only override it later if we experience failure.
   151  			Description: fmt.Sprintf("Rollback to %d", previousVersion),
   152  		},
   153  		Version:  currentRelease.Version + 1,
   154  		Labels:   previousRelease.Labels,
   155  		Manifest: previousRelease.Manifest,
   156  		Hooks:    previousRelease.Hooks,
   157  	}
   158  
   159  	return currentRelease, targetRelease, nil
   160  }
   161  
   162  func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release) (*release.Release, error) {
   163  	if r.DryRun {
   164  		r.cfg.Log("dry run for %s", targetRelease.Name)
   165  		return targetRelease, nil
   166  	}
   167  
   168  	current, err := r.cfg.KubeClient.Build(bytes.NewBufferString(currentRelease.Manifest), false)
   169  	if err != nil {
   170  		return targetRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest")
   171  	}
   172  	target, err := r.cfg.KubeClient.Build(bytes.NewBufferString(targetRelease.Manifest), false)
   173  	if err != nil {
   174  		return targetRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest")
   175  	}
   176  
   177  	// pre-rollback hooks
   178  	if !r.DisableHooks {
   179  		if err := r.cfg.execHook(targetRelease, release.HookPreRollback, r.Timeout); err != nil {
   180  			return targetRelease, err
   181  		}
   182  	} else {
   183  		r.cfg.Log("rollback hooks disabled for %s", targetRelease.Name)
   184  	}
   185  
   186  	// It is safe to use "force" here because these are resources currently rendered by the chart.
   187  	err = target.Visit(setMetadataVisitor(targetRelease.Name, targetRelease.Namespace, true))
   188  	if err != nil {
   189  		return targetRelease, errors.Wrap(err, "unable to set metadata visitor from target release")
   190  	}
   191  	results, err := r.cfg.KubeClient.Update(current, target, r.Force)
   192  
   193  	if err != nil {
   194  		msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err)
   195  		r.cfg.Log("warning: %s", msg)
   196  		currentRelease.Info.Status = release.StatusSuperseded
   197  		targetRelease.Info.Status = release.StatusFailed
   198  		targetRelease.Info.Description = msg
   199  		r.cfg.recordRelease(currentRelease)
   200  		r.cfg.recordRelease(targetRelease)
   201  		if r.CleanupOnFail {
   202  			r.cfg.Log("Cleanup on fail set, cleaning up %d resources", len(results.Created))
   203  			_, errs := r.cfg.KubeClient.Delete(results.Created)
   204  			if errs != nil {
   205  				var errorList []string
   206  				for _, e := range errs {
   207  					errorList = append(errorList, e.Error())
   208  				}
   209  				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)
   210  			}
   211  			r.cfg.Log("Resource cleanup complete")
   212  		}
   213  		return targetRelease, err
   214  	}
   215  
   216  	if r.Recreate {
   217  		// NOTE: Because this is not critical for a release to succeed, we just
   218  		// log if an error occurs and continue onward. If we ever introduce log
   219  		// levels, we should make these error level logs so users are notified
   220  		// that they'll need to go do the cleanup on their own
   221  		if err := recreate(r.cfg, results.Updated); err != nil {
   222  			r.cfg.Log(err.Error())
   223  		}
   224  	}
   225  
   226  	if r.Wait {
   227  		if r.WaitForJobs {
   228  			if err := r.cfg.KubeClient.WaitWithJobs(target, r.Timeout); err != nil {
   229  				targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
   230  				r.cfg.recordRelease(currentRelease)
   231  				r.cfg.recordRelease(targetRelease)
   232  				return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name)
   233  			}
   234  		} else {
   235  			if err := r.cfg.KubeClient.Wait(target, r.Timeout); err != nil {
   236  				targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
   237  				r.cfg.recordRelease(currentRelease)
   238  				r.cfg.recordRelease(targetRelease)
   239  				return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name)
   240  			}
   241  		}
   242  	}
   243  
   244  	// post-rollback hooks
   245  	if !r.DisableHooks {
   246  		if err := r.cfg.execHook(targetRelease, release.HookPostRollback, r.Timeout); err != nil {
   247  			return targetRelease, err
   248  		}
   249  	}
   250  
   251  	deployed, err := r.cfg.Releases.DeployedAll(currentRelease.Name)
   252  	if err != nil && !strings.Contains(err.Error(), "has no deployed releases") {
   253  		return nil, err
   254  	}
   255  	// Supersede all previous deployments, see issue #2941.
   256  	for _, rel := range deployed {
   257  		r.cfg.Log("superseding previous deployment %d", rel.Version)
   258  		rel.Info.Status = release.StatusSuperseded
   259  		r.cfg.recordRelease(rel)
   260  	}
   261  
   262  	targetRelease.Info.Status = release.StatusDeployed
   263  
   264  	return targetRelease, nil
   265  }
   266  

View as plain text