mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 11:48:04 +00:00
Repository-related functions (#19)
* Roadmap item * Start building repo view * Section switcher * Show repo list * Adding chart repo works * Showing the pane * Couple of buttons * Listing items * Styling * Enriching repo view * Navigate from repo to installed * Tuning install popup * Working on install * Cosmetics
This commit is contained in:
54
README.md
54
README.md
@@ -6,17 +6,21 @@ A simplified way of working with Helm.
|
|||||||
|
|
||||||
## What it Does?
|
## 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:
|
Some of the key capabilities of the tool:
|
||||||
- See all installed charts and their revision history
|
|
||||||
- See manifest diff of the past revisions
|
- See all installed charts and their revision history
|
||||||
- Browse k8s resources resulting from the chart
|
- See manifest diff of the past revisions
|
||||||
- Easy rollback or upgrade version with a clear and easy manifest diff
|
- Browse k8s resources resulting from the chart
|
||||||
- Integration with popular problem scanners
|
- Easy rollback or upgrade version with a clear and easy manifest diff
|
||||||
- Easy switch between multiple clusters
|
- Integration with popular problem scanners
|
||||||
|
- Easy switch between multiple clusters
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
|
|
||||||
@@ -27,6 +31,7 @@ helm plugin install https://github.com/komodorio/helm-dashboard.git
|
|||||||
```
|
```
|
||||||
|
|
||||||
To update the plugin to the latest version, run:
|
To update the plugin to the latest version, run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
helm plugin update dashboard
|
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.
|
To use the plugin, your machine needs to have working `helm` and also `kubectl` commands.
|
||||||
|
|
||||||
After installing, start the UI by running:
|
After installing, start the UI by running:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
helm dashboard
|
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.
|
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
|
## 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:
|
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
|
## 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.
|
and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
|
||||||
|
|
||||||
|
|
||||||
## Roadmap & Ideas
|
## Roadmap & Ideas
|
||||||
|
|
||||||
### First Public Version
|
### First Public Version
|
||||||
@@ -93,22 +104,12 @@ and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real
|
|||||||
- Styled properly
|
- Styled properly
|
||||||
|
|
||||||
### Further Ideas
|
### Further Ideas
|
||||||
|
|
||||||
- solve umbrella-chart case
|
- solve umbrella-chart case
|
||||||
- Have cleaner idea on the web API structure
|
- Have cleaner idea on the web API structure
|
||||||
- Recognise & show ArgoCD-originating charts/objects, those `helm ls` does not show
|
- Recognise & show ArgoCD-originating charts/objects, those `helm ls` does not show
|
||||||
- loki example - DaemonSet and StatefulSet better status display
|
- 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
|
## Local Dev Testing
|
||||||
|
|
||||||
Prerequisites: `helm` and `kubectl` binaries installed and operational.
|
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 .
|
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.
|
reinstall a plugin.
|
||||||
|
|
||||||
To use the plugin, run in your terminal:
|
To use the plugin, run in your terminal:
|
||||||
|
|||||||
@@ -81,16 +81,23 @@ func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *g
|
|||||||
|
|
||||||
func configureHelms(api *gin.RouterGroup, data *subproc.DataLayer) {
|
func configureHelms(api *gin.RouterGroup, data *subproc.DataLayer) {
|
||||||
h := handlers.HelmHandler{Data: data}
|
h := handlers.HelmHandler{Data: data}
|
||||||
|
|
||||||
api.GET("/charts", h.GetCharts)
|
api.GET("/charts", h.GetCharts)
|
||||||
api.DELETE("/charts", h.Uninstall)
|
api.DELETE("/charts", h.Uninstall)
|
||||||
api.POST("/charts/rollback", h.Rollback)
|
|
||||||
api.GET("/charts/history", h.History)
|
api.GET("/charts/history", h.History)
|
||||||
api.GET("/charts/resources", h.Resources)
|
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.GET("/repo/search", h.RepoSearch)
|
||||||
api.POST("/repo/update", h.RepoUpdate)
|
api.POST("/repo/update", h.RepoUpdate)
|
||||||
api.GET("/repo/values", h.RepoValues)
|
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) {
|
func configureKubectls(api *gin.RouterGroup, data *subproc.DataLayer) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func (h *HelmHandler) Uninstall(c *gin.Context) {
|
|||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = h.Data.UninstallChart(qp.Namespace, qp.Name)
|
err = h.Data.ChartUninstall(qp.Namespace, qp.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -99,6 +99,21 @@ func (h *HelmHandler) RepoSearch(c *gin.Context) {
|
|||||||
c.IndentedJSON(http.StatusOK, res)
|
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) {
|
func (h *HelmHandler) RepoUpdate(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
qp, err := utils.GetQueryProps(c, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,21 +137,25 @@ func (h *HelmHandler) Install(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
justTemplate := c.Query("flag") != "true"
|
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 {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !justTemplate {
|
if justTemplate {
|
||||||
c.Header("Content-Type", "application/json")
|
manifests := ""
|
||||||
} else {
|
if isInitial {
|
||||||
manifests, err := h.Data.RevisionManifests(qp.Namespace, qp.Name, 0, false)
|
manifests, err = h.Data.RevisionManifests(qp.Namespace, qp.Name, 0, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out = subproc.GetDiff(strings.TrimSpace(manifests), out, "current.yaml", "upgraded.yaml")
|
out = subproc.GetDiff(strings.TrimSpace(manifests), out, "current.yaml", "upgraded.yaml")
|
||||||
|
} else {
|
||||||
|
c.Header("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.String(http.StatusAccepted, out)
|
c.String(http.StatusAccepted, out)
|
||||||
@@ -168,6 +187,39 @@ func (h *HelmHandler) RepoValues(c *gin.Context) {
|
|||||||
c.String(http.StatusOK, out)
|
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) {
|
func handleGetSection(data *subproc.DataLayer, section string, rDiff string, qp *utils.QueryProps, flag bool) (string, error) {
|
||||||
sections := map[string]subproc.SectionFn{
|
sections := map[string]subproc.SectionFn{
|
||||||
"manifests": data.RevisionManifests,
|
"manifests": data.RevisionManifests,
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -30,17 +30,6 @@ function checkUpgradeable(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const verCur = $("#specRev").data("last-chart-ver");
|
const verCur = $("#specRev").data("last-chart-ver");
|
||||||
$('#upgradeModal select').empty()
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
const opt = $("<option value='" + data[i].version + "'></option>");
|
|
||||||
if (data[i].version === verCur) {
|
|
||||||
opt.html(data[i].version + " ·")
|
|
||||||
} else {
|
|
||||||
opt.html(data[i].version)
|
|
||||||
}
|
|
||||||
$('#upgradeModal select').append(opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
const elm = data[0]
|
const elm = data[0]
|
||||||
$("#btnUpgradeCheck").data("repo", elm.name.split('/').shift())
|
$("#btnUpgradeCheck").data("repo", elm.name.split('/').shift())
|
||||||
$("#btnUpgradeCheck").data("chart", elm.name.split('/').pop())
|
$("#btnUpgradeCheck").data("chart", elm.name.split('/').pop())
|
||||||
@@ -56,55 +45,86 @@ function checkUpgradeable(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$("#btnUpgrade").off("click").click(function () {
|
$("#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) {
|
function popUpUpgrade(elm, ns, name, verCur, lastRev) {
|
||||||
const name = getHashParam("chart");
|
$("#upgradeModal .btn-confirm").prop("disabled", true)
|
||||||
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)
|
|
||||||
|
|
||||||
$("#upgradeModalLabel .name").text(name)
|
$('#upgradeModal').data("chart", elm.name).data("initial", !verCur)
|
||||||
$("#upgradeModal .ver-old").text(verCur)
|
|
||||||
|
|
||||||
$('#upgradeModal select').val(elm.version).trigger("change")
|
$("#upgradeModalLabel .name").text(elm.name)
|
||||||
|
|
||||||
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
|
if (verCur) {
|
||||||
myModal.show()
|
$("#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");
|
$.getJSON("/api/helm/repo/search?name=" + elm.name).fail(function (xhr) {
|
||||||
btnConfirm.prop("disabled", true).off('click').click(function () {
|
reportError("Failed to find chart in repo", xhr)
|
||||||
btnConfirm.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
}).done(function (vers) {
|
||||||
$.ajax({
|
// fill versions
|
||||||
type: 'POST',
|
$('#upgradeModal select').empty()
|
||||||
url: url + "&version=" + $('#upgradeModal select').val() + "&flag=true",
|
for (let i = 0; i < vers.length; i++) {
|
||||||
data: $("#upgradeModal textarea").data("dirty") ? $("#upgradeModal form").serialize() : null,
|
const opt = $("<option value='" + vers[i].version + "'></option>");
|
||||||
}).fail(function (xhr) {
|
if (vers[i].version === verCur) {
|
||||||
reportError("Failed to upgrade the chart", xhr)
|
opt.html(vers[i].version + " ·")
|
||||||
}).done(function (data) {
|
|
||||||
if (data.version) {
|
|
||||||
setHashParam("revision", data.version)
|
|
||||||
window.location.reload()
|
|
||||||
} else {
|
} else {
|
||||||
reportError("Failed to get new revision number")
|
opt.html(vers[i].version)
|
||||||
}
|
}
|
||||||
})
|
$('#upgradeModal select').append(opt)
|
||||||
})
|
}
|
||||||
|
|
||||||
// fill current values
|
$('#upgradeModal select').val(elm.version).trigger("change")
|
||||||
const lastRev = $("#specRev").data("last-rev")
|
|
||||||
$.get("/api/helm/charts/values?namespace=" + getHashParam("namespace") + "&revision=" + lastRev + "&name=" + getHashParam("chart") + "&flag=true").fail(function (xhr) {
|
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
|
||||||
reportError("Failed to get charts values info", xhr)
|
myModal.show()
|
||||||
}).done(function (data) {
|
|
||||||
$("#upgradeModal textarea").val(data).data("dirty", false)
|
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('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
$.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;
|
let reconfigTimeout = null;
|
||||||
$("#upgradeModal textarea").keyup(function () {
|
|
||||||
|
function changeTimer() {
|
||||||
const self = $(this);
|
const self = $(this);
|
||||||
self.data("dirty", true)
|
self.data("dirty", true)
|
||||||
if (reconfigTimeout) {
|
if (reconfigTimeout) {
|
||||||
@@ -113,7 +133,11 @@ $("#upgradeModal textarea").keyup(function () {
|
|||||||
reconfigTimeout = window.setTimeout(function () {
|
reconfigTimeout = window.setTimeout(function () {
|
||||||
requestChangeDiff()
|
requestChangeDiff()
|
||||||
}, 500)
|
}, 500)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
$("#upgradeModal textarea").keyup(changeTimer)
|
||||||
|
$("#upgradeModal .rel-name").keyup(changeTimer)
|
||||||
|
$("#upgradeModal .rel-ns").keyup(changeTimer)
|
||||||
|
|
||||||
$('#upgradeModal select').change(function () {
|
$('#upgradeModal select').change(function () {
|
||||||
const self = $(this)
|
const self = $(this)
|
||||||
@@ -122,7 +146,7 @@ $('#upgradeModal select').change(function () {
|
|||||||
|
|
||||||
// fill reference values
|
// fill reference values
|
||||||
$("#upgradeModal .ref-vals").html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
$("#upgradeModal .ref-vals").html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
$.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)
|
reportError("Failed to get upgrade info", xhr)
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
data = hljs.highlight(data, {language: 'yaml'}).value
|
data = hljs.highlight(data, {language: 'yaml'}).value
|
||||||
@@ -134,10 +158,9 @@ $('#upgradeModal .btn-scan').click(function () {
|
|||||||
const self = $(this)
|
const self = $(this)
|
||||||
|
|
||||||
self.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
self.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
const qstr = $('#upgradeModal select').data("qstr")
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
url: "/api/scanners/manifests" + qstr + "&version=" + $('#upgradeModal select').val(),
|
url: "/api/scanners/manifests" + upgradeModalQstr(),
|
||||||
data: $("#upgradeModal form").serialize(),
|
data: $("#upgradeModal form").serialize(),
|
||||||
}).fail(function (xhr) {
|
}).fail(function (xhr) {
|
||||||
reportError("Failed to scan the manifest", xhr)
|
reportError("Failed to scan the manifest", xhr)
|
||||||
@@ -186,7 +209,7 @@ function requestChangeDiff() {
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
url: self.data("url") + "&version=" + self.val(),
|
url: "/api/helm/charts/install" + upgradeModalQstr(),
|
||||||
data: values,
|
data: values,
|
||||||
}).fail(function (xhr) {
|
}).fail(function (xhr) {
|
||||||
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>")
|
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>")
|
||||||
@@ -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");
|
const btnConfirm = $("#confirmModal .btn-confirm");
|
||||||
$("#btnUninstall").click(function () {
|
$("#btnUninstall").click(function () {
|
||||||
const chart = getHashParam('chart');
|
const chart = getHashParam('chart');
|
||||||
|
|||||||
@@ -48,18 +48,11 @@
|
|||||||
|
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
<li class="nav-item mx-2">
|
<li class="nav-item mx-2">
|
||||||
<a class="nav-link px-3 active" aria-current="page" href="/">Installed</a>
|
<a class="nav-link px-3 section-installed">Installed</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- TODO
|
|
||||||
<li class="nav-item mx-2">
|
<li class="nav-item mx-2">
|
||||||
<a href="#" class="nav-link px-3">Repository</a>
|
<a class="nav-link px-3 section-repo">Repository</a>
|
||||||
</li>
|
</li>
|
||||||
-->
|
|
||||||
<!-- TODO
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link disabled">Provisional Charts</a>
|
|
||||||
</li>
|
|
||||||
-->
|
|
||||||
</ul>
|
</ul>
|
||||||
<div>
|
<div>
|
||||||
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img
|
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img
|
||||||
@@ -75,7 +68,48 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<!-- /TOP BAR -->
|
<!-- /TOP BAR -->
|
||||||
|
|
||||||
<div class="row mt-3 pt-3 me-5" id="sectionList" style="display: none">
|
<!--REPO SECTION-->
|
||||||
|
<div class="row mt-3 pt-3 me-5 section" id="sectionRepo" style="display: none">
|
||||||
|
<div class="col-3 ps-4 repo-list">
|
||||||
|
<div class="p-2 bg-white rounded-1 b-shadow">
|
||||||
|
<h4 class="fs-6">Repositories</h4>
|
||||||
|
<ul class="list-unstyled p-2">
|
||||||
|
</ul>
|
||||||
|
<button class="btn btn-sm border-secondary text-muted">
|
||||||
|
<i class="bi-plus-lg"></i> Add Repository
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-9 repo-details bg-white b-shadow pt-4 px-5 overflow-auto rounded">
|
||||||
|
<div class="float-end">
|
||||||
|
<button class="me-2 btn btn-sm btn-light bg-white border border-secondary btn-update">
|
||||||
|
<i class="bi-arrow-repeat"></i> Update
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-light bg-white border border-secondary btn-remove">
|
||||||
|
<i class="bi-trash3"></i> Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div><span class="text-muted small fw-bold me-3">REPOSITORY</span></div>
|
||||||
|
<h2 class="mb-3">name-of-repo</h2>
|
||||||
|
<div class="mb-5">
|
||||||
|
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">URL: <span class="url fw-bold">http://somerepo/somepath</span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="float-end">
|
||||||
|
<!-- TODO <input class="form-control form-control-sm" type="text" placeholder="Filter..."> -->
|
||||||
|
</div>
|
||||||
|
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
|
||||||
|
style="text-transform: uppercase">
|
||||||
|
<div class="col-3">Chart Name</div>
|
||||||
|
<div class="col">Description</div>
|
||||||
|
<div class="col-1">Version</div>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="list-unstyled mt-4"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3 pt-3 me-5 section" id="sectionList" style="display: none">
|
||||||
<div class="col-2 ms-3">
|
<div class="col-2 ms-3">
|
||||||
<!-- FILTER BLOCK -->
|
<!-- FILTER BLOCK -->
|
||||||
<div class="p-2 ps-2 bg-white rounded-1 b-shadow" id="filters">
|
<div class="p-2 ps-2 bg-white rounded-1 b-shadow" id="filters">
|
||||||
@@ -117,7 +151,7 @@
|
|||||||
<!-- /INSTALLED LIST -->
|
<!-- /INSTALLED LIST -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row flex-nowrap pt-0 mx-0" id="sectionDetails" style="display: none">
|
<div class="row flex-nowrap pt-0 mx-0 section" id="sectionDetails" style="display: none">
|
||||||
<div class="col-2 px-4 py-4 pe-3 rev-list">
|
<div class="col-2 px-4 py-4 pe-3 rev-list">
|
||||||
<h3 class="fw-bold small">Revisions</h3>
|
<h3 class="fw-bold small">Revisions</h3>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
@@ -270,21 +304,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="repoAddModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Add Chart Repository</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form enctype="application/x-www-form-urlencoded">
|
||||||
|
<label class="form-label">Name: <input class="form-control" name="name"></label>
|
||||||
|
<label class="form-label">URL: <input class="form-control" name="url"></label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary btn-confirm">Add Repository</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal" id="upgradeModal" tabindex="-1" aria-labelledby="describeModalLabel" aria-hidden="true">
|
<div class="modal" id="upgradeModal" tabindex="-1">
|
||||||
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="upgradeModalLabel">
|
<h4 class="modal-title" id="upgradeModalLabel">
|
||||||
Upgrade <b class='text-success name'></b>
|
Install <b class='text-success name'></b>
|
||||||
</h4>
|
</h4>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<form class="modal-body border-bottom fs-5" enctype="multipart/form-data">
|
<form class="modal-body border-bottom fs-5" enctype="multipart/form-data">
|
||||||
<div class="input-group mb-3 text-muted">
|
<div class="input-group mb-3 text-muted">
|
||||||
<label class="form-label me-4 text-dark">Version to install: <select
|
<label class="form-label me-4 text-dark">Version to install: <select
|
||||||
class='fw-bold text-success ver-new'></select></label> (current version is <span
|
class='fw-bold text-success ver-new'></select></label> <span class="ver-old">(current version is <span
|
||||||
class='text-success ver-old ms-1'>0.0.0</span>)
|
class='text-success ms-1'>0.0.0</span>)</span>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3 text-muted">
|
||||||
|
<label class="form-label me-4 text-dark">
|
||||||
|
Release Name: <input class="form-control rel-name">
|
||||||
|
</label>
|
||||||
|
<label class="form-label me-4 text-dark">
|
||||||
|
Namespace (optional): <input class="form-control rel-ns">
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 pe-3">
|
<div class="col-6 pe-3">
|
||||||
@@ -296,7 +357,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 pe-3">
|
<div class="col-6 pe-3">
|
||||||
<textarea name="values" class="form-control w-100 h-100" rows="5" style="font-family: monospace"></textarea>
|
<textarea name="values" class="form-control w-100 h-100" rows="5"
|
||||||
|
style="font-family: monospace"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 ps-3">
|
<div class="col-6 ps-3">
|
||||||
<pre class="ref-vals fs-6 w-100 bg-secondary p-2 rounded" style="max-height: 20rem"></pre>
|
<pre class="ref-vals fs-6 w-100 bg-secondary p-2 rounded" style="max-height: 20rem"></pre>
|
||||||
@@ -313,7 +375,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<div class="modal-footer d-flex">
|
<div class="modal-footer d-flex">
|
||||||
<button type="button" class="btn btn-scan bg-white border-secondary">Scan for Problems</button>
|
<button type="button" class="btn btn-scan bg-white border-secondary">Scan for Problems</button>
|
||||||
<button type="button" class="btn btn-primary btn-confirm">Confirm Upgrade</button>
|
<button type="button" class="btn btn-primary btn-confirm">Confirm</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -332,6 +394,7 @@
|
|||||||
integrity="sha512-CSBhVREyzHAjAFfBlIBakjoRUKp5h7VSweP0InR/pAJyptH7peuhCsqAI/snV+TwZmXZqoUklpXp6R6wMnYf5Q=="
|
integrity="sha512-CSBhVREyzHAjAFfBlIBakjoRUKp5h7VSweP0InR/pAJyptH7peuhCsqAI/snV+TwZmXZqoUklpXp6R6wMnYf5Q=="
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
|
||||||
|
<script src="static/repo.js"></script>
|
||||||
<script src="static/list-view.js"></script>
|
<script src="static/list-view.js"></script>
|
||||||
<script src="static/revisions-view.js"></script>
|
<script src="static/revisions-view.js"></script>
|
||||||
<script src="static/details-view.js"></script>
|
<script src="static/details-view.js"></script>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ function loadChartsList() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function buildChartCard(elm) {
|
function buildChartCard(elm) {
|
||||||
const card = $(`<div class="row m-0 py-3 bg-white rounded-1 b-shadow border-4 border-start">
|
const card = $(`<div class="row m-0 py-3 bg-white rounded-1 b-shadow border-4 border-start">
|
||||||
<div class="col-4 rel-name"><span class="link">release-name</span><div></div></div>
|
<div class="col-4 rel-name"><span class="link">release-name</span><div></div></div>
|
||||||
|
|||||||
120
pkg/dashboard/static/repo.js
Normal file
120
pkg/dashboard/static/repo.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
function loadRepoView() {
|
||||||
|
$("#sectionRepo .repo-details").hide()
|
||||||
|
$("#sectionRepo").show()
|
||||||
|
|
||||||
|
$.getJSON("/api/helm/repo").fail(function (xhr) {
|
||||||
|
reportError("Failed to get list of repositories", xhr)
|
||||||
|
}).done(function (data) {
|
||||||
|
const items = $("#sectionRepo .repo-list ul").empty()
|
||||||
|
|
||||||
|
data.forEach(function (elm) {
|
||||||
|
let opt = $('<li class="mb-2"><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
|
||||||
|
opt.attr('title', elm.url)
|
||||||
|
opt.find("input").val(elm.name).text(elm.name).data("item", elm)
|
||||||
|
opt.find("span").text(elm.name)
|
||||||
|
items.append(opt)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!data.length) {
|
||||||
|
items.text("No repositories found, try adding one")
|
||||||
|
}
|
||||||
|
|
||||||
|
items.find("input").click(function () {
|
||||||
|
const self = $(this)
|
||||||
|
const elm = self.data("item");
|
||||||
|
setHashParam("repo", elm.name)
|
||||||
|
$("#sectionRepo .repo-details").show()
|
||||||
|
$("#sectionRepo .repo-details h2").text(elm.name)
|
||||||
|
$("#sectionRepo .repo-details .url").text(elm.url)
|
||||||
|
|
||||||
|
$("#sectionRepo .repo-details ul").html('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>')
|
||||||
|
$.getJSON("/api/helm/repo/charts?name=" + elm.name).fail(function (xhr) {
|
||||||
|
reportError("Failed to get list of charts in repo", xhr)
|
||||||
|
}).done(function (data) {
|
||||||
|
$("#sectionRepo .repo-details ul").empty()
|
||||||
|
data.forEach(function (elm) {
|
||||||
|
const li = $(`<li class="row p-2 rounded">
|
||||||
|
<h6 class="col-3 py-2">` + elm.name.split('/').pop() + `</h6>
|
||||||
|
<div class="col py-2">` + elm.description + `</div>
|
||||||
|
<div class="col-1 py-2">` + elm.version + `</div>
|
||||||
|
<div class="col-1 action text-nowrap"><button class="btn btn-sm border-secondary bg-white">Install</button></div>
|
||||||
|
</li>`)
|
||||||
|
li.data("item", elm)
|
||||||
|
|
||||||
|
if (elm.installed_namespace) {
|
||||||
|
li.find("button").text("View").addClass("btn-success").removeClass("bg-white")
|
||||||
|
li.find(".action").prepend("<i class='bi-check-circle-fill me-1 text-success' title='Already installed'></i>")
|
||||||
|
}
|
||||||
|
|
||||||
|
li.click(repoChartClicked)
|
||||||
|
|
||||||
|
$("#sectionRepo .repo-details ul").append(li)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (getHashParam("repo")) {
|
||||||
|
items.find("input[value='" + getHashParam("repo") + "']").click()
|
||||||
|
} else {
|
||||||
|
items.find("input").first().click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#sectionRepo .repo-list .btn").click(function () {
|
||||||
|
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
|
||||||
|
myModal.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#repoAddModal .btn-confirm").click(function () {
|
||||||
|
$("#repoAddModal .btn-confirm").prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>')
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: "/api/helm/repo",
|
||||||
|
data: $("#repoAddModal form").serialize(),
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
reportError("Failed to add repo", xhr)
|
||||||
|
}).done(function () {
|
||||||
|
setHashParam("repo", $("#repoAddModal form input[name=name]").val())
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#sectionRepo .btn-remove").click(function () {
|
||||||
|
if (confirm("Confirm removing repository?")) {
|
||||||
|
$.ajax({
|
||||||
|
type: 'DELETE',
|
||||||
|
url: "/api/helm/repo?name=" + $("#sectionRepo .repo-details h2").text(),
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
reportError("Failed to add repo", xhr)
|
||||||
|
}).done(function () {
|
||||||
|
setHashParam("repo", null)
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#sectionRepo .btn-update").click(function () {
|
||||||
|
$("#sectionRepo .btn-update i").removeClass("bi-arrow-repeat").append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: "/api/helm/repo/update?name=" + $("#sectionRepo .repo-details h2").text(),
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
reportError("Failed to add repo", xhr)
|
||||||
|
}).done(function () {
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function repoChartClicked() {
|
||||||
|
const self = $(this)
|
||||||
|
const elm = self.data("item")
|
||||||
|
if (elm.installed_namespace) {
|
||||||
|
setHashParam("section", null)
|
||||||
|
setHashParam("namespace", elm.installed_namespace)
|
||||||
|
setHashParam("chart", elm.installed_name)
|
||||||
|
window.location.reload()
|
||||||
|
} else {
|
||||||
|
popUpUpgrade(elm)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,27 @@ $(function () {
|
|||||||
const context = getHashParam("context")
|
const context = getHashParam("context")
|
||||||
fillClusterList(data, context);
|
fillClusterList(data, context);
|
||||||
|
|
||||||
|
initView(); // can only do it after loading cluster list
|
||||||
|
})
|
||||||
|
|
||||||
|
$.getJSON("/api/scanners").fail(function (xhr) {
|
||||||
|
reportError("Failed to get list of scanners", xhr)
|
||||||
|
}).done(function (data) {
|
||||||
|
if (!data.length) {
|
||||||
|
$("#upgradeModal .btn-scan").hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
$(".section").hide()
|
||||||
|
|
||||||
|
const section = getHashParam("section")
|
||||||
|
if (section === "repository") {
|
||||||
|
$("#topNav ul a.section-repo").addClass("active")
|
||||||
|
loadRepoView()
|
||||||
|
} else {
|
||||||
|
$("#topNav ul a.section-installed").addClass("active")
|
||||||
const namespace = getHashParam("namespace")
|
const namespace = getHashParam("namespace")
|
||||||
const chart = getHashParam("chart")
|
const chart = getHashParam("chart")
|
||||||
if (!chart) {
|
if (!chart) {
|
||||||
@@ -18,27 +39,27 @@ $(function () {
|
|||||||
} else {
|
} else {
|
||||||
loadChartHistory(namespace, chart)
|
loadChartHistory(namespace, chart)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$.getJSON("/api/scanners").fail(function (xhr) {
|
$("#topNav ul a").click(function () {
|
||||||
reportError("Failed to get list of scanners", xhr)
|
const self = $(this)
|
||||||
}).done(function (data) {
|
|
||||||
for (let n = 0; n < data.length; n++) {
|
|
||||||
const item = $(`
|
|
||||||
<label class="form-check-label me-4">
|
|
||||||
<input class="form-check-input me-1" type="checkbox" checked name="scanner" value="` + data[n] + `"> ` + data[n] + `
|
|
||||||
</label>`)
|
|
||||||
|
|
||||||
$("#nav-scanners form span").prepend(item)
|
$("#topNav ul a").removeClass("active")
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.length) {
|
const ctx = getHashParam("context")
|
||||||
$("#upgradeModal .btn-scan").hide()
|
setHashParam(null, null)
|
||||||
}
|
setHashParam("context", ctx)
|
||||||
})
|
|
||||||
|
if (self.hasClass("section-repo")) {
|
||||||
|
setHashParam("section", "repository")
|
||||||
|
} else {
|
||||||
|
setHashParam("section", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
initView()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const myAlert = document.getElementById('errorAlert')
|
const myAlert = document.getElementById('errorAlert')
|
||||||
myAlert.addEventListener('close.bs.alert', event => {
|
myAlert.addEventListener('close.bs.alert', event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -60,8 +81,14 @@ function getHashParam(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setHashParam(name, val) {
|
function setHashParam(name, val) {
|
||||||
const params = new URLSearchParams(window.location.hash.substring(1))
|
let params = new URLSearchParams(window.location.hash.substring(1))
|
||||||
params.set(name, val)
|
if (!name) {
|
||||||
|
params = new URLSearchParams()
|
||||||
|
} else if (!val) {
|
||||||
|
params.delete(name)
|
||||||
|
} else {
|
||||||
|
params.set(name, val)
|
||||||
|
}
|
||||||
window.location.hash = new URLSearchParams(params).toString()
|
window.location.hash = new URLSearchParams(params).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,14 +113,14 @@ function statusStyle(status, card, txt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCleanClusterName(rawClusterName) {
|
function getCleanClusterName(rawClusterName) {
|
||||||
if (rawClusterName.indexOf('arn') == 0) {
|
if (rawClusterName.indexOf('arn') === 0) {
|
||||||
// AWS cluster
|
// AWS cluster
|
||||||
clusterSplit = rawClusterName.split(':')
|
const clusterSplit = rawClusterName.split(':')
|
||||||
clusterName = clusterSplit.at(-1).split("/").at(-1)
|
const clusterName = clusterSplit.at(-1).split("/").at(-1)
|
||||||
region = clusterSplit.at(-3)
|
const region = clusterSplit.at(-3)
|
||||||
return region + "/" + clusterName + ' [AWS]'
|
return region + "/" + clusterName + ' [AWS]'
|
||||||
}
|
}
|
||||||
if (rawClusterName.indexOf('gke') == 0) {
|
if (rawClusterName.indexOf('gke') === 0) {
|
||||||
// GKE cluster
|
// GKE cluster
|
||||||
return rawClusterName.split('_').at(-2) + '/' + rawClusterName.split('_').at(-1) + ' [GKE]'
|
return rawClusterName.split('_').at(-2) + '/' + rawClusterName.split('_').at(-1) + ' [GKE]'
|
||||||
}
|
}
|
||||||
@@ -102,13 +129,11 @@ function getCleanClusterName(rawClusterName) {
|
|||||||
|
|
||||||
function fillClusterList(data, context) {
|
function fillClusterList(data, context) {
|
||||||
data.forEach(function (elm) {
|
data.forEach(function (elm) {
|
||||||
// aws CLI uses complicated context names, the suffix does not work well
|
let label = getCleanClusterName(elm.Name)
|
||||||
// maybe we should have an `if` statement here
|
|
||||||
let label = elm.Name //+ " (" + elm.Cluster + "/" + elm.AuthInfo + "/" + elm.Namespace + ")"
|
|
||||||
let opt = $('<li><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
|
let opt = $('<li><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
|
||||||
opt.attr('title', label)
|
opt.attr('title', elm.Name)
|
||||||
opt.find("input").val(elm.Name).text(label)
|
opt.find("input").val(elm.Name).text(label)
|
||||||
opt.find("span").text(getCleanClusterName(label))
|
opt.find("span").text(label)
|
||||||
if (elm.IsCurrent && !context) {
|
if (elm.IsCurrent && !context) {
|
||||||
opt.find("input").prop("checked", true)
|
opt.find("input").prop("checked", true)
|
||||||
setCurrentContext(elm.Name)
|
setCurrentContext(elm.Name)
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.link, .nav-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.strike {
|
.strike {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
@@ -325,7 +329,7 @@ span.link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#nav-resources .bg-secondary {
|
#nav-resources .bg-secondary {
|
||||||
background-color: #E6E7EB!important;
|
background-color: #E6E7EB !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.res-actions .btn-sm {
|
.res-actions .btn-sm {
|
||||||
@@ -350,3 +354,15 @@ span.link {
|
|||||||
#describeModalBody pre {
|
#describeModalBody pre {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sectionRepo .repo-details ul .row .btn {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sectionRepo .repo-details ul .row:hover {
|
||||||
|
background-color: #F4F7FA !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sectionRepo .repo-details ul .row:hover .btn {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
@@ -122,6 +122,7 @@ func (d *DataLayer) ListContexts() (res []KubeContext, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) {
|
func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) {
|
||||||
|
// TODO: filter by namespace
|
||||||
out, err := d.runCommandHelm("ls", "--all", "--all-namespaces", "--output", "json", "--time-format", time.RFC3339)
|
out, err := d.runCommandHelm("ls", "--all", "--all-namespaces", "--output", "json", "--time-format", time.RFC3339)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -159,8 +160,13 @@ func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*His
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) ChartRepoVersions(chartName string) (res []RepoChartElement, err error) {
|
func (d *DataLayer) ChartRepoVersions(chartName string) (res []*RepoChartElement, err error) {
|
||||||
cmd := []string{"search", "repo", "--regexp", "/" + chartName + "\v", "--versions", "--output", "json"}
|
search := "/" + chartName + "\v"
|
||||||
|
if strings.Contains(chartName, "/") {
|
||||||
|
search = "\v" + chartName + "\v"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := []string{"search", "repo", "--regexp", search, "--versions", "--output", "json"}
|
||||||
out, err := d.runCommandHelm(cmd...)
|
out, err := d.runCommandHelm(cmd...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -173,6 +179,46 @@ func (d *DataLayer) ChartRepoVersions(chartName string) (res []RepoChartElement,
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ChartRepoCharts(repoName string) (res []*RepoChartElement, err error) {
|
||||||
|
cmd := []string{"search", "repo", "--regexp", "\v" + repoName + "/", "--output", "json"}
|
||||||
|
out, err := d.runCommandHelm(cmd...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(out), &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ins, err := d.ListInstalled()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
enrichRepoChartsWithInstalled(res, ins)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func enrichRepoChartsWithInstalled(charts []*RepoChartElement, installed []ReleaseElement) {
|
||||||
|
for _, chart := range charts {
|
||||||
|
for _, rel := range installed {
|
||||||
|
c, _, err := utils.ChartAndVersion(rel.Chart)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to parse chart: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pieces := strings.Split(chart.Name, "/")
|
||||||
|
if pieces[1] == c {
|
||||||
|
chart.InstalledNamespace = rel.Namespace
|
||||||
|
chart.InstalledName = rel.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type SectionFn = func(string, string, int, bool) (string, error) // TODO: rework it into struct-based argument?
|
type SectionFn = func(string, string, int, bool) (string, error) // TODO: rework it into struct-based argument?
|
||||||
|
|
||||||
func (d *DataLayer) RevisionManifests(namespace string, chartName string, revision int, _ bool) (res string, err error) {
|
func (d *DataLayer) RevisionManifests(namespace string, chartName string, revision int, _ bool) (res string, err error) {
|
||||||
@@ -305,7 +351,7 @@ func (d *DataLayer) DescribeResource(namespace string, kind string, name string)
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) UninstallChart(namespace string, name string) error {
|
func (d *DataLayer) ChartUninstall(namespace string, name string) error {
|
||||||
_, err := d.runCommandHelm("uninstall", name, "--namespace", namespace)
|
_, err := d.runCommandHelm("uninstall", name, "--namespace", namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -335,8 +381,8 @@ func (d *DataLayer) ChartRepoUpdate(name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) ChartUpgrade(namespace string, name string, repoChart string, version string, justTemplate bool, values string) (string, error) {
|
func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string, version string, justTemplate bool, values string, reuseVals bool) (string, error) {
|
||||||
if values == "" {
|
if values == "" && reuseVals {
|
||||||
oldVals, err := d.RevisionValues(namespace, name, 0, true)
|
oldVals, err := d.RevisionValues(namespace, name, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -344,13 +390,13 @@ func (d *DataLayer) ChartUpgrade(namespace string, name string, repoChart string
|
|||||||
values = oldVals
|
values = oldVals
|
||||||
}
|
}
|
||||||
|
|
||||||
oldValsFile, close1, err := utils.TempFile(values)
|
valsFile, close1, err := utils.TempFile(values)
|
||||||
defer close1()
|
defer close1()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := []string{"upgrade", name, repoChart, "--version", version, "--namespace", namespace, "--values", oldValsFile, "--output", "json"}
|
cmd := []string{"upgrade", "--install", "--create-namespace", name, repoChart, "--version", version, "--namespace", namespace, "--values", valsFile, "--output", "json"}
|
||||||
if justTemplate {
|
if justTemplate {
|
||||||
cmd = append(cmd, "--dry-run")
|
cmd = append(cmd, "--dry-run")
|
||||||
}
|
}
|
||||||
@@ -375,6 +421,37 @@ func (d *DataLayer) ShowValues(chart string, ver string) (string, error) {
|
|||||||
return d.runCommandHelm("show", "values", chart, "--version", ver)
|
return d.runCommandHelm("show", "values", chart, "--version", ver)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ChartRepoList() (res []RepositoryElement, err error) {
|
||||||
|
out, err := d.runCommandHelm("repo", "list", "--output", "json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(out), &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ChartRepoAdd(name string, url string) (string, error) {
|
||||||
|
out, err := d.runCommandHelm("repo", "add", "--force-update", name, url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ChartRepoDelete(name string) (string, error) {
|
||||||
|
out, err := d.runCommandHelm("repo", "remove", name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, 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)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// unpleasant copy from Helm sources, where they have it non-public
|
// unpleasant copy from Helm sources, where they have it non-public
|
||||||
|
|
||||||
type ReleaseElement struct {
|
type ReleaseElement struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
@@ -32,4 +33,12 @@ type RepoChartElement struct {
|
|||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
AppVersion string `json:"app_version"`
|
AppVersion string `json:"app_version"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
|
InstalledNamespace string `json:"installed_namespace"` // custom addition on top of Helm
|
||||||
|
InstalledName string `json:"installed_name"` // custom addition on top of Helm
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepositoryElement struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user