Rollback action (#9)

* Show rollback confirm

* Implement rollback backend

* Refactoring

* Refactoring
This commit is contained in:
Andrey Pokhilko
2022-09-11 12:54:18 +01:00
committed by GitHub
parent fa48cf5435
commit 5ea54f9257
5 changed files with 158 additions and 79 deletions

View File

@@ -70,4 +70,5 @@ Adding new repository
Recognise & show ArgoCD-originating charts/objects Recognise & show ArgoCD-originating charts/objects
Have cleaner idea on the web API structure Have cleaner idea on the web API structure
See if we can build in Chechov or Validkube validation See if we can build in Chechov or Validkube validation
Show manifest/describe upon clicking on resource Show manifest/describe upon clicking on resource
Recognise the revisions that are rollbacks by their description and mark in timeline

View File

@@ -74,13 +74,25 @@ func configureHelms(api *gin.Engine, data *DataLayer) {
}) })
api.DELETE("/api/helm/charts", func(c *gin.Context) { api.DELETE("/api/helm/charts", func(c *gin.Context) {
cName := c.Query("chart") qp, err := getQueryProps(c, false)
cNamespace := c.Query("namespace") if err != nil {
if cName == "" { _ = c.AbortWithError(http.StatusBadRequest, err)
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart")) }
err = data.UninstallChart(qp.Namespace, qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
err := data.UninstallChart(cNamespace, cName) c.Redirect(http.StatusFound, "/")
})
api.POST("/api/helm/charts/rollback", func(c *gin.Context) {
qp, err := getQueryProps(c, true)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
}
err = data.Revert(qp.Namespace, qp.Name, qp.Revision)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return
@@ -89,14 +101,12 @@ func configureHelms(api *gin.Engine, data *DataLayer) {
}) })
api.GET("/api/helm/charts/history", func(c *gin.Context) { api.GET("/api/helm/charts/history", func(c *gin.Context) {
cName := c.Query("chart") qp, err := getQueryProps(c, false)
cNamespace := c.Query("namespace") if err != nil {
if cName == "" { _ = c.AbortWithError(http.StatusBadRequest, err)
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
return
} }
res, err := data.ChartHistory(cNamespace, cName) res, err := data.ChartHistory(qp.Namespace, qp.Name)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return
@@ -105,19 +115,12 @@ func configureHelms(api *gin.Engine, data *DataLayer) {
}) })
api.GET("/api/helm/charts/resources", func(c *gin.Context) { api.GET("/api/helm/charts/resources", func(c *gin.Context) {
cName := c.Query("chart") qp, err := getQueryProps(c, true)
cNamespace := c.Query("namespace")
if cName == "" {
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
return
}
cRev, err := strconv.Atoi(c.Query("revision"))
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return
} }
res, err := data.RevisionManifestsParsed(cNamespace, cName, cRev) res, err := data.RevisionManifestsParsed(qp.Namespace, qp.Name, qp.Revision)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return
@@ -125,60 +128,57 @@ func configureHelms(api *gin.Engine, data *DataLayer) {
c.IndentedJSON(http.StatusOK, res) c.IndentedJSON(http.StatusOK, res)
}) })
api.GET("/api/helm/charts/:section", func(c *gin.Context) {
qp, err := getQueryProps(c, true)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
}
flag := c.Query("flag") == "true"
rDiff := c.Query("revisionDiff")
res, err := handleGetSection(data, c.Param("section"), rDiff, qp, flag)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
}
c.String(http.StatusOK, res)
})
}
func handleGetSection(data *DataLayer, section string, rDiff string, qp *QueryProps, flag bool) (string, error) {
sections := map[string]SectionFn{ sections := map[string]SectionFn{
"manifests": data.RevisionManifests, "manifests": data.RevisionManifests,
"values": data.RevisionValues, "values": data.RevisionValues,
"notes": data.RevisionNotes, "notes": data.RevisionNotes,
} }
api.GET("/api/helm/charts/:section", func(c *gin.Context) { functor, found := sections[section]
functor, found := sections[c.Param("section")] if !found {
if !found { return "", errors.New("unsupported section: " + section)
_ = c.AbortWithError(http.StatusNotFound, errors.New("unsupported section: "+c.Param("section"))) }
return
}
cName := c.Query("chart") if rDiff != "" {
cNamespace := c.Query("namespace") cRevDiff, err := strconv.Atoi(rDiff)
if cName == "" {
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
return
}
cRev, err := strconv.Atoi(c.Query("revision"))
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) return "", err
return
} }
flag := c.Query("flag") == "true"
rDiff := c.Query("revisionDiff")
if rDiff != "" {
cRevDiff, err := strconv.Atoi(rDiff)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
ext := ".yaml" ext := ".yaml"
if c.Param("section") == "notes" { if section == "notes" {
ext = ".txt" ext = ".txt"
}
res, err := RevisionDiff(functor, ext, cNamespace, cName, cRevDiff, cRev, flag)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, res)
} else {
res, err := functor(cNamespace, cName, cRev, flag)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, res)
} }
})
res, err := RevisionDiff(functor, ext, qp.Namespace, qp.Name, cRevDiff, qp.Revision, flag)
if err != nil {
return "", err
}
return res, nil
} else {
res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag)
if err != nil {
return "", err
}
return res, nil
}
} }
func configureKubectls(api *gin.Engine, data *DataLayer) { func configureKubectls(api *gin.Engine, data *DataLayer) {
@@ -192,16 +192,14 @@ func configureKubectls(api *gin.Engine, data *DataLayer) {
}) })
api.GET("/api/kube/resources/:kind", func(c *gin.Context) { api.GET("/api/kube/resources/:kind", func(c *gin.Context) {
cName := c.Query("name") qp, err := getQueryProps(c, false)
cNamespace := c.Query("namespace") if err != nil {
if cName == "" { _ = c.AbortWithError(http.StatusBadRequest, err)
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: name"))
return
} }
res, err := data.GetResource(cNamespace, &GenericResource{ res, err := data.GetResource(qp.Namespace, &GenericResource{
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")}, TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
ObjectMeta: v1.ObjectMeta{Name: cName}, ObjectMeta: v1.ObjectMeta{Name: qp.Name},
}) })
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
@@ -225,14 +223,12 @@ func configureKubectls(api *gin.Engine, data *DataLayer) {
}) })
api.GET("/api/kube/describe/:kind", func(c *gin.Context) { api.GET("/api/kube/describe/:kind", func(c *gin.Context) {
cName := c.Query("name") qp, err := getQueryProps(c, false)
cNamespace := c.Query("namespace") if err != nil {
if cName == "" { _ = c.AbortWithError(http.StatusBadRequest, err)
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: name"))
return
} }
res, err := data.DescribeResource(cNamespace, c.Param("kind"), cName) res, err := data.DescribeResource(qp.Namespace, c.Param("kind"), qp.Name)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return
@@ -281,3 +277,27 @@ func contextSetter(data *DataLayer) gin.HandlerFunc {
c.Next() c.Next()
} }
} }
type QueryProps struct {
Namespace string
Name string
Revision int
}
func getQueryProps(c *gin.Context, revRequired bool) (*QueryProps, error) {
qp := QueryProps{}
qp.Namespace = c.Query("namespace")
qp.Name = c.Query("name")
if qp.Name == "" {
return nil, errors.New("missing required query string parameter: name")
}
cRev, err := strconv.Atoi(c.Query("revision"))
if err != nil && revRequired {
return nil, err
}
qp.Revision = cRev
return &qp, nil
}

View File

@@ -333,6 +333,14 @@ func (d *DataLayer) UninstallChart(namespace string, name string) error {
return nil return nil
} }
func (d *DataLayer) Revert(namespace string, name string, rev int) error {
_, err := d.runCommandHelm("rollback", name, strconv.Itoa(rev), "--namespace", namespace)
if err != nil {
return err
}
return nil
}
func RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) { func RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) {
if revision1 == 0 || revision2 == 0 { if revision1 == 0 || revision2 == 0 {
log.Debugf("One of revisions is zero: %d %d", revision1, revision2) log.Debugf("One of revisions is zero: %d %d", revision1, revision2)

View File

@@ -60,7 +60,7 @@
</div> </div>
<h1><span class="name"></span>, revision <span class="rev"></span> <h1><span class="name"></span>, revision <span class="rev"></span>
<span class="float-end"> <span class="float-end">
<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> <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>
<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> <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>
</span> </span>
</h1> </h1>

View File

@@ -20,6 +20,13 @@ function revisionClicked(namespace, name, self) {
$("#revDescr").addClass("text-danger") $("#revDescr").addClass("text-danger")
} }
if (false) { // TODO: hide if only one revision
$("#btnRollback").hide()
} else {
const rev = $("#specRev").data("last-rev") == elm.revision ? elm.revision - 1 : elm.revision
$("#btnRollback").data("rev", rev).show().find("span").text("Rollback to #" + rev)
}
const tab = getHashParam("tab") const tab = getHashParam("tab")
if (!tab) { if (!tab) {
$("#nav-tab [data-tab=resources]").click() $("#nav-tab [data-tab=resources]").click()
@@ -420,4 +427,47 @@ $("#btnUninstall").click(function () {
$("#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>") $("#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>")
} }
}) })
})
$("#btnRollback").click(function () {
const chart = getHashParam('chart');
const namespace = getHashParam('namespace');
const revisionNew = $("#btnRollback").data("rev")
const revisionCur = $("#specRev").data("last-rev")
$("#confirmModalLabel").html("Rollback <b class='text-danger'>" + chart + "</b> from revision " + revisionCur + " to " + revisionNew)
$("#confirmModalBody").empty().append("<i class='fa fa-spin fa-spinner fa-2x'></i>")
$("#confirmModal .btn-primary").prop("disabled", true).off('click').click(function () {
$("#confirmModal .btn-primary").prop("disabled", true).append("<i class='fa fa-spin fa-spinner'></i>")
const url = "/api/helm/charts/rollback?namespace=" + namespace + "&chart=" + chart + "&revision=" + revisionNew;
$.ajax({
url: url,
type: 'POST',
}).fail(function () {
reportError("Failed to rollback the chart")
}).done(function () {
window.location.reload()
})
})
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'), {});
myModal.show()
let qstr = "chart=" + chart + "&namespace=" + namespace + "&revision=" + revisionNew + "&revisionDiff=" + revisionCur
let url = "/api/helm/charts/manifests"
url += "?" + qstr
$.get(url).fail(function () {
reportError("Failed to get list of resources")
}).done(function (data) {
$("#confirmModalBody").empty();
$("#confirmModal .btn-primary").prop("disabled", false)
const targetElement = document.getElementById('confirmModalBody');
const configuration = {
inputFormat: 'diff', outputFormat: 'side-by-side',
drawFileList: false, showFiles: false, highlight: true,
};
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
diff2htmlUi.draw()
$("#confirmModalBody").prepend("<p>Following changes will happen to cluster:</p>")
})
}) })