github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/action/upgrade.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  	"context"
    22  	"fmt"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/pkg/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/cli-runtime/pkg/resource"
    30  
    31  	"github.com/stefanmcshane/helm/pkg/chart"
    32  	"github.com/stefanmcshane/helm/pkg/chartutil"
    33  	"github.com/stefanmcshane/helm/pkg/kube"
    34  	"github.com/stefanmcshane/helm/pkg/postrender"
    35  	"github.com/stefanmcshane/helm/pkg/release"
    36  	"github.com/stefanmcshane/helm/pkg/releaseutil"
    37  	"github.com/stefanmcshane/helm/pkg/storage/driver"
    38  )
    39  
    40  // Upgrade is the action for upgrading releases.
    41  //
    42  // It provides the implementation of 'helm upgrade'.
    43  type Upgrade struct {
    44  	cfg *Configuration
    45  
    46  	ChartPathOptions
    47  
    48  	// Install is a purely informative flag that indicates whether this upgrade was done in "install" mode.
    49  	//
    50  	// Applications may use this to determine whether this Upgrade operation was done as part of a
    51  	// pure upgrade (Upgrade.Install == false) or as part of an install-or-upgrade operation
    52  	// (Upgrade.Install == true).
    53  	//
    54  	// Setting this to `true` will NOT cause `Upgrade` to perform an install if the release does not exist.
    55  	// That process must be handled by creating an Install action directly. See cmd/upgrade.go for an
    56  	// example of how this flag is used.
    57  	Install bool
    58  	// Devel indicates that the operation is done in devel mode.
    59  	Devel bool
    60  	// Namespace is the namespace in which this operation should be performed.
    61  	Namespace string
    62  	// SkipCRDs skips installing CRDs when install flag is enabled during upgrade
    63  	SkipCRDs bool
    64  	// Timeout is the timeout for this operation
    65  	Timeout time.Duration
    66  	// Wait determines whether the wait operation should be performed after the upgrade is requested.
    67  	Wait bool
    68  	// WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested.
    69  	WaitForJobs bool
    70  	// DisableHooks disables hook processing if set to true.
    71  	DisableHooks bool
    72  	// DryRun controls whether the operation is prepared, but not executed.
    73  	// If `true`, the upgrade is prepared but not performed.
    74  	DryRun bool
    75  	// Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway.
    76  	//
    77  	// This should be used with caution.
    78  	Force bool
    79  	// ResetValues will reset the values to the chart's built-ins rather than merging with existing.
    80  	ResetValues bool
    81  	// ReuseValues will re-use the user's last supplied values.
    82  	ReuseValues bool
    83  	// Recreate will (if true) recreate pods after a rollback.
    84  	Recreate bool
    85  	// MaxHistory limits the maximum number of revisions saved per release
    86  	MaxHistory int
    87  	// Atomic, if true, will roll back on failure.
    88  	Atomic bool
    89  	// CleanupOnFail will, if true, cause the upgrade to delete newly-created resources on a failed update.
    90  	CleanupOnFail bool
    91  	// SubNotes determines whether sub-notes are rendered in the chart.
    92  	SubNotes bool
    93  	// Description is the description of this operation
    94  	Description string
    95  	// PostRender is an optional post-renderer
    96  	//
    97  	// If this is non-nil, then after templates are rendered, they will be sent to the
    98  	// post renderer before sending to the Kubernetes API server.
    99  	PostRenderer postrender.PostRenderer
   100  	// DisableOpenAPIValidation controls whether OpenAPI validation is enforced.
   101  	DisableOpenAPIValidation bool
   102  	// Get missing dependencies
   103  	DependencyUpdate bool
   104  	// Lock to control raceconditions when the process receives a SIGTERM
   105  	Lock sync.Mutex
   106  }
   107  
   108  type resultMessage struct {
   109  	r *release.Release
   110  	e error
   111  }
   112  
   113  // NewUpgrade creates a new Upgrade object with the given configuration.
   114  func NewUpgrade(cfg *Configuration) *Upgrade {
   115  	up := &Upgrade{
   116  		cfg: cfg,
   117  	}
   118  	up.ChartPathOptions.registryClient = cfg.RegistryClient
   119  
   120  	return up
   121  }
   122  
   123  // Run executes the upgrade on the given release.
   124  func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
   125  	ctx := context.Background()
   126  	return u.RunWithContext(ctx, name, chart, vals)
   127  }
   128  
   129  // RunWithContext executes the upgrade on the given release with context.
   130  func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
   131  	if err := u.cfg.KubeClient.IsReachable(); err != nil {
   132  		return nil, err
   133  	}
   134  
   135  	// Make sure if Atomic is set, that wait is set as well. This makes it so
   136  	// the user doesn't have to specify both
   137  	u.Wait = u.Wait || u.Atomic
   138  
   139  	if err := chartutil.ValidateReleaseName(name); err != nil {
   140  		return nil, errors.Errorf("release name is invalid: %s", name)
   141  	}
   142  	u.cfg.Log("preparing upgrade for %s", name)
   143  	currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	u.cfg.Releases.MaxHistory = u.MaxHistory
   149  
   150  	u.cfg.Log("performing update for %s", name)
   151  	res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease)
   152  	if err != nil {
   153  		return res, err
   154  	}
   155  
   156  	if !u.DryRun {
   157  		u.cfg.Log("updating status for upgraded release for %s", name)
   158  		if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
   159  			return res, err
   160  		}
   161  	}
   162  
   163  	return res, nil
   164  }
   165  
   166  // prepareUpgrade builds an upgraded release for an upgrade operation.
   167  func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) {
   168  	if chart == nil {
   169  		return nil, nil, errMissingChart
   170  	}
   171  
   172  	// finds the last non-deleted release with the given name
   173  	lastRelease, err := u.cfg.Releases.Last(name)
   174  	if err != nil {
   175  		// to keep existing behavior of returning the "%q has no deployed releases" error when an existing release does not exist
   176  		if errors.Is(err, driver.ErrReleaseNotFound) {
   177  			return nil, nil, driver.NewErrNoDeployedReleases(name)
   178  		}
   179  		return nil, nil, err
   180  	}
   181  
   182  	// Concurrent `helm upgrade`s will either fail here with `errPending` or when creating the release with "already exists". This should act as a pessimistic lock.
   183  	if lastRelease.Info.Status.IsPending() {
   184  		return nil, nil, errPending
   185  	}
   186  
   187  	var currentRelease *release.Release
   188  	if lastRelease.Info.Status == release.StatusDeployed {
   189  		// no need to retrieve the last deployed release from storage as the last release is deployed
   190  		currentRelease = lastRelease
   191  	} else {
   192  		// finds the deployed release with the given name
   193  		currentRelease, err = u.cfg.Releases.Deployed(name)
   194  		if err != nil {
   195  			if errors.Is(err, driver.ErrNoDeployedReleases) &&
   196  				(lastRelease.Info.Status == release.StatusFailed || lastRelease.Info.Status == release.StatusSuperseded) {
   197  				currentRelease = lastRelease
   198  			} else {
   199  				return nil, nil, err
   200  			}
   201  		}
   202  	}
   203  
   204  	// determine if values will be reused
   205  	vals, err = u.reuseValues(chart, currentRelease, vals)
   206  	if err != nil {
   207  		return nil, nil, err
   208  	}
   209  
   210  	if err := chartutil.ProcessDependencies(chart, vals); err != nil {
   211  		return nil, nil, err
   212  	}
   213  
   214  	// Increment revision count. This is passed to templates, and also stored on
   215  	// the release object.
   216  	revision := lastRelease.Version + 1
   217  
   218  	options := chartutil.ReleaseOptions{
   219  		Name:      name,
   220  		Namespace: currentRelease.Namespace,
   221  		Revision:  revision,
   222  		IsUpgrade: true,
   223  	}
   224  
   225  	caps, err := u.cfg.getCapabilities()
   226  	if err != nil {
   227  		return nil, nil, err
   228  	}
   229  	valuesToRender, err := chartutil.ToRenderValues(chart, vals, options, caps)
   230  	if err != nil {
   231  		return nil, nil, err
   232  	}
   233  
   234  	hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun)
   235  	if err != nil {
   236  		return nil, nil, err
   237  	}
   238  
   239  	// Store an upgraded release.
   240  	upgradedRelease := &release.Release{
   241  		Name:      name,
   242  		Namespace: currentRelease.Namespace,
   243  		Chart:     chart,
   244  		Config:    vals,
   245  		Info: &release.Info{
   246  			FirstDeployed: currentRelease.Info.FirstDeployed,
   247  			LastDeployed:  Timestamper(),
   248  			Status:        release.StatusPendingUpgrade,
   249  			Description:   "Preparing upgrade", // This should be overwritten later.
   250  		},
   251  		Version:  revision,
   252  		Manifest: manifestDoc.String(),
   253  		Hooks:    hooks,
   254  	}
   255  
   256  	if len(notesTxt) > 0 {
   257  		upgradedRelease.Info.Notes = notesTxt
   258  	}
   259  	err = validateManifest(u.cfg.KubeClient, manifestDoc.Bytes(), !u.DisableOpenAPIValidation)
   260  	return currentRelease, upgradedRelease, err
   261  }
   262  
   263  func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedRelease *release.Release) (*release.Release, error) {
   264  	current, err := u.cfg.KubeClient.Build(bytes.NewBufferString(originalRelease.Manifest), false)
   265  	if err != nil {
   266  		// Checking for removed Kubernetes API error so can provide a more informative error message to the user
   267  		// Ref: https://github.com/helm/helm/issues/7219
   268  		if strings.Contains(err.Error(), "unable to recognize \"\": no matches for kind") {
   269  			return upgradedRelease, errors.Wrap(err, "current release manifest contains removed kubernetes api(s) for this "+
   270  				"kubernetes version and it is therefore unable to build the kubernetes "+
   271  				"objects for performing the diff. error from kubernetes")
   272  		}
   273  		return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest")
   274  	}
   275  	target, err := u.cfg.KubeClient.Build(bytes.NewBufferString(upgradedRelease.Manifest), !u.DisableOpenAPIValidation)
   276  	if err != nil {
   277  		return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest")
   278  	}
   279  
   280  	// It is safe to use force only on target because these are resources currently rendered by the chart.
   281  	err = target.Visit(setMetadataVisitor(upgradedRelease.Name, upgradedRelease.Namespace, true))
   282  	if err != nil {
   283  		return upgradedRelease, err
   284  	}
   285  
   286  	// Do a basic diff using gvk + name to figure out what new resources are being created so we can validate they don't already exist
   287  	existingResources := make(map[string]bool)
   288  	for _, r := range current {
   289  		existingResources[objectKey(r)] = true
   290  	}
   291  
   292  	var toBeCreated kube.ResourceList
   293  	for _, r := range target {
   294  		if !existingResources[objectKey(r)] {
   295  			toBeCreated = append(toBeCreated, r)
   296  		}
   297  	}
   298  
   299  	toBeUpdated, err := existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace)
   300  	if err != nil {
   301  		return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with update")
   302  	}
   303  
   304  	toBeUpdated.Visit(func(r *resource.Info, err error) error {
   305  		if err != nil {
   306  			return err
   307  		}
   308  		current.Append(r)
   309  		return nil
   310  	})
   311  
   312  	if u.DryRun {
   313  		u.cfg.Log("dry run for %s", upgradedRelease.Name)
   314  		if len(u.Description) > 0 {
   315  			upgradedRelease.Info.Description = u.Description
   316  		} else {
   317  			upgradedRelease.Info.Description = "Dry run complete"
   318  		}
   319  		return upgradedRelease, nil
   320  	}
   321  
   322  	u.cfg.Log("creating upgraded release for %s", upgradedRelease.Name)
   323  	if err := u.cfg.Releases.Create(upgradedRelease); err != nil {
   324  		return nil, err
   325  	}
   326  	rChan := make(chan resultMessage)
   327  	ctxChan := make(chan resultMessage)
   328  	doneChan := make(chan interface{})
   329  	defer close(doneChan)
   330  	go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease)
   331  	go u.handleContext(ctx, doneChan, ctxChan, upgradedRelease)
   332  	select {
   333  	case result := <-rChan:
   334  		return result.r, result.e
   335  	case result := <-ctxChan:
   336  		return result.r, result.e
   337  	}
   338  }
   339  
   340  // Function used to lock the Mutex, this is important for the case when the atomic flag is set.
   341  // In that case the upgrade will finish before the rollback is finished so it is necessary to wait for the rollback to finish.
   342  // The rollback will be trigger by the function failRelease
   343  func (u *Upgrade) reportToPerformUpgrade(c chan<- resultMessage, rel *release.Release, created kube.ResourceList, err error) {
   344  	u.Lock.Lock()
   345  	if err != nil {
   346  		rel, err = u.failRelease(rel, created, err)
   347  	}
   348  	c <- resultMessage{r: rel, e: err}
   349  	u.Lock.Unlock()
   350  }
   351  
   352  // Setup listener for SIGINT and SIGTERM
   353  func (u *Upgrade) handleContext(ctx context.Context, done chan interface{}, c chan<- resultMessage, upgradedRelease *release.Release) {
   354  	select {
   355  	case <-ctx.Done():
   356  		err := ctx.Err()
   357  
   358  		// when the atomic flag is set the ongoing release finish first and doesn't give time for the rollback happens.
   359  		u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, err)
   360  	case <-done:
   361  		return
   362  	}
   363  }
   364  func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) {
   365  	// pre-upgrade hooks
   366  
   367  	if !u.DisableHooks {
   368  		if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil {
   369  			u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err))
   370  			return
   371  		}
   372  	} else {
   373  		u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name)
   374  	}
   375  
   376  	results, err := u.cfg.KubeClient.Update(current, target, u.Force)
   377  	if err != nil {
   378  		u.cfg.recordRelease(originalRelease)
   379  		u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
   380  		return
   381  	}
   382  
   383  	if u.Recreate {
   384  		// NOTE: Because this is not critical for a release to succeed, we just
   385  		// log if an error occurs and continue onward. If we ever introduce log
   386  		// levels, we should make these error level logs so users are notified
   387  		// that they'll need to go do the cleanup on their own
   388  		if err := recreate(u.cfg, results.Updated); err != nil {
   389  			u.cfg.Log(err.Error())
   390  		}
   391  	}
   392  
   393  	if u.Wait {
   394  		u.cfg.Log(
   395  			"waiting for release %s resources (created: %d updated: %d  deleted: %d)",
   396  			upgradedRelease.Name, len(results.Created), len(results.Updated), len(results.Deleted))
   397  		if u.WaitForJobs {
   398  			if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil {
   399  				u.cfg.recordRelease(originalRelease)
   400  				u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
   401  				return
   402  			}
   403  		} else {
   404  			if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil {
   405  				u.cfg.recordRelease(originalRelease)
   406  				u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
   407  				return
   408  			}
   409  		}
   410  	}
   411  
   412  	// post-upgrade hooks
   413  	if !u.DisableHooks {
   414  		if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil {
   415  			u.reportToPerformUpgrade(c, upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err))
   416  			return
   417  		}
   418  	}
   419  
   420  	originalRelease.Info.Status = release.StatusSuperseded
   421  	u.cfg.recordRelease(originalRelease)
   422  
   423  	upgradedRelease.Info.Status = release.StatusDeployed
   424  	if len(u.Description) > 0 {
   425  		upgradedRelease.Info.Description = u.Description
   426  	} else {
   427  		upgradedRelease.Info.Description = "Upgrade complete"
   428  	}
   429  	u.reportToPerformUpgrade(c, upgradedRelease, nil, nil)
   430  }
   431  
   432  func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) {
   433  	msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err)
   434  	u.cfg.Log("warning: %s", msg)
   435  
   436  	rel.Info.Status = release.StatusFailed
   437  	rel.Info.Description = msg
   438  	u.cfg.recordRelease(rel)
   439  	if u.CleanupOnFail && len(created) > 0 {
   440  		u.cfg.Log("Cleanup on fail set, cleaning up %d resources", len(created))
   441  		_, errs := u.cfg.KubeClient.Delete(created)
   442  		if errs != nil {
   443  			var errorList []string
   444  			for _, e := range errs {
   445  				errorList = append(errorList, e.Error())
   446  			}
   447  			return rel, errors.Wrapf(fmt.Errorf("unable to cleanup resources: %s", strings.Join(errorList, ", ")), "an error occurred while cleaning up resources. original upgrade error: %s", err)
   448  		}
   449  		u.cfg.Log("Resource cleanup complete")
   450  	}
   451  	if u.Atomic {
   452  		u.cfg.Log("Upgrade failed and atomic is set, rolling back to last successful release")
   453  
   454  		// As a protection, get the last successful release before rollback.
   455  		// If there are no successful releases, bail out
   456  		hist := NewHistory(u.cfg)
   457  		fullHistory, herr := hist.Run(rel.Name)
   458  		if herr != nil {
   459  			return rel, errors.Wrapf(herr, "an error occurred while finding last successful release. original upgrade error: %s", err)
   460  		}
   461  
   462  		// There isn't a way to tell if a previous release was successful, but
   463  		// generally failed releases do not get superseded unless the next
   464  		// release is successful, so this should be relatively safe
   465  		filteredHistory := releaseutil.FilterFunc(func(r *release.Release) bool {
   466  			return r.Info.Status == release.StatusSuperseded || r.Info.Status == release.StatusDeployed
   467  		}).Filter(fullHistory)
   468  		if len(filteredHistory) == 0 {
   469  			return rel, errors.Wrap(err, "unable to find a previously successful release when attempting to rollback. original upgrade error")
   470  		}
   471  
   472  		releaseutil.Reverse(filteredHistory, releaseutil.SortByRevision)
   473  
   474  		rollin := NewRollback(u.cfg)
   475  		rollin.Version = filteredHistory[0].Version
   476  		rollin.Wait = true
   477  		rollin.WaitForJobs = u.WaitForJobs
   478  		rollin.DisableHooks = u.DisableHooks
   479  		rollin.Recreate = u.Recreate
   480  		rollin.Force = u.Force
   481  		rollin.Timeout = u.Timeout
   482  		if rollErr := rollin.Run(rel.Name); rollErr != nil {
   483  			return rel, errors.Wrapf(rollErr, "an error occurred while rolling back the release. original upgrade error: %s", err)
   484  		}
   485  		return rel, errors.Wrapf(err, "release %s failed, and has been rolled back due to atomic being set", rel.Name)
   486  	}
   487  
   488  	return rel, err
   489  }
   490  
   491  // reuseValues copies values from the current release to a new release if the
   492  // new release does not have any values.
   493  //
   494  // If the request already has values, or if there are no values in the current
   495  // release, this does nothing.
   496  //
   497  // This is skipped if the u.ResetValues flag is set, in which case the
   498  // request values are not altered.
   499  func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) {
   500  	if u.ResetValues {
   501  		// If ResetValues is set, we completely ignore current.Config.
   502  		u.cfg.Log("resetting values to the chart's original version")
   503  		return newVals, nil
   504  	}
   505  
   506  	// If the ReuseValues flag is set, we always copy the old values over the new config's values.
   507  	if u.ReuseValues {
   508  		u.cfg.Log("reusing the old release's values")
   509  
   510  		// We have to regenerate the old coalesced values:
   511  		oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config)
   512  		if err != nil {
   513  			return nil, errors.Wrap(err, "failed to rebuild old values")
   514  		}
   515  
   516  		newVals = chartutil.CoalesceTables(newVals, current.Config)
   517  
   518  		chart.Values = oldVals
   519  
   520  		return newVals, nil
   521  	}
   522  
   523  	if len(newVals) == 0 && len(current.Config) > 0 {
   524  		u.cfg.Log("copying values from %s (v%d) to new release.", current.Name, current.Version)
   525  		newVals = current.Config
   526  	}
   527  	return newVals, nil
   528  }
   529  
   530  func validateManifest(c kube.Interface, manifest []byte, openAPIValidation bool) error {
   531  	_, err := c.Build(bytes.NewReader(manifest), openAPIValidation)
   532  	return err
   533  }
   534  
   535  // recreate captures all the logic for recreating pods for both upgrade and
   536  // rollback. If we end up refactoring rollback to use upgrade, this can just be
   537  // made an unexported method on the upgrade action.
   538  func recreate(cfg *Configuration, resources kube.ResourceList) error {
   539  	for _, res := range resources {
   540  		versioned := kube.AsVersioned(res)
   541  		selector, err := kube.SelectorsForObject(versioned)
   542  		if err != nil {
   543  			// If no selector is returned, it means this object is
   544  			// definitely not a pod, so continue onward
   545  			continue
   546  		}
   547  
   548  		client, err := cfg.KubernetesClientSet()
   549  		if err != nil {
   550  			return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name)
   551  		}
   552  
   553  		pods, err := client.CoreV1().Pods(res.Namespace).List(context.Background(), metav1.ListOptions{
   554  			LabelSelector: selector.String(),
   555  		})
   556  		if err != nil {
   557  			return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name)
   558  		}
   559  
   560  		// Restart pods
   561  		for _, pod := range pods.Items {
   562  			// Delete each pod for get them restarted with changed spec.
   563  			if err := client.CoreV1().Pods(pod.Namespace).Delete(context.Background(), pod.Name, *metav1.NewPreconditionDeleteOptions(string(pod.UID))); err != nil {
   564  				return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name)
   565  			}
   566  		}
   567  	}
   568  	return nil
   569  }
   570  
   571  func objectKey(r *resource.Info) string {
   572  	gvk := r.Object.GetObjectKind().GroupVersionKind()
   573  	return fmt.Sprintf("%s/%s/%s/%s", gvk.GroupVersion().String(), gvk.Kind, r.Namespace, r.Name)
   574  }