mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 11:48:04 +00:00
Rollback action (#9)
* Show rollback confirm * Implement rollback backend * Refactoring * Refactoring
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user