diff --git a/README.md b/README.md index 2787e9e..89a2024 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,23 @@ A simplified way of working with Helm. ## What it Does? -The _Helm Dashboard_ plugin offers a UI-driven way to view the installed Helm charts, see their revision history and corresponding k8s resources. Also, you can perform simple actions like roll back to a revision or upgrade to newer version. +The _Helm Dashboard_ plugin offers a UI-driven way to view the installed Helm charts, see their revision history and +corresponding k8s resources. Also, you can perform simple actions like roll back to a revision or upgrade to newer +version. -This project is part of [Komodor's](https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash-gh) vision of helping Kubernetes users to navigate and troubleshoot their clusters. +This project is part of [Komodor's](https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash-gh) vision of +helping Kubernetes users to navigate and troubleshoot their clusters. Some of the key capabilities of the tool: - - See all installed charts and their revision history - - See manifest diff of the past revisions - - Browse k8s resources resulting from the chart - - Easy rollback or upgrade version with a clear and easy manifest diff - - Integration with popular problem scanners - - Easy switch between multiple clusters -## Installing +- See all installed charts and their revision history +- See manifest diff of the past revisions +- Browse k8s resources resulting from the chart +- Easy rollback or upgrade version with a clear and easy manifest diff +- Integration with popular problem scanners +- Easy switch between multiple clusters + +## Installing To install it, simply run Helm command: @@ -27,6 +31,7 @@ helm plugin install https://github.com/komodorio/helm-dashboard.git ``` To update the plugin to the latest version, run: + ```shell helm plugin update dashboard ``` @@ -42,13 +47,16 @@ helm plugin uninstall dashboard To use the plugin, your machine needs to have working `helm` and also `kubectl` commands. After installing, start the UI by running: + ```shell helm dashboard ``` -The command above will launch the local Web server and will open the UI in new browser tab. The command will hang waiting for you to terminate it in command-line or web UI. +The command above will launch the local Web server and will open the UI in new browser tab. The command will hang +waiting for you to terminate it in command-line or web UI. -By default, the web server is only available locally. You can change that by specifying `HD_BIND` environment variable to the desired value. For example, `0.0.0.0` would bind to all IPv4 addresses or `[::0]` would be all IPv6 addresses. +By default, the web server is only available locally. You can change that by specifying `HD_BIND` environment variable +to the desired value. For example, `0.0.0.0` would bind to all IPv4 addresses or `[::0]` would be all IPv6 addresses. If your port 8080 is busy, you can specify a different port to use via `HD_PORT` environment variable. @@ -58,20 +66,23 @@ If you want to increase the logging verbosity and see all the debug info, set `D ## Scanner Integrations -Upon startup, Helm Dashboard detects the presence of [Trivy](https://github.com/aquasecurity/trivy) and [Checkov](https://github.com/bridgecrewio/checkov) scanners. When available, these scanners are offered on k8s resources page, as well as install/upgrade preview page. +Upon startup, Helm Dashboard detects the presence of [Trivy](https://github.com/aquasecurity/trivy) +and [Checkov](https://github.com/bridgecrewio/checkov) scanners. When available, these scanners are offered on k8s +resources page, as well as install/upgrade preview page. You can request scanning of the specific k8s resource in your cluster:  -If you want to validate the k8s manifest prior to installing/reconfiguring a Helm chart, look for "Scan for Problems" button at the bottom of the dialog: +If you want to validate the k8s manifest prior to installing/reconfiguring a Helm chart, look for "Scan for Problems" +button at the bottom of the dialog:  ## Support Channels -We have two main channels for supporting the Helm Dashboard users: [Slack community](https://komodorkommunity.slack.com/archives/C044U1B0265) for general conversations +We have two main channels for supporting the Helm Dashboard +users: [Slack community](https://komodorkommunity.slack.com/archives/C044U1B0265) for general conversations and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs. - ## Roadmap & Ideas ### First Public Version @@ -93,22 +104,12 @@ and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real - Styled properly ### Further Ideas + - solve umbrella-chart case - Have cleaner idea on the web API structure - Recognise & show ArgoCD-originating charts/objects, those `helm ls` does not show - loki example - DaemonSet and StatefulSet better status display -#### Iteration "Value Setting" - -- Setting parameter values and installing -- Reconfiguring the application - -#### Iteration "Repo View" - -- Browsing repositories -- Adding new repository -- Installing new app from repo - ## Local Dev Testing Prerequisites: `helm` and `kubectl` binaries installed and operational. @@ -127,7 +128,8 @@ To install, checkout the source code and run from source dir: helm plugin install . ``` -Local installation of plugin just creates a symlink, so making the changes and rebuilding the binary would not require to +Local installation of plugin just creates a symlink, so making the changes and rebuilding the binary would not require +to reinstall a plugin. To use the plugin, run in your terminal: diff --git a/pkg/dashboard/api.go b/pkg/dashboard/api.go index 4ea071e..931af84 100644 --- a/pkg/dashboard/api.go +++ b/pkg/dashboard/api.go @@ -81,16 +81,23 @@ func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *g func configureHelms(api *gin.RouterGroup, data *subproc.DataLayer) { h := handlers.HelmHandler{Data: data} + api.GET("/charts", h.GetCharts) api.DELETE("/charts", h.Uninstall) - api.POST("/charts/rollback", h.Rollback) + api.GET("/charts/history", h.History) api.GET("/charts/resources", h.Resources) + api.GET("/charts/:section", h.GetInfoSection) + api.POST("/charts/install", h.Install) + api.POST("/charts/rollback", h.Rollback) + + api.GET("/repo", h.RepoList) + api.POST("/repo", h.RepoAdd) + api.DELETE("/repo", h.RepoDelete) + api.GET("/repo/charts", h.RepoCharts) api.GET("/repo/search", h.RepoSearch) api.POST("/repo/update", h.RepoUpdate) api.GET("/repo/values", h.RepoValues) - api.POST("/charts/install", h.Install) - api.GET("/charts/:section", h.GetInfoSection) } func configureKubectls(api *gin.RouterGroup, data *subproc.DataLayer) { diff --git a/pkg/dashboard/handlers/helmHandlers.go b/pkg/dashboard/handlers/helmHandlers.go index 7c51952..f55a42b 100644 --- a/pkg/dashboard/handlers/helmHandlers.go +++ b/pkg/dashboard/handlers/helmHandlers.go @@ -31,7 +31,7 @@ func (h *HelmHandler) Uninstall(c *gin.Context) { _ = c.AbortWithError(http.StatusBadRequest, err) return } - err = h.Data.UninstallChart(qp.Namespace, qp.Name) + err = h.Data.ChartUninstall(qp.Namespace, qp.Name) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return @@ -99,6 +99,21 @@ func (h *HelmHandler) RepoSearch(c *gin.Context) { c.IndentedJSON(http.StatusOK, res) } +func (h *HelmHandler) RepoCharts(c *gin.Context) { + qp, err := utils.GetQueryProps(c, false) + if err != nil { + _ = c.AbortWithError(http.StatusBadRequest, err) + return + } + + res, err := h.Data.ChartRepoCharts(qp.Name) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + c.IndentedJSON(http.StatusOK, res) +} + func (h *HelmHandler) RepoUpdate(c *gin.Context) { qp, err := utils.GetQueryProps(c, false) if err != nil { @@ -122,21 +137,25 @@ func (h *HelmHandler) Install(c *gin.Context) { } justTemplate := c.Query("flag") != "true" - out, err := h.Data.ChartUpgrade(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), justTemplate, c.PostForm("values")) + isInitial := c.Query("initial") != "true" + out, err := h.Data.ChartInstall(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), justTemplate, c.PostForm("values"), isInitial) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return } - if !justTemplate { - c.Header("Content-Type", "application/json") - } else { - manifests, err := h.Data.RevisionManifests(qp.Namespace, qp.Name, 0, false) - if err != nil { - _ = c.AbortWithError(http.StatusInternalServerError, err) - return + if justTemplate { + manifests := "" + if isInitial { + manifests, err = h.Data.RevisionManifests(qp.Namespace, qp.Name, 0, false) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } } out = subproc.GetDiff(strings.TrimSpace(manifests), out, "current.yaml", "upgraded.yaml") + } else { + c.Header("Content-Type", "application/json") } c.String(http.StatusAccepted, out) @@ -168,6 +187,39 @@ func (h *HelmHandler) RepoValues(c *gin.Context) { c.String(http.StatusOK, out) } +func (h *HelmHandler) RepoList(c *gin.Context) { + out, err := h.Data.ChartRepoList() + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + c.IndentedJSON(http.StatusOK, out) +} + +func (h *HelmHandler) RepoAdd(c *gin.Context) { + _, err := h.Data.ChartRepoAdd(c.PostForm("name"), c.PostForm("url")) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + c.Status(http.StatusNoContent) +} + +func (h *HelmHandler) RepoDelete(c *gin.Context) { + qp, err := utils.GetQueryProps(c, false) + if err != nil { + _ = c.AbortWithError(http.StatusBadRequest, err) + return + } + + _, err = h.Data.ChartRepoDelete(qp.Name) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + c.Status(http.StatusNoContent) +} + func handleGetSection(data *subproc.DataLayer, section string, rDiff string, qp *utils.QueryProps, flag bool) (string, error) { sections := map[string]subproc.SectionFn{ "manifests": data.RevisionManifests, diff --git a/pkg/dashboard/handlers/scannerHandlers.go b/pkg/dashboard/handlers/scannerHandlers.go index acc7876..82eba67 100644 --- a/pkg/dashboard/handlers/scannerHandlers.go +++ b/pkg/dashboard/handlers/scannerHandlers.go @@ -26,7 +26,8 @@ func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) { return } - mnf, err := h.Data.ChartUpgrade(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), true, c.PostForm("values")) + reuseVals := c.Query("initial") != "true" + mnf, err := h.Data.ChartInstall(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), true, c.PostForm("values"), reuseVals) if err != nil { _ = c.AbortWithError(http.StatusInternalServerError, err) return diff --git a/pkg/dashboard/static/actions.js b/pkg/dashboard/static/actions.js index 2651129..8f79465 100644 --- a/pkg/dashboard/static/actions.js +++ b/pkg/dashboard/static/actions.js @@ -30,17 +30,6 @@ function checkUpgradeable(name) { } const verCur = $("#specRev").data("last-chart-ver"); - $('#upgradeModal select').empty() - for (let i = 0; i < data.length; i++) { - const opt = $(""); - if (data[i].version === verCur) { - opt.html(data[i].version + " ·") - } else { - opt.html(data[i].version) - } - $('#upgradeModal select').append(opt) - } - const elm = data[0] $("#btnUpgradeCheck").data("repo", elm.name.split('/').shift()) $("#btnUpgradeCheck").data("chart", elm.name.split('/').pop()) @@ -56,55 +45,86 @@ function checkUpgradeable(name) { } $("#btnUpgrade").off("click").click(function () { - popUpUpgrade($(this), verCur, elm) + popUpUpgrade(elm, getHashParam("namespace"), getHashParam("chart"), verCur, $("#specRev").data("last-rev")) }) }) } -function popUpUpgrade(self, verCur, elm) { - const name = getHashParam("chart"); - const qstr = "?namespace=" + getHashParam("namespace") + "&name=" + name + "&chart=" + elm.name; - let url = "/api/helm/charts/install" + qstr - $('#upgradeModal select').data("qstr", qstr).data("url", url).data("chart", elm.name) +function popUpUpgrade(elm, ns, name, verCur, lastRev) { + $("#upgradeModal .btn-confirm").prop("disabled", true) - $("#upgradeModalLabel .name").text(name) - $("#upgradeModal .ver-old").text(verCur) + $('#upgradeModal').data("chart", elm.name).data("initial", !verCur) - $('#upgradeModal select').val(elm.version).trigger("change") + $("#upgradeModalLabel .name").text(elm.name) - const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {}); - myModal.show() + if (verCur) { + $("#upgradeModal .ver-old").show().find("span").text(verCur) + $("#upgradeModal .rel-name").prop("disabled", true).val(name) + $("#upgradeModal .rel-ns").prop("disabled", true).val(ns) + } else { + $("#upgradeModal .ver-old").hide() + $("#upgradeModal .rel-name").prop("disabled", false).val(elm.name.split("/").pop()) + $("#upgradeModal .rel-ns").prop("disabled", false).val("") + } - const btnConfirm = $("#upgradeModal .btn-confirm"); - btnConfirm.prop("disabled", true).off('click').click(function () { - btnConfirm.prop("disabled", true).prepend('') - $.ajax({ - type: 'POST', - url: url + "&version=" + $('#upgradeModal select').val() + "&flag=true", - data: $("#upgradeModal textarea").data("dirty") ? $("#upgradeModal form").serialize() : null, - }).fail(function (xhr) { - reportError("Failed to upgrade the chart", xhr) - }).done(function (data) { - if (data.version) { - setHashParam("revision", data.version) - window.location.reload() + $.getJSON("/api/helm/repo/search?name=" + elm.name).fail(function (xhr) { + reportError("Failed to find chart in repo", xhr) + }).done(function (vers) { + // fill versions + $('#upgradeModal select').empty() + for (let i = 0; i < vers.length; i++) { + const opt = $(""); + if (vers[i].version === verCur) { + opt.html(vers[i].version + " ·") } else { - reportError("Failed to get new revision number") + opt.html(vers[i].version) } - }) - }) + $('#upgradeModal select').append(opt) + } - // fill current values - const lastRev = $("#specRev").data("last-rev") - $.get("/api/helm/charts/values?namespace=" + getHashParam("namespace") + "&revision=" + lastRev + "&name=" + getHashParam("chart") + "&flag=true").fail(function (xhr) { - reportError("Failed to get charts values info", xhr) - }).done(function (data) { - $("#upgradeModal textarea").val(data).data("dirty", false) + $('#upgradeModal select').val(elm.version).trigger("change") + + const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {}); + myModal.show() + + if (verCur) { + // fill current values + $.get("/api/helm/charts/values?namespace=" + ns + "&revision=" + lastRev + "&name=" + name + "&flag=true").fail(function (xhr) { + reportError("Failed to get charts values info", xhr) + }).done(function (data) { + $("#upgradeModal textarea").val(data).data("dirty", false) + }) + } else { + $("#upgradeModal textarea").val("").data("dirty", true) + } }) } +$("#upgradeModal .btn-confirm").click(function () { + const btnConfirm = $("#upgradeModal .btn-confirm") + btnConfirm.prop("disabled", true).prepend('') + $.ajax({ + type: 'POST', + url: "/api/helm/charts/install" + upgradeModalQstr() + "&flag=true", + data: $("#upgradeModal textarea").data("dirty") ? $("#upgradeModal form").serialize() : null, + }).fail(function (xhr) { + reportError("Failed to upgrade the chart", xhr) + }).done(function (data) { + if (data.version) { + setHashParam("section", null) + setHashParam("namespace", $("#upgradeModal .rel-ns").val()) + setHashParam("chart", $("#upgradeModal .rel-name").val()) + setHashParam("revision", data.version) + window.location.reload() + } else { + reportError("Failed to get new revision number") + } + }) +}) + let reconfigTimeout = null; -$("#upgradeModal textarea").keyup(function () { + +function changeTimer() { const self = $(this); self.data("dirty", true) if (reconfigTimeout) { @@ -113,7 +133,11 @@ $("#upgradeModal textarea").keyup(function () { reconfigTimeout = window.setTimeout(function () { requestChangeDiff() }, 500) -}) +} + +$("#upgradeModal textarea").keyup(changeTimer) +$("#upgradeModal .rel-name").keyup(changeTimer) +$("#upgradeModal .rel-ns").keyup(changeTimer) $('#upgradeModal select').change(function () { const self = $(this) @@ -122,7 +146,7 @@ $('#upgradeModal select').change(function () { // fill reference values $("#upgradeModal .ref-vals").html('') - $.get("/api/helm/repo/values?chart=" + self.data("chart") + "&version=" + self.val()).fail(function (xhr) { + $.get("/api/helm/repo/values?chart=" + $("#upgradeModal").data("chart") + "&version=" + self.val()).fail(function (xhr) { reportError("Failed to get upgrade info", xhr) }).done(function (data) { data = hljs.highlight(data, {language: 'yaml'}).value @@ -134,10 +158,9 @@ $('#upgradeModal .btn-scan').click(function () { const self = $(this) self.prop("disabled", true).prepend('') - const qstr = $('#upgradeModal select').data("qstr") $.ajax({ type: "POST", - url: "/api/scanners/manifests" + qstr + "&version=" + $('#upgradeModal select').val(), + url: "/api/scanners/manifests" + upgradeModalQstr(), data: $("#upgradeModal form").serialize(), }).fail(function (xhr) { reportError("Failed to scan the manifest", xhr) @@ -186,7 +209,7 @@ function requestChangeDiff() { $.ajax({ type: "POST", - url: self.data("url") + "&version=" + self.val(), + url: "/api/helm/charts/install" + upgradeModalQstr(), data: values, }).fail(function (xhr) { $("#upgradeModalBody").html("
Failed to get upgrade info: " + xhr.responseText + "
") @@ -207,6 +230,20 @@ function requestChangeDiff() { }) } +function upgradeModalQstr() { + let qstr = "?" + + "namespace=" + $("#upgradeModal .rel-ns").val() + + "&name=" + $("#upgradeModal .rel-name").val() + + "&chart=" + $("#upgradeModal").data("chart") + + "&version=" + $('#upgradeModal select').val() + + if ($("#upgradeModal").data("initial")) { + qstr += "&initial=true" + } + + return qstr +} + const btnConfirm = $("#confirmModal .btn-confirm"); $("#btnUninstall").click(function () { const chart = getHashParam('chart'); diff --git a/pkg/dashboard/static/index.html b/pkg/dashboard/static/index.html index 4b2a834..a7d9425 100644 --- a/pkg/dashboard/static/index.html +++ b/pkg/dashboard/static/index.html @@ -48,18 +48,11 @@