1
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
33
34
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
45 Force bool
46 CleanupOnFail bool
47 MaxHistory int
48 }
49
50
51 func NewRollback(cfg *Configuration) *Rollback {
52 return &Rollback{
53 cfg: cfg,
54 }
55 }
56
57
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
93
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
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
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
150
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
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
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
218
219
220
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
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
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