Skip to content

Commit 5ea54f9

Browse files
authored
Rollback action (#9)
* Show rollback confirm * Implement rollback backend * Refactoring * Refactoring
1 parent fa48cf5 commit 5ea54f9

File tree

5 files changed

+155
-76
lines changed

5 files changed

+155
-76
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,5 @@ Adding new repository
7070
Recognise & show ArgoCD-originating charts/objects
7171
Have cleaner idea on the web API structure
7272
See if we can build in Chechov or Validkube validation
73-
Show manifest/describe upon clicking on resource
73+
Show manifest/describe upon clicking on resource
74+
Recognise the revisions that are rollbacks by their description and mark in timeline

pkg/dashboard/api.go

Lines changed: 94 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,25 @@ func configureHelms(api *gin.Engine, data *DataLayer) {
7474
})
7575

7676
api.DELETE("/api/helm/charts", func(c *gin.Context) {
77-
cName := c.Query("chart")
78-
cNamespace := c.Query("namespace")
79-
if cName == "" {
80-
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
77+
qp, err := getQueryProps(c, false)
78+
if err != nil {
79+
_ = c.AbortWithError(http.StatusBadRequest, err)
80+
}
81+
err = data.UninstallChart(qp.Namespace, qp.Name)
82+
if err != nil {
83+
_ = c.AbortWithError(http.StatusInternalServerError, err)
8184
return
8285
}
83-
err := data.UninstallChart(cNamespace, cName)
86+
c.Redirect(http.StatusFound, "/")
87+
})
88+
89+
api.POST("/api/helm/charts/rollback", func(c *gin.Context) {
90+
qp, err := getQueryProps(c, true)
91+
if err != nil {
92+
_ = c.AbortWithError(http.StatusBadRequest, err)
93+
}
94+
95+
err = data.Revert(qp.Namespace, qp.Name, qp.Revision)
8496
if err != nil {
8597
_ = c.AbortWithError(http.StatusInternalServerError, err)
8698
return
@@ -89,14 +101,12 @@ func configureHelms(api *gin.Engine, data *DataLayer) {
89101
})
90102

91103
api.GET("/api/helm/charts/history", func(c *gin.Context) {
92-
cName := c.Query("chart")
93-
cNamespace := c.Query("namespace")
94-
if cName == "" {
95-
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
96-
return
104+
qp, err := getQueryProps(c, false)
105+
if err != nil {
106+
_ = c.AbortWithError(http.StatusBadRequest, err)
97107
}
98108

99-
res, err := data.ChartHistory(cNamespace, cName)
109+
res, err := data.ChartHistory(qp.Namespace, qp.Name)
100110
if err != nil {
101111
_ = c.AbortWithError(http.StatusInternalServerError, err)
102112
return
@@ -105,80 +115,70 @@ func configureHelms(api *gin.Engine, data *DataLayer) {
105115
})
106116

107117
api.GET("/api/helm/charts/resources", func(c *gin.Context) {
108-
cName := c.Query("chart")
109-
cNamespace := c.Query("namespace")
110-
if cName == "" {
111-
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
112-
return
118+
qp, err := getQueryProps(c, true)
119+
if err != nil {
120+
_ = c.AbortWithError(http.StatusBadRequest, err)
113121
}
114-
cRev, err := strconv.Atoi(c.Query("revision"))
122+
123+
res, err := data.RevisionManifestsParsed(qp.Namespace, qp.Name, qp.Revision)
115124
if err != nil {
116125
_ = c.AbortWithError(http.StatusInternalServerError, err)
117126
return
118127
}
128+
c.IndentedJSON(http.StatusOK, res)
129+
})
130+
131+
api.GET("/api/helm/charts/:section", func(c *gin.Context) {
132+
qp, err := getQueryProps(c, true)
133+
if err != nil {
134+
_ = c.AbortWithError(http.StatusBadRequest, err)
135+
}
119136

120-
res, err := data.RevisionManifestsParsed(cNamespace, cName, cRev)
137+
flag := c.Query("flag") == "true"
138+
rDiff := c.Query("revisionDiff")
139+
res, err := handleGetSection(data, c.Param("section"), rDiff, qp, flag)
121140
if err != nil {
122141
_ = c.AbortWithError(http.StatusInternalServerError, err)
123-
return
124142
}
125-
c.IndentedJSON(http.StatusOK, res)
143+
c.String(http.StatusOK, res)
126144
})
145+
}
127146

147+
func handleGetSection(data *DataLayer, section string, rDiff string, qp *QueryProps, flag bool) (string, error) {
128148
sections := map[string]SectionFn{
129149
"manifests": data.RevisionManifests,
130150
"values": data.RevisionValues,
131151
"notes": data.RevisionNotes,
132152
}
133153

134-
api.GET("/api/helm/charts/:section", func(c *gin.Context) {
135-
functor, found := sections[c.Param("section")]
136-
if !found {
137-
_ = c.AbortWithError(http.StatusNotFound, errors.New("unsupported section: "+c.Param("section")))
138-
return
154+
functor, found := sections[section]
155+
if !found {
156+
return "", errors.New("unsupported section: " + section)
157+
}
158+
159+
if rDiff != "" {
160+
cRevDiff, err := strconv.Atoi(rDiff)
161+
if err != nil {
162+
return "", err
139163
}
140164

141-
cName := c.Query("chart")
142-
cNamespace := c.Query("namespace")
143-
if cName == "" {
144-
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
145-
return
165+
ext := ".yaml"
166+
if section == "notes" {
167+
ext = ".txt"
146168
}
147169

148-
cRev, err := strconv.Atoi(c.Query("revision"))
170+
res, err := RevisionDiff(functor, ext, qp.Namespace, qp.Name, cRevDiff, qp.Revision, flag)
149171
if err != nil {
150-
_ = c.AbortWithError(http.StatusInternalServerError, err)
151-
return
172+
return "", err
152173
}
153-
flag := c.Query("flag") == "true"
154-
rDiff := c.Query("revisionDiff")
155-
if rDiff != "" {
156-
cRevDiff, err := strconv.Atoi(rDiff)
157-
if err != nil {
158-
_ = c.AbortWithError(http.StatusInternalServerError, err)
159-
return
160-
}
161-
162-
ext := ".yaml"
163-
if c.Param("section") == "notes" {
164-
ext = ".txt"
165-
}
166-
167-
res, err := RevisionDiff(functor, ext, cNamespace, cName, cRevDiff, cRev, flag)
168-
if err != nil {
169-
_ = c.AbortWithError(http.StatusInternalServerError, err)
170-
return
171-
}
172-
c.String(http.StatusOK, res)
173-
} else {
174-
res, err := functor(cNamespace, cName, cRev, flag)
175-
if err != nil {
176-
_ = c.AbortWithError(http.StatusInternalServerError, err)
177-
return
178-
}
179-
c.String(http.StatusOK, res)
174+
return res, nil
175+
} else {
176+
res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag)
177+
if err != nil {
178+
return "", err
180179
}
181-
})
180+
return res, nil
181+
}
182182
}
183183

184184
func configureKubectls(api *gin.Engine, data *DataLayer) {
@@ -192,16 +192,14 @@ func configureKubectls(api *gin.Engine, data *DataLayer) {
192192
})
193193

194194
api.GET("/api/kube/resources/:kind", func(c *gin.Context) {
195-
cName := c.Query("name")
196-
cNamespace := c.Query("namespace")
197-
if cName == "" {
198-
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: name"))
199-
return
195+
qp, err := getQueryProps(c, false)
196+
if err != nil {
197+
_ = c.AbortWithError(http.StatusBadRequest, err)
200198
}
201199

202-
res, err := data.GetResource(cNamespace, &GenericResource{
200+
res, err := data.GetResource(qp.Namespace, &GenericResource{
203201
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
204-
ObjectMeta: v1.ObjectMeta{Name: cName},
202+
ObjectMeta: v1.ObjectMeta{Name: qp.Name},
205203
})
206204
if err != nil {
207205
_ = c.AbortWithError(http.StatusInternalServerError, err)
@@ -225,14 +223,12 @@ func configureKubectls(api *gin.Engine, data *DataLayer) {
225223
})
226224

227225
api.GET("/api/kube/describe/:kind", func(c *gin.Context) {
228-
cName := c.Query("name")
229-
cNamespace := c.Query("namespace")
230-
if cName == "" {
231-
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: name"))
232-
return
226+
qp, err := getQueryProps(c, false)
227+
if err != nil {
228+
_ = c.AbortWithError(http.StatusBadRequest, err)
233229
}
234230

235-
res, err := data.DescribeResource(cNamespace, c.Param("kind"), cName)
231+
res, err := data.DescribeResource(qp.Namespace, c.Param("kind"), qp.Name)
236232
if err != nil {
237233
_ = c.AbortWithError(http.StatusInternalServerError, err)
238234
return
@@ -281,3 +277,27 @@ func contextSetter(data *DataLayer) gin.HandlerFunc {
281277
c.Next()
282278
}
283279
}
280+
281+
type QueryProps struct {
282+
Namespace string
283+
Name string
284+
Revision int
285+
}
286+
287+
func getQueryProps(c *gin.Context, revRequired bool) (*QueryProps, error) {
288+
qp := QueryProps{}
289+
290+
qp.Namespace = c.Query("namespace")
291+
qp.Name = c.Query("name")
292+
if qp.Name == "" {
293+
return nil, errors.New("missing required query string parameter: name")
294+
}
295+
296+
cRev, err := strconv.Atoi(c.Query("revision"))
297+
if err != nil && revRequired {
298+
return nil, err
299+
}
300+
qp.Revision = cRev
301+
302+
return &qp, nil
303+
}

pkg/dashboard/data.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,14 @@ func (d *DataLayer) UninstallChart(namespace string, name string) error {
333333
return nil
334334
}
335335

336+
func (d *DataLayer) Revert(namespace string, name string, rev int) error {
337+
_, err := d.runCommandHelm("rollback", name, strconv.Itoa(rev), "--namespace", namespace)
338+
if err != nil {
339+
return err
340+
}
341+
return nil
342+
}
343+
336344
func RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) {
337345
if revision1 == 0 || revision2 == 0 {
338346
log.Debugf("One of revisions is zero: %d %d", revision1, revision2)

pkg/dashboard/static/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
</div>
6161
<h1><span class="name"></span>, revision <span class="rev"></span>
6262
<span class="float-end">
63-
<a id="btnRollback" class="btn btn-sm bg-primary border border-secondary text-light" title="Rollback to this revision"><i class="fa fa-backward"></i> Rollback</a>
63+
<a id="btnRollback" class="btn btn-sm bg-primary border border-secondary text-light" title="Rollback to this revision"><i class="fa fa-backward"></i> <span>Rollback</span></a>
6464
<a id="btnUninstall" class="btn btn-sm bg-danger border border-secondary text-light" title="Uninstall the chart"><i class="fa fa-trash"></i> Uninstall</a>
6565
</span>
6666
</h1>

pkg/dashboard/static/scripts.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ function revisionClicked(namespace, name, self) {
2020
$("#revDescr").addClass("text-danger")
2121
}
2222

23+
if (false) { // TODO: hide if only one revision
24+
$("#btnRollback").hide()
25+
} else {
26+
const rev = $("#specRev").data("last-rev") == elm.revision ? elm.revision - 1 : elm.revision
27+
$("#btnRollback").data("rev", rev).show().find("span").text("Rollback to #" + rev)
28+
}
29+
2330
const tab = getHashParam("tab")
2431
if (!tab) {
2532
$("#nav-tab [data-tab=resources]").click()
@@ -420,4 +427,47 @@ $("#btnUninstall").click(function () {
420427
$("#confirmModalBody").append("<p class='row'><i class='col-sm-3 text-end'>" + res.kind + "</i><b class='col-sm-9'>" + res.metadata.name + "</b></p>")
421428
}
422429
})
430+
})
431+
432+
$("#btnRollback").click(function () {
433+
const chart = getHashParam('chart');
434+
const namespace = getHashParam('namespace');
435+
const revisionNew = $("#btnRollback").data("rev")
436+
const revisionCur = $("#specRev").data("last-rev")
437+
$("#confirmModalLabel").html("Rollback <b class='text-danger'>" + chart + "</b> from revision " + revisionCur + " to " + revisionNew)
438+
$("#confirmModalBody").empty().append("<i class='fa fa-spin fa-spinner fa-2x'></i>")
439+
$("#confirmModal .btn-primary").prop("disabled", true).off('click').click(function () {
440+
$("#confirmModal .btn-primary").prop("disabled", true).append("<i class='fa fa-spin fa-spinner'></i>")
441+
const url = "/api/helm/charts/rollback?namespace=" + namespace + "&chart=" + chart + "&revision=" + revisionNew;
442+
$.ajax({
443+
url: url,
444+
type: 'POST',
445+
}).fail(function () {
446+
reportError("Failed to rollback the chart")
447+
}).done(function () {
448+
window.location.reload()
449+
})
450+
})
451+
452+
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'), {});
453+
myModal.show()
454+
455+
let qstr = "chart=" + chart + "&namespace=" + namespace + "&revision=" + revisionNew + "&revisionDiff=" + revisionCur
456+
let url = "/api/helm/charts/manifests"
457+
url += "?" + qstr
458+
$.get(url).fail(function () {
459+
reportError("Failed to get list of resources")
460+
}).done(function (data) {
461+
$("#confirmModalBody").empty();
462+
$("#confirmModal .btn-primary").prop("disabled", false)
463+
464+
const targetElement = document.getElementById('confirmModalBody');
465+
const configuration = {
466+
inputFormat: 'diff', outputFormat: 'side-by-side',
467+
drawFileList: false, showFiles: false, highlight: true,
468+
};
469+
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
470+
diff2htmlUi.draw()
471+
$("#confirmModalBody").prepend("<p>Following changes will happen to cluster:</p>")
472+
})
423473
})

0 commit comments

Comments
 (0)