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 }