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 }