github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/action/rollback.go (about)

     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  	"github.com/stefanmcshane/helm/pkg/chartutil"
    28  	"github.com/stefanmcshane/helm/pkg/release"
    29  	helmtime "github.com/stefanmcshane/helm/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  	r.cfg.Log("rolling back %s (current: v%d, target: v%d)", name, currentRelease.Version, previousVersion)
   114  
   115  	previousRelease, err := r.cfg.Releases.Get(name, previousVersion)
   116  	if err != nil {
   117  		return nil, nil, err
   118  	}
   119  
   120  	// Store a new release object with previous release's configuration
   121  	targetRelease := &release.Release{
   122  		Name:      name,
   123  		Namespace: currentRelease.Namespace,
   124  		Chart:     previousRelease.Chart,
   125  		Config:    previousRelease.Config,
   126  		Info: &release.Info{
   127  			FirstDeployed: currentRelease.Info.FirstDeployed,
   128  			LastDeployed:  helmtime.Now(),
   129  			Status:        release.StatusPendingRollback,
   130  			Notes:         previousRelease.Info.Notes,
   131  			// Because we lose the reference to previous version elsewhere, we set the
   132  			// message here, and only override it later if we experience failure.
   133  			Description: fmt.Sprintf("Rollback to %d", previousVersion),
   134  		},
   135  		Version:  currentRelease.Version + 1,
   136  		Manifest: previousRelease.Manifest,
   137  		Hooks:    previousRelease.Hooks,
   138  	}
   139  
   140  	return currentRelease, targetRelease, nil
   141  }
   142  
   143  func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release) (*release.Release, error) {
   144  	if r.DryRun {
   145  		r.cfg.Log("dry run for %s", targetRelease.Name)
   146  		return targetRelease, nil
   147  	}
   148  
   149  	current, err := r.cfg.KubeClient.Build(bytes.NewBufferString(currentRelease.Manifest), false)
   150  	if err != nil {
   151  		return targetRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest")
   152  	}
   153  	target, err := r.cfg.KubeClient.Build(bytes.NewBufferString(targetRelease.Manifest), false)
   154  	if err != nil {
   155  		return targetRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest")
   156  	}
   157  
   158  	// pre-rollback hooks
   159  	if !r.DisableHooks {
   160  		if err := r.cfg.execHook(targetRelease, release.HookPreRollback, r.Timeout); err != nil {
   161  			return targetRelease, err
   162  		}
   163  	} else {
   164  		r.cfg.Log("rollback hooks disabled for %s", targetRelease.Name)
   165  	}
   166  
   167  	// It is safe to use "force" here because these are resources currently rendered by the chart.
   168  	err = target.Visit(setMetadataVisitor(targetRelease.Name, targetRelease.Namespace, true))
   169  	if err != nil {
   170  		return targetRelease, errors.Wrap(err, "unable to set metadata visitor from target release")
   171  	}
   172  	results, err := r.cfg.KubeClient.Update(current, target, r.Force)
   173  
   174  	if err != nil {
   175  		msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err)
   176  		r.cfg.Log("warning: %s", msg)
   177  		currentRelease.Info.Status = release.StatusSuperseded
   178  		targetRelease.Info.Status = release.StatusFailed
   179  		targetRelease.Info.Description = msg
   180  		r.cfg.recordRelease(currentRelease)
   181  		r.cfg.recordRelease(targetRelease)
   182  		if r.CleanupOnFail {
   183  			r.cfg.Log("Cleanup on fail set, cleaning up %d resources", len(results.Created))
   184  			_, errs := r.cfg.KubeClient.Delete(results.Created)
   185  			if errs != nil {
   186  				var errorList []string
   187  				for _, e := range errs {
   188  					errorList = append(errorList, e.Error())
   189  				}
   190  				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)
   191  			}
   192  			r.cfg.Log("Resource cleanup complete")
   193  		}
   194  		return targetRelease, err
   195  	}
   196  
   197  	if r.Recreate {
   198  		// NOTE: Because this is not critical for a release to succeed, we just
   199  		// log if an error occurs and continue onward. If we ever introduce log
   200  		// levels, we should make these error level logs so users are notified
   201  		// that they'll need to go do the cleanup on their own
   202  		if err := recreate(r.cfg, results.Updated); err != nil {
   203  			r.cfg.Log(err.Error())
   204  		}
   205  	}
   206  
   207  	if r.Wait {
   208  		if r.WaitForJobs {
   209  			if err := r.cfg.KubeClient.WaitWithJobs(target, r.Timeout); err != nil {
   210  				targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
   211  				r.cfg.recordRelease(currentRelease)
   212  				r.cfg.recordRelease(targetRelease)
   213  				return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name)
   214  			}
   215  		} else {
   216  			if err := r.cfg.KubeClient.Wait(target, r.Timeout); err != nil {
   217  				targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
   218  				r.cfg.recordRelease(currentRelease)
   219  				r.cfg.recordRelease(targetRelease)
   220  				return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name)
   221  			}
   222  		}
   223  	}
   224  
   225  	// post-rollback hooks
   226  	if !r.DisableHooks {
   227  		if err := r.cfg.execHook(targetRelease, release.HookPostRollback, r.Timeout); err != nil {
   228  			return targetRelease, err
   229  		}
   230  	}
   231  
   232  	deployed, err := r.cfg.Releases.DeployedAll(currentRelease.Name)
   233  	if err != nil && !strings.Contains(err.Error(), "has no deployed releases") {
   234  		return nil, err
   235  	}
   236  	// Supersede all previous deployments, see issue #2941.
   237  	for _, rel := range deployed {
   238  		r.cfg.Log("superseding previous deployment %d", rel.Version)
   239  		rel.Info.Status = release.StatusSuperseded
   240  		r.cfg.recordRelease(rel)
   241  	}
   242  
   243  	targetRelease.Info.Status = release.StatusDeployed
   244  
   245  	return targetRelease, nil
   246  }