mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-26 06:18:04 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b8d959491 | ||
|
|
269895ae31 | ||
|
|
47929785e7 | ||
|
|
ab17544c96 | ||
|
|
5ea54f9257 | ||
|
|
fa48cf5435 | ||
|
|
91fd3793c7 | ||
|
|
7b6e9f1748 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -34,4 +34,10 @@ jobs:
|
|||||||
- name: Test Binary is Runnable
|
- name: Test Binary is Runnable
|
||||||
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
|
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v3.2.0
|
||||||
|
with:
|
||||||
|
# version: latest
|
||||||
|
# skip-go-installation: true
|
||||||
|
skip-pkg-cache: true
|
||||||
|
skip-build-cache: true
|
||||||
|
# args: --timeout=15m
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
builds:
|
builds:
|
||||||
- main: ./main.go
|
- main: ./main.go
|
||||||
binary: helm-dashboard
|
binary: helm-dashboard
|
||||||
ldflags: -s -w -X main.version={{.Version}} -X main.version={{.Version}} -X main.version={{.Version}} -X main.date={{.Date}}
|
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||||
goos:
|
goos:
|
||||||
- windows
|
- windows
|
||||||
- darwin
|
- darwin
|
||||||
|
|||||||
62
README.md
62
README.md
@@ -11,62 +11,76 @@ Prerequisites: `helm` and `kubectl` binaries installed and operational.
|
|||||||
Until we make our repo public, we have to use a custom way to install the plugin.
|
Until we make our repo public, we have to use a custom way to install the plugin.
|
||||||
|
|
||||||
There is a need to build binary for plugin to function, run:
|
There is a need to build binary for plugin to function, run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
go build -o bin/dashboard .
|
go build -o bin/dashboard .
|
||||||
```
|
```
|
||||||
|
|
||||||
To install, checkout the source code and run from source dir:
|
To install, checkout the source code and run from source dir:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
helm plugin install .
|
helm plugin install .
|
||||||
```
|
```
|
||||||
|
|
||||||
Local install of plugin just creates a symlink, so making the changes and rebuilding the binary would not require to reinstall a plugin.
|
Local install 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:
|
To use the plugin, run in your terminal:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
helm dashboard
|
helm dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, use the web UI.
|
Then, use the web UI.
|
||||||
|
|
||||||
## Uninstalling
|
## Uninstalling
|
||||||
|
|
||||||
To uninstall, run:
|
To uninstall, run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
helm plugin uninstall dashboard
|
helm plugin uninstall dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
## Support Channels
|
## Support Channels
|
||||||
|
|
||||||
We have two main channels for supporting the tool users: [Slack community](#TODO) for general conversations and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
|
We have two main channels for supporting the Helm Dashboard users: [Slack community](#TODO) for general conversations
|
||||||
|
and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
### Internal Milestone 1
|
### First Public Version
|
||||||
|
|
||||||
- Helm Plugin Packaging
|
- Helm Plugin Packaging
|
||||||
- CLI launcher
|
- CLI launcher
|
||||||
- Web Server with REST API
|
- Web Server with REST API
|
||||||
|
- Listing the installed applications
|
||||||
|
- View k8s resources created by the application (describe, status)
|
||||||
### First Public Version
|
- Viewing revision history for application
|
||||||
Listing the installed applications
|
- View manifest diffs between revisions, also changelogs etc
|
||||||
View k8s resources created by the application (describe, status)
|
- Analytics reporting (telemetry)
|
||||||
Viewing revision history for application
|
- Rollback to a revision
|
||||||
View manifest diffs between revisions, also changelogs etc
|
- Check for repo updates & upgrade flow
|
||||||
Analytics reporting (telemetry)
|
- Uninstalling the app completely
|
||||||
|
- Switch clusters
|
||||||
|
- Show manifest/describe upon clicking on resource
|
||||||
|
|
||||||
### Further Ideas
|
### Further Ideas
|
||||||
Setting parameter values and installing
|
- Have cleaner idea on the web API structure
|
||||||
Installing new app from repo
|
- Recognise & show ArgoCD-originating charts/objects, those `helm ls` does not show
|
||||||
Uninstalling the app completely
|
- Recognise the revisions that are rollbacks by their description and mark in timeline
|
||||||
Reconfiguring the application
|
|
||||||
Rollback a revision
|
|
||||||
|
|
||||||
Validate manifests before deploy and get better errors
|
#### Topic "Validating Manifests"
|
||||||
Switch clusters (?)
|
|
||||||
Browsing repositories
|
|
||||||
Adding new repository
|
|
||||||
|
|
||||||
Recognise & show ArgoCD-originating charts/objects
|
- Validate manifests before deploy and get better errors
|
||||||
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
|
|
||||||
|
#### Iteration "Value Setting"
|
||||||
|
|
||||||
|
- Setting parameter values and installing
|
||||||
|
- Reconfiguring the application
|
||||||
|
|
||||||
|
#### Iteration "Repo View"
|
||||||
|
|
||||||
|
- Browsing repositories
|
||||||
|
- Adding new repository
|
||||||
|
- Installing new app from repo
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -44,12 +42,13 @@ func NewRouter(abortWeb ControlChan, data *DataLayer) *gin.Engine {
|
|||||||
api = gin.Default()
|
api = gin.Default()
|
||||||
}
|
}
|
||||||
|
|
||||||
api.Use(noCache)
|
|
||||||
api.Use(contextSetter(data))
|
api.Use(contextSetter(data))
|
||||||
|
api.Use(noCache)
|
||||||
api.Use(errorHandler)
|
api.Use(errorHandler)
|
||||||
configureStatic(api)
|
|
||||||
|
|
||||||
|
configureStatic(api)
|
||||||
configureRoutes(abortWeb, data, api)
|
configureRoutes(abortWeb, data, api)
|
||||||
|
|
||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,152 +58,29 @@ func configureRoutes(abortWeb ControlChan, data *DataLayer, api *gin.Engine) {
|
|||||||
abortWeb <- struct{}{}
|
abortWeb <- struct{}{}
|
||||||
})
|
})
|
||||||
|
|
||||||
api.GET("/api/helm/charts", func(c *gin.Context) {
|
configureHelms(api.Group("/api/helm"), data)
|
||||||
res, err := data.ListInstalled()
|
configureKubectls(api.Group("/api/kube"), data)
|
||||||
if err != nil {
|
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
|
||||||
})
|
|
||||||
|
|
||||||
api.GET("/api/helm/charts/history", func(c *gin.Context) {
|
|
||||||
cName := c.Query("chart")
|
|
||||||
cNamespace := c.Query("namespace")
|
|
||||||
if cName == "" {
|
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := data.ChartHistory(cNamespace, cName)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
|
||||||
})
|
|
||||||
|
|
||||||
api.GET("/api/helm/charts/resources", func(c *gin.Context) {
|
|
||||||
cName := c.Query("chart")
|
|
||||||
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 {
|
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := data.RevisionManifestsParsed(cNamespace, cName, cRev)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
|
||||||
})
|
|
||||||
|
|
||||||
configureKubectls(api, data)
|
|
||||||
|
|
||||||
sections := map[string]SectionFn{
|
|
||||||
"manifests": data.RevisionManifests,
|
|
||||||
"values": data.RevisionValues,
|
|
||||||
"notes": data.RevisionNotes,
|
|
||||||
}
|
|
||||||
|
|
||||||
api.GET("/api/helm/charts/:section", func(c *gin.Context) {
|
|
||||||
functor, found := sections[c.Param("section")]
|
|
||||||
if !found {
|
|
||||||
_ = c.AbortWithError(http.StatusNotFound, errors.New("unsupported section: "+c.Param("section")))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cName := c.Query("chart")
|
|
||||||
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 {
|
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, 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"
|
|
||||||
if c.Param("section") == "notes" {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureKubectls(api *gin.Engine, data *DataLayer) {
|
func configureHelms(api *gin.RouterGroup, data *DataLayer) {
|
||||||
api.GET("/api/kube/contexts", func(c *gin.Context) {
|
h := HelmHandler{Data: data}
|
||||||
res, err := data.ListContexts()
|
api.GET("/charts", h.GetCharts)
|
||||||
if err != nil {
|
api.DELETE("/charts", h.Uninstall)
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
api.POST("/charts/rollback", h.Rollback)
|
||||||
return
|
api.GET("/charts/history", h.History)
|
||||||
}
|
api.GET("/charts/resources", h.Resources)
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
api.GET("/repo/search", h.RepoSearch)
|
||||||
})
|
api.POST("/repo/update", h.RepoUpdate)
|
||||||
|
api.GET("/charts/install", h.InstallPreview)
|
||||||
|
api.POST("/charts/install", h.Install)
|
||||||
|
api.GET("/charts/:section", h.GetInfoSection)
|
||||||
|
}
|
||||||
|
|
||||||
api.GET("/api/kube/resources/:kind", func(c *gin.Context) {
|
func configureKubectls(api *gin.RouterGroup, data *DataLayer) {
|
||||||
cName := c.Query("name")
|
h := KubeHandler{Data: data}
|
||||||
cNamespace := c.Query("namespace")
|
api.GET("/contexts", h.GetContexts)
|
||||||
if cName == "" {
|
api.GET("/resources/:kind", h.GetResourceInfo)
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: name"))
|
api.GET("/describe/:kind", h.Describe)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := data.GetResource(cNamespace, &GenericResource{
|
|
||||||
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
|
|
||||||
ObjectMeta: v1.ObjectMeta{Name: cName},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Status.Phase == "Active" || res.Status.Phase == "Error" {
|
|
||||||
_ = res.Name + ""
|
|
||||||
} else if res.Status.Phase == "" && len(res.Status.Conditions) > 0 {
|
|
||||||
res.Status.Phase = v12.CarpPhase(res.Status.Conditions[len(res.Status.Conditions)-1].Type)
|
|
||||||
res.Status.Message = res.Status.Conditions[len(res.Status.Conditions)-1].Message
|
|
||||||
res.Status.Reason = res.Status.Conditions[len(res.Status.Conditions)-1].Reason
|
|
||||||
if res.Status.Conditions[len(res.Status.Conditions)-1].Status == "False" {
|
|
||||||
res.Status.Phase = "Not" + res.Status.Phase
|
|
||||||
}
|
|
||||||
} else if res.Status.Phase == "" {
|
|
||||||
res.Status.Phase = "Exists"
|
|
||||||
}
|
|
||||||
|
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureStatic(api *gin.Engine) {
|
func configureStatic(api *gin.Engine) {
|
||||||
@@ -246,3 +122,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
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/hexops/gotextdiff/span"
|
"github.com/hexops/gotextdiff/span"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
"io/ioutil"
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -214,7 +215,8 @@ func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*his
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) ChartRepoVersions(chartName string) (res []repoChartElement, err error) {
|
func (d *DataLayer) ChartRepoVersions(chartName string) (res []repoChartElement, err error) {
|
||||||
out, err := d.runCommandHelm("search", "repo", "--regexp", "/"+chartName+"\v", "--versions", "--output", "json")
|
cmd := []string{"search", "repo", "--regexp", "/" + chartName + "\v", "--versions", "--output", "json"}
|
||||||
|
out, err := d.runCommandHelm(cmd...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -229,7 +231,12 @@ func (d *DataLayer) ChartRepoVersions(chartName string) (res []repoChartElement,
|
|||||||
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) {
|
||||||
out, err := d.runCommandHelm("get", "manifest", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision))
|
cmd := []string{"get", "manifest", chartName, "--namespace", namespace}
|
||||||
|
if revision > 0 {
|
||||||
|
cmd = append(cmd, "--revision", strconv.Itoa(revision))
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := d.runCommandHelm(cmd...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -275,7 +282,12 @@ func (d *DataLayer) RevisionNotes(namespace string, chartName string, revision i
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) RevisionValues(namespace string, chartName string, revision int, onlyUserDefined bool) (res string, err error) {
|
func (d *DataLayer) RevisionValues(namespace string, chartName string, revision int, onlyUserDefined bool) (res string, err error) {
|
||||||
cmd := []string{"get", "values", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision), "--output", "yaml"}
|
cmd := []string{"get", "values", chartName, "--namespace", namespace, "--output", "yaml"}
|
||||||
|
|
||||||
|
if revision > 0 {
|
||||||
|
cmd = append(cmd, "--revision", strconv.Itoa(revision))
|
||||||
|
}
|
||||||
|
|
||||||
if !onlyUserDefined {
|
if !onlyUserDefined {
|
||||||
cmd = append(cmd, "--all")
|
cmd = append(cmd, "--all")
|
||||||
}
|
}
|
||||||
@@ -317,6 +329,85 @@ func (d *DataLayer) GetResource(namespace string, def *GenericResource) (*Generi
|
|||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) DescribeResource(namespace string, kind string, name string) (string, error) {
|
||||||
|
out, err := d.runCommandKubectl("describe", strings.ToLower(kind), name, "--namespace", namespace)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) UninstallChart(namespace string, name string) error {
|
||||||
|
_, err := d.runCommandHelm("uninstall", name, "--namespace", namespace)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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 (d *DataLayer) ChartRepoUpdate(name string) error {
|
||||||
|
cmd := []string{"repo", "update"}
|
||||||
|
if name != "" {
|
||||||
|
cmd = append(cmd, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.runCommandHelm(cmd...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ChartUpgrade(namespace string, name string, repoChart string, version string, justTemplate bool) (string, error) {
|
||||||
|
oldVals, err := d.RevisionValues(namespace, name, 0, false)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := ioutil.TempFile("", "helm_vals_")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(file.Name())
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(file.Name(), []byte(oldVals), 0600)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := []string{name, repoChart, "--version", version, "--namespace", namespace, "--values", file.Name()}
|
||||||
|
if justTemplate {
|
||||||
|
cmd = append([]string{"template"}, cmd...)
|
||||||
|
} else {
|
||||||
|
cmd = append([]string{"upgrade"}, cmd...)
|
||||||
|
cmd = append(cmd, "--output", "json")
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := d.runCommandHelm(cmd...)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if justTemplate {
|
||||||
|
manifests, err := d.RevisionManifests(namespace, name, 0, false)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
out = getDiff(strings.TrimSpace(manifests), strings.TrimSpace(out), "current.yaml", "upgraded.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -333,11 +424,16 @@ func RevisionDiff(functor SectionFn, ext string, namespace string, name string,
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
edits := myers.ComputeEdits(span.URIFromPath(""), manifest1, manifest2)
|
diff := getDiff(manifest1, manifest2, strconv.Itoa(revision1)+ext, strconv.Itoa(revision2)+ext)
|
||||||
unified := gotextdiff.ToUnified(strconv.Itoa(revision1)+ext, strconv.Itoa(revision2)+ext, manifest1, edits)
|
|
||||||
diff := fmt.Sprint(unified)
|
|
||||||
log.Debugf("The diff is: %s", diff)
|
|
||||||
return diff, nil
|
return diff, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDiff(text1 string, text2 string, name1 string, name2 string) string {
|
||||||
|
edits := myers.ComputeEdits(span.URIFromPath(""), text1, text2)
|
||||||
|
unified := gotextdiff.ToUnified(name1, name2, text1, edits)
|
||||||
|
diff := fmt.Sprint(unified)
|
||||||
|
log.Debugf("The diff is: %s", diff)
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
type GenericResource = v1.Carp
|
type GenericResource = v1.Carp
|
||||||
|
|||||||
205
pkg/dashboard/helmHandlers.go
Normal file
205
pkg/dashboard/helmHandlers.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package dashboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"helm.sh/helm/v3/pkg/release"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HelmHandler struct {
|
||||||
|
Data *DataLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) GetCharts(c *gin.Context) {
|
||||||
|
res, err := h.Data.ListInstalled()
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) Uninstall(c *gin.Context) {
|
||||||
|
qp, err := getQueryProps(c, false)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = h.Data.UninstallChart(qp.Namespace, qp.Name)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Status(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) Rollback(c *gin.Context) {
|
||||||
|
qp, err := getQueryProps(c, true)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.Data.Revert(qp.Namespace, qp.Name, qp.Revision)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Status(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) History(c *gin.Context) {
|
||||||
|
qp, err := getQueryProps(c, false)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.Data.ChartHistory(qp.Namespace, qp.Name)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) Resources(c *gin.Context) {
|
||||||
|
qp, err := getQueryProps(c, true)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.Data.RevisionManifestsParsed(qp.Namespace, qp.Name, qp.Revision)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) RepoSearch(c *gin.Context) {
|
||||||
|
qp, err := getQueryProps(c, false)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.Data.ChartRepoVersions(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 := getQueryProps(c, false)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.Data.ChartRepoUpdate(qp.Name)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) InstallPreview(c *gin.Context) {
|
||||||
|
out, err := chartInstall(c, h.Data, true)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.String(http.StatusOK, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) Install(c *gin.Context) {
|
||||||
|
out, err := chartInstall(c, h.Data, false)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := release.Release{}
|
||||||
|
err = json.Unmarshal([]byte(out), &res)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.IndentedJSON(http.StatusAccepted, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) GetInfoSection(c *gin.Context) {
|
||||||
|
qp, err := getQueryProps(c, true)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
flag := c.Query("flag") == "true"
|
||||||
|
rDiff := c.Query("revisionDiff")
|
||||||
|
res, err := handleGetSection(h.Data, c.Param("section"), rDiff, qp, flag)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.String(http.StatusOK, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chartInstall(c *gin.Context, data *DataLayer, justTemplate bool) (string, error) {
|
||||||
|
qp, err := getQueryProps(c, false)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := data.ChartUpgrade(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), justTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetSection(data *DataLayer, section string, rDiff string, qp *QueryProps, flag bool) (string, error) {
|
||||||
|
sections := map[string]SectionFn{
|
||||||
|
"manifests": data.RevisionManifests,
|
||||||
|
"values": data.RevisionValues,
|
||||||
|
"notes": data.RevisionNotes,
|
||||||
|
}
|
||||||
|
|
||||||
|
functor, found := sections[section]
|
||||||
|
if !found {
|
||||||
|
return "", errors.New("unsupported section: " + section)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rDiff != "" {
|
||||||
|
cRevDiff, err := strconv.Atoi(rDiff)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := ".yaml"
|
||||||
|
if section == "notes" {
|
||||||
|
ext = ".txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
69
pkg/dashboard/kubeHandlers.go
Normal file
69
pkg/dashboard/kubeHandlers.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package dashboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KubeHandler struct {
|
||||||
|
Data *DataLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *KubeHandler) GetContexts(c *gin.Context) {
|
||||||
|
res, err := h.Data.ListContexts()
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
|
||||||
|
qp, err := getQueryProps(c, false)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.Data.GetResource(qp.Namespace, &GenericResource{
|
||||||
|
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
|
||||||
|
ObjectMeta: v1.ObjectMeta{Name: qp.Name},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Status.Phase == "Active" || res.Status.Phase == "Error" {
|
||||||
|
_ = res.Name + ""
|
||||||
|
} else if res.Status.Phase == "" && len(res.Status.Conditions) > 0 {
|
||||||
|
res.Status.Phase = v12.CarpPhase(res.Status.Conditions[len(res.Status.Conditions)-1].Type)
|
||||||
|
res.Status.Message = res.Status.Conditions[len(res.Status.Conditions)-1].Message
|
||||||
|
res.Status.Reason = res.Status.Conditions[len(res.Status.Conditions)-1].Reason
|
||||||
|
if res.Status.Conditions[len(res.Status.Conditions)-1].Status == "False" {
|
||||||
|
res.Status.Phase = "Not" + res.Status.Phase
|
||||||
|
}
|
||||||
|
} else if res.Status.Phase == "" {
|
||||||
|
res.Status.Phase = "Exists"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *KubeHandler) Describe(c *gin.Context) {
|
||||||
|
qp, err := getQueryProps(c, false)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.Data.DescribeResource(qp.Namespace, c.Param("kind"), qp.Name)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.String(http.StatusOK, res)
|
||||||
|
}
|
||||||
@@ -7,8 +7,7 @@
|
|||||||
<title>Helm Dashboard</title>
|
<title>Helm Dashboard</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/>
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
|
||||||
<!-- CSS -->
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/lightfair.min.css"/>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/lightfair.min.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"/>
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"/>
|
||||||
<link href="static/styles.css" rel="stylesheet">
|
<link href="static/styles.css" rel="stylesheet">
|
||||||
@@ -16,8 +15,7 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<i class="fa-solid fa-arrow-trend-down"></i>
|
<nav class="navbar navbar-expand-lg bg-light rounded mb-2 mt-2">
|
||||||
<nav class="navbar navbar-expand-lg bg-light rounded" style="margin-bottom: 0.75rem">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div style="line-height: 90%; font-size: 1.5rem" class="navbar-brand">
|
<div style="line-height: 90%; font-size: 1.5rem" class="navbar-brand">
|
||||||
<img src="static/logo.png" style="height: 3rem; float: left" alt="Logo">
|
<img src="static/logo.png" style="height: 3rem; float: left" alt="Logo">
|
||||||
@@ -47,19 +45,24 @@
|
|||||||
<label for="cluster" style="margin-top: 0.5rem">K8s Context:</label>
|
<label for="cluster" style="margin-top: 0.5rem">K8s Context:</label>
|
||||||
<select id="cluster" class="form-control"></select>
|
<select id="cluster" class="form-control"></select>
|
||||||
</form>
|
</form>
|
||||||
<i class="btn fa fa-power-off text-muted" title="Shut down the Helm Dashboard application"></i>
|
<i class="btn bi-power text-muted" title="Shut down the Helm Dashboard application"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="bg-light p-5 pt-0 rounded display-none" id="sectionDetails">
|
||||||
<div class="bg-light p-5 pt-0 rounded" id="sectionDetails" style="display: none">
|
|
||||||
<span class="text-muted"
|
<span class="text-muted"
|
||||||
style="transform: rotate(270deg); z-index: 100; display: inline-block; position: relative; left:-4rem; top: 4rem; color: #BBB!important; text-transform: uppercase">Revisions</span>
|
style="transform: rotate(270deg); z-index: 100; display: inline-block; position: relative; left:-4rem; top: 4rem; color: #BBB!important; text-transform: uppercase">Revisions</span>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<h1><span class="name"></span>,
|
<h1><span class="name"></span>, revision <span class="rev"></span>
|
||||||
revision <span class="rev"></span></h1>
|
<span class="float-end" id="actionButtons">
|
||||||
|
<button id="btnUpgrade" class="opacity-10 btn btn-sm bg-secondary text-light bg-opacity-50 rounded-0 me-0 rounded-start ">Checking...</button><button id="btnUpgradeCheck" class="btn btn-sm text-muted btn-light border-secondary rounded-0 rounded-end ms-0" title="Check for newer chart version from repo"><i class="bi-repeat"></i><span class="spinner-border spinner-border-sm" style="display: none" role="status" aria-hidden="true"></span></button>
|
||||||
|
|
||||||
|
<button id="btnRollback" class="btn btn-sm bg-primary border border-secondary text-light" title="Rollback to this revision"><i class="bi-rewind-fill"></i> <span>Rollback</span></button>
|
||||||
|
<button id="btnUninstall" class="btn btn-sm bg-danger border border-secondary text-light" title="Uninstall the chart"><i class="bi-trash-fill"></i> Uninstall</button>
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
Chart <b id="chartName"></b>: <i id="revDescr"></i>
|
Chart <b id="chartName"></b>: <i id="revDescr"></i>
|
||||||
|
|
||||||
<nav class="mt-2">
|
<nav class="mt-2">
|
||||||
@@ -115,15 +118,82 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-light p-5 rounded" id="sectionList" style="display: none">
|
<div class="bg-light p-5 rounded display-none" id="sectionList">
|
||||||
<h1>Charts List</h1>
|
<h1>Charts List</h1>
|
||||||
<div id="charts" class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
<div id="charts" class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="errorAlert" class="display-none alert alert-sm alert-danger alert-dismissible position-absolute position-absolute top-0 start-50 translate-middle-x mt-3 border-danger" role="alert">
|
||||||
|
<h4 class="alert-heading"><i class="bi-exclamation-triangle-fill"></i> <span></span></h4>
|
||||||
|
<hr>
|
||||||
|
<p style="white-space: pre-wrap"></p>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="describeModal"
|
||||||
|
tabindex="-1" aria-labelledby="describeModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="describeModalLabel"></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="describeModalBody">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="confirmModal"
|
||||||
|
tabindex="-1" aria-labelledby="describeModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="confirmModalLabel"></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="confirmModalBody">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="modal" id="upgradeModal"
|
||||||
|
tabindex="-1" aria-labelledby="describeModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="upgradeModalLabel">
|
||||||
|
Upgrade <b class='text-success name'></b> from version <b class='text-success ver-old'></b> to <select class='fw-bold text-success ver-new'></select>
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="upgradeModalBody">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-success">Confirm Upgrade</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<footer class="text-center small mt-3">
|
||||||
|
Brought to you by <img src="https://komodor.com/wp-content/uploads/2021/05/favicon.png" style="height: 1rem"> <a href="https://komodor.io">Komodor.io</a> |
|
||||||
|
<i class="bi-github"></i>
|
||||||
|
<a href="https://github.com/komodorio/helm-dashboard">Project page on GitHub</a>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
|
||||||
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
|
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ const clusterSelect = $("#cluster");
|
|||||||
const chartsCards = $("#charts");
|
const chartsCards = $("#charts");
|
||||||
const revRow = $("#sectionDetails .row");
|
const revRow = $("#sectionDetails .row");
|
||||||
|
|
||||||
function reportError(err) {
|
function reportError(err, xhr) {
|
||||||
alert(err) // TODO: nice modal/baloon/etc
|
$("#errorAlert h4 span").text(err)
|
||||||
|
$("#errorAlert p").text(xhr.responseText)
|
||||||
|
$("#errorAlert").show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function revisionClicked(namespace, name, self) {
|
function revisionClicked(namespace, name, self) {
|
||||||
@@ -20,6 +22,14 @@ function revisionClicked(namespace, name, self) {
|
|||||||
$("#revDescr").addClass("text-danger")
|
$("#revDescr").addClass("text-danger")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rev = $("#specRev").data("last-rev") == elm.revision ? elm.revision - 1 : elm.revision
|
||||||
|
console.log(rev, $("#specRev").data("first-rev"))
|
||||||
|
if (!rev || getHashParam("revision") === $("#specRev").data("first-rev")) {
|
||||||
|
$("#btnRollback").hide()
|
||||||
|
} else {
|
||||||
|
$("#btnRollback").show().data("rev", rev).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()
|
||||||
@@ -46,7 +56,7 @@ $("#nav-tab [data-tab]").click(function () {
|
|||||||
} else {
|
} else {
|
||||||
const mode = getHashParam("mode")
|
const mode = getHashParam("mode")
|
||||||
if (!mode) {
|
if (!mode) {
|
||||||
$("#modePanel [data-mode=diff-prev]").trigger('click')
|
$("#modePanel [data-mode=view]").trigger('click')
|
||||||
} else {
|
} else {
|
||||||
$("#modePanel [data-mode=" + mode + "]").trigger('click')
|
$("#modePanel [data-mode=" + mode + "]").trigger('click')
|
||||||
}
|
}
|
||||||
@@ -71,17 +81,20 @@ $("#userDefinedVals").change(function () {
|
|||||||
function loadContentWrapper() {
|
function loadContentWrapper() {
|
||||||
let revDiff = 0
|
let revDiff = 0
|
||||||
const revision = parseInt(getHashParam("revision"));
|
const revision = parseInt(getHashParam("revision"));
|
||||||
if (getHashParam("mode") === "diff-prev") {
|
if (revision === $("#specRev").data("first-rev")) {
|
||||||
|
revDiff = 0
|
||||||
|
} else if (getHashParam("mode") === "diff-prev") {
|
||||||
revDiff = revision - 1
|
revDiff = revision - 1
|
||||||
} else if (getHashParam("mode") === "diff-rev") {
|
} else if (getHashParam("mode") === "diff-rev") {
|
||||||
revDiff = $("#specRev").val()
|
revDiff = $("#specRev").val()
|
||||||
}
|
}
|
||||||
|
|
||||||
const flag = $("#userDefinedVals").prop("checked");
|
const flag = $("#userDefinedVals").prop("checked");
|
||||||
loadContent(getHashParam("tab"), getHashParam("namespace"), getHashParam("chart"), revision, revDiff, flag)
|
loadContent(getHashParam("tab"), getHashParam("namespace"), getHashParam("chart"), revision, revDiff, flag)
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadContent(mode, namespace, name, revision, revDiff, flag) {
|
function loadContent(mode, namespace, name, revision, revDiff, flag) {
|
||||||
let qstr = "chart=" + name + "&namespace=" + namespace + "&revision=" + revision
|
let qstr = "name=" + name + "&namespace=" + namespace + "&revision=" + revision
|
||||||
if (revDiff) {
|
if (revDiff) {
|
||||||
qstr += "&revisionDiff=" + revDiff
|
qstr += "&revisionDiff=" + revDiff
|
||||||
}
|
}
|
||||||
@@ -93,7 +106,7 @@ function loadContent(mode, namespace, name, revision, revDiff, flag) {
|
|||||||
let url = "/api/helm/charts/" + mode
|
let url = "/api/helm/charts/" + mode
|
||||||
url += "?" + qstr
|
url += "?" + qstr
|
||||||
const diffDisplay = $("#manifestText");
|
const diffDisplay = $("#manifestText");
|
||||||
diffDisplay.empty().append("<i class='fa fa-spinner fa-spin fa-2x'></i>")
|
diffDisplay.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
$.get(url).fail(function () {
|
$.get(url).fail(function () {
|
||||||
reportError("Failed to get diff of " + mode)
|
reportError("Failed to get diff of " + mode)
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
@@ -127,62 +140,73 @@ $('#specRev').keyup(function (event) {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
});
|
});
|
||||||
|
|
||||||
function loadChartHistory(namespace, name) {
|
function fillChartHistory(data, namespace, name) {
|
||||||
$("#sectionDetails").show()
|
revRow.empty()
|
||||||
$("#sectionDetails h1 span.name").text(name)
|
for (let x = 0; x < data.length; x++) {
|
||||||
revRow.empty().append("<div><i class='fa fa-spinner fa-spin fa-2x'></i></div>")
|
const elm = data[x]
|
||||||
$.getJSON("/api/helm/charts/history?chart=" + name + "&namespace=" + namespace).fail(function () {
|
$("#specRev").val(elm.revision).data("last-rev", elm.revision).data("last-chart-ver", elm.chart_ver)
|
||||||
reportError("Failed to get chart details")
|
|
||||||
}).done(function (data) {
|
if (!x) {
|
||||||
revRow.empty()
|
$("#specRev").data("first-rev", elm.revision)
|
||||||
for (let x = 0; x < data.length; x++) {
|
}
|
||||||
const elm = data[x]
|
|
||||||
$("#specRev").val(elm.revision)
|
const rev = $(`<div class="col-md-2 p-2 rounded border border-secondary bg-gradient bg-white">
|
||||||
const rev = $(`<div class="col-md-2 p-2 rounded border border-secondary bg-gradient bg-white">
|
|
||||||
<span><b class="rev-number"></b> - <span class="rev-status"></span></span><br/>
|
<span><b class="rev-number"></b> - <span class="rev-status"></span></span><br/>
|
||||||
<span class="text-muted">Chart:</span> <span class="chart-ver"></span><br/>
|
<span class="text-muted">Chart:</span> <span class="chart-ver"></span><br/>
|
||||||
<span class="text-muted">App ver:</span> <span class="app-ver"></span><br/>
|
<span class="text-muted">App ver:</span> <span class="app-ver"></span><br/>
|
||||||
<p class="small mt-3 mb-0"><span class="text-muted">Age:</span> <span class="rev-age"></span><br/>
|
<p class="small mt-3 mb-0"><span class="text-muted">Age:</span> <span class="rev-age"></span><br/>
|
||||||
<span class="text-muted rev-date"></span><br/></p>
|
<span class="text-muted rev-date"></span><br/></p>
|
||||||
</div>`)
|
</div>`)
|
||||||
rev.find(".rev-number").text("#" + elm.revision)
|
rev.find(".rev-number").text("#" + elm.revision)
|
||||||
rev.find(".app-ver").text(elm.app_version)
|
rev.find(".app-ver").text(elm.app_version)
|
||||||
rev.find(".chart-ver").text(elm.chart_ver)
|
rev.find(".chart-ver").text(elm.chart_ver)
|
||||||
rev.find(".rev-date").text(elm.updated.replace("T", " "))
|
rev.find(".rev-date").text(elm.updated.replace("T", " "))
|
||||||
rev.find(".rev-age").text(getAge(elm, data[x + 1]))
|
rev.find(".rev-age").text(getAge(elm, data[x + 1]))
|
||||||
rev.find(".rev-status").text(elm.status)
|
rev.find(".rev-status").text(elm.status)
|
||||||
rev.find(".fa").attr("title", elm.action)
|
rev.find(".fa").attr("title", elm.action)
|
||||||
|
|
||||||
if (elm.status === "failed") {
|
if (elm.status === "failed") {
|
||||||
rev.find(".rev-status").parent().addClass("text-danger")
|
rev.find(".rev-status").parent().addClass("text-danger")
|
||||||
}
|
|
||||||
|
|
||||||
switch (elm.action) {
|
|
||||||
case "app_upgrade":
|
|
||||||
rev.find(".app-ver").append(" <i class='fa fa-angle-double-up text-success'></i>")
|
|
||||||
break
|
|
||||||
case "app_downgrade":
|
|
||||||
rev.find(".app-ver").append(" <i class='fa fa-angle-double-down text-danger'></i>")
|
|
||||||
break
|
|
||||||
case "chart_upgrade":
|
|
||||||
rev.find(".chart-ver").append(" <i class='fa fa-angle-up text-success'></i>")
|
|
||||||
break
|
|
||||||
case "chart_downgrade":
|
|
||||||
rev.find(".chart-ver").append(" <i class='fa fa-angle-down text-danger'></i>")
|
|
||||||
break
|
|
||||||
case "reconfigure": // ?
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
rev.data("elm", elm)
|
|
||||||
rev.addClass("rev-" + elm.revision)
|
|
||||||
rev.click(function () {
|
|
||||||
revisionClicked(namespace, name, $(this))
|
|
||||||
})
|
|
||||||
|
|
||||||
revRow.append(rev)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (elm.action) {
|
||||||
|
case "app_upgrade":
|
||||||
|
rev.find(".app-ver").append(" <i class='bi-chevron-double-up text-success'></i>")
|
||||||
|
break
|
||||||
|
case "app_downgrade":
|
||||||
|
rev.find(".app-ver").append(" <i class='bi-chevron-double-down text-danger'></i>")
|
||||||
|
break
|
||||||
|
case "chart_upgrade":
|
||||||
|
rev.find(".chart-ver").append(" <i class='bi-chevron-up text-success'></i>")
|
||||||
|
break
|
||||||
|
case "chart_downgrade":
|
||||||
|
rev.find(".chart-ver").append(" <i class='bi-chevron-down text-danger'></i>")
|
||||||
|
break
|
||||||
|
case "reconfigure": // ?
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
rev.data("elm", elm)
|
||||||
|
rev.addClass("rev-" + elm.revision)
|
||||||
|
rev.click(function () {
|
||||||
|
revisionClicked(namespace, name, $(this))
|
||||||
|
})
|
||||||
|
|
||||||
|
revRow.append(rev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadChartHistory(namespace, name) {
|
||||||
|
$("#sectionDetails").show()
|
||||||
|
$("#sectionDetails h1 span.name").text(name)
|
||||||
|
revRow.empty().append("<div><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span></div>")
|
||||||
|
$.getJSON("/api/helm/charts/history?name=" + name + "&namespace=" + namespace).fail(function () {
|
||||||
|
reportError("Failed to get chart details")
|
||||||
|
}).done(function (data) {
|
||||||
|
fillChartHistory(data, namespace, name);
|
||||||
|
|
||||||
|
checkUpgradeable(data[data.length - 1].chart_name)
|
||||||
|
|
||||||
const rev = getHashParam("revision")
|
const rev = getHashParam("revision")
|
||||||
if (rev) {
|
if (rev) {
|
||||||
revRow.find(".rev-" + rev).click()
|
revRow.find(".rev-" + rev).click()
|
||||||
@@ -192,6 +216,116 @@ function loadChartHistory(namespace, name) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$("#btnUpgradeCheck").click(function () {
|
||||||
|
const self = $(this)
|
||||||
|
self.find(".bi-repeat").hide()
|
||||||
|
self.find(".spinner-border").show()
|
||||||
|
const repoName = self.data("repo")
|
||||||
|
$.post("/api/helm/repo/update?name=" + repoName).fail(function () {
|
||||||
|
reportError("Failed to update chart repo")
|
||||||
|
}).done(function () {
|
||||||
|
self.find(".spinner-border").hide()
|
||||||
|
self.find(".bi-repeat").show()
|
||||||
|
|
||||||
|
checkUpgradeable(self.data("chart"))
|
||||||
|
$("#btnUpgradeCheck").prop("disabled", true).find(".fa").removeClass("fa-spin fa-spinner").addClass("fa-times")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function checkUpgradeable(name) {
|
||||||
|
$("#btnUpgrade").text("Checking...")
|
||||||
|
$.getJSON("/api/helm/repo/search?name=" + name).fail(function () {
|
||||||
|
reportError("Failed to find chart in repo")
|
||||||
|
}).done(function (data) {
|
||||||
|
if (!data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#upgradeModalLabel select').empty()
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
$('#upgradeModalLabel select').append("<option value='" + data[i].version + "'>" + data[i].version + "</option>")
|
||||||
|
}
|
||||||
|
|
||||||
|
const elm = data[0]
|
||||||
|
$("#btnUpgradeCheck").data("repo", elm.name.split('/').shift())
|
||||||
|
$("#btnUpgradeCheck").data("chart", elm.name.split('/').pop())
|
||||||
|
|
||||||
|
const verCur = $("#specRev").data("last-chart-ver");
|
||||||
|
const canUpgrade = isNewerVersion(verCur, elm.version);
|
||||||
|
$("#btnUpgradeCheck").prop("disabled", false)
|
||||||
|
if (canUpgrade) {
|
||||||
|
$("#btnUpgrade").removeClass("bg-secondary bg-opacity-50").addClass("bg-success").text("Upgrade to " + elm.version)
|
||||||
|
} else {
|
||||||
|
$("#btnUpgrade").removeClass("bg-success").addClass("bg-secondary bg-opacity-50").text("No upgrades")
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#btnUpgrade").off("click").click(function () {
|
||||||
|
popUpUpgrade($(this), verCur, elm)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#upgradeModalLabel select').change(function () {
|
||||||
|
const self = $(this)
|
||||||
|
|
||||||
|
$("#upgradeModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
$("#upgradeModal .btn-success").prop("disabled", true)
|
||||||
|
$.get(self.data("url") + "&version=" + self.val()).fail(function () {
|
||||||
|
reportError("Failed to get upgrade")
|
||||||
|
}).done(function (data) {
|
||||||
|
$("#upgradeModalBody").empty();
|
||||||
|
$("#upgradeModal .btn-success").prop("disabled", false)
|
||||||
|
|
||||||
|
const targetElement = document.getElementById('upgradeModalBody');
|
||||||
|
const configuration = {
|
||||||
|
inputFormat: 'diff', outputFormat: 'side-by-side',
|
||||||
|
drawFileList: false, showFiles: false, highlight: true,
|
||||||
|
};
|
||||||
|
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
||||||
|
diff2htmlUi.draw()
|
||||||
|
$("#upgradeModalBody").prepend("<p>Following changes will happen to cluster:</p>")
|
||||||
|
if (!data) {
|
||||||
|
$("#upgradeModalBody").html("No changes will happen to cluster")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#upgradeModal .btn-secondary").click(function () {
|
||||||
|
const self = $(this)
|
||||||
|
self.find(".fa").removeClass("fa-cloud-download").addClass("fa-spin fa-spinner").prop("disabled", true)
|
||||||
|
$("#btnUpgradeCheck").click()
|
||||||
|
$("#upgradeModal .btn-close").click()
|
||||||
|
})
|
||||||
|
|
||||||
|
function popUpUpgrade(self, verCur, elm) {
|
||||||
|
const name = getHashParam("chart");
|
||||||
|
let url = "/api/helm/charts/install?namespace=" + getHashParam("namespace") + "&name=" + name + "&chart=" + elm.name;
|
||||||
|
$('#upgradeModalLabel select').data("url", url)
|
||||||
|
|
||||||
|
self.prop("disabled", true)
|
||||||
|
$("#upgradeModalLabel .name").text(name)
|
||||||
|
$("#upgradeModalLabel .ver-old").text(verCur)
|
||||||
|
|
||||||
|
$('#upgradeModalLabel select').val(elm.version).trigger("change")
|
||||||
|
|
||||||
|
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
|
||||||
|
myModal.show()
|
||||||
|
|
||||||
|
$("#upgradeModal .btn-success").prop("disabled", true).off('click').click(function () {
|
||||||
|
$("#upgradeModal .btn-success").prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
$.ajax({
|
||||||
|
url: url + "&version=" + $('#upgradeModalLabel select').val(),
|
||||||
|
type: 'POST',
|
||||||
|
}).fail(function () {
|
||||||
|
reportError("Failed to upgrade the chart")
|
||||||
|
}).done(function (data) {
|
||||||
|
setHashParam("revision", data.version)
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function getHashParam(name) {
|
function getHashParam(name) {
|
||||||
const params = new URLSearchParams(window.location.hash.substring(1))
|
const params = new URLSearchParams(window.location.hash.substring(1))
|
||||||
return params.get(name)
|
return params.get(name)
|
||||||
@@ -203,46 +337,51 @@ function setHashParam(name, val) {
|
|||||||
window.location.hash = new URLSearchParams(params).toString()
|
window.location.hash = new URLSearchParams(params).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildChartCard(elm) {
|
||||||
|
const header = $("<div class='card-header'></div>")
|
||||||
|
header.append($('<div class="float-end"><h5 class="float-end text-muted text-end">#' + elm.revision + '</h5><br/><div class="badge">' + elm.status + "</div>"))
|
||||||
|
// TODO: for pending- and uninstalling, add the spinner
|
||||||
|
if (elm.status === "failed") {
|
||||||
|
header.find(".badge").addClass("bg-danger text-light")
|
||||||
|
} else if (elm.status === "deployed" || elm.status === "superseded") {
|
||||||
|
header.find(".badge").addClass("bg-info")
|
||||||
|
} else {
|
||||||
|
header.find(".badge").addClass("bg-light text-dark")
|
||||||
|
}
|
||||||
|
|
||||||
|
header.append($('<h5 class="card-title"><a href="#namespace=' + elm.namespace + '&name=' + elm.name + '" class="link-dark" style="text-decoration: none">' + elm.name + '</a></h5>'))
|
||||||
|
header.append($('<p class="card-text small text-muted"></p>').append("Chart: " + elm.chart))
|
||||||
|
|
||||||
|
const body = $("<div class='card-body'></div>")
|
||||||
|
body.append($('<p class="card-text"></p>').append("Namespace: " + elm.namespace))
|
||||||
|
body.append($('<p class="card-text"></p>').append("Version: " + elm.app_version))
|
||||||
|
body.append($('<p class="card-text"></p>').append("Updated: " + elm.updated))
|
||||||
|
|
||||||
|
let card = $("<div class='card'></div>").append(header).append(body);
|
||||||
|
|
||||||
|
card.data("chart", elm)
|
||||||
|
card.click(function () {
|
||||||
|
const self = $(this)
|
||||||
|
$("#sectionList").hide()
|
||||||
|
|
||||||
|
let chart = self.data("chart");
|
||||||
|
setHashParam("namespace", chart.namespace)
|
||||||
|
setHashParam("chart", chart.name)
|
||||||
|
|
||||||
|
loadChartHistory(chart.namespace, chart.name, elm.chart_name)
|
||||||
|
})
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
function loadChartsList() {
|
function loadChartsList() {
|
||||||
$("#sectionList").show()
|
$("#sectionList").show()
|
||||||
chartsCards.empty().append("<div><i class='fa fa-spinner fa-spin fa-2x'></i> Loading...</div>")
|
chartsCards.empty().append("<div><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span> Loading...</div>")
|
||||||
$.getJSON("/api/helm/charts").fail(function () {
|
$.getJSON("/api/helm/charts").fail(function (xhr) {
|
||||||
reportError("Failed to get list of charts")
|
reportError("Failed to get list of charts", xhr)
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
chartsCards.empty()
|
chartsCards.empty()
|
||||||
data.forEach(function (elm) {
|
data.forEach(function (elm) {
|
||||||
const header = $("<div class='card-header'></div>")
|
let card = buildChartCard(elm);
|
||||||
header.append($('<div class="float-end"><h5 class="float-end text-muted text-end">#' + elm.revision + '</h5><br/><div class="badge">' + elm.status + "</div>"))
|
|
||||||
// TODO: for pending- and uninstalling, add the spinner
|
|
||||||
if (elm.status === "failed") {
|
|
||||||
header.find(".badge").addClass("bg-danger text-light")
|
|
||||||
} else if (elm.status === "deployed" || elm.status === "superseded") {
|
|
||||||
header.find(".badge").addClass("bg-info")
|
|
||||||
} else {
|
|
||||||
header.find(".badge").addClass("bg-light text-dark")
|
|
||||||
}
|
|
||||||
|
|
||||||
header.append($('<h5 class="card-title"><a href="#namespace=' + elm.namespace + '&chart=' + elm.name + '" class="link-dark" style="text-decoration: none">' + elm.name + '</a></h5>'))
|
|
||||||
header.append($('<p class="card-text small text-muted"></p>').append("Chart: " + elm.chart))
|
|
||||||
|
|
||||||
const body = $("<div class='card-body'></div>")
|
|
||||||
body.append($('<p class="card-text"></p>').append("Namespace: " + elm.namespace))
|
|
||||||
body.append($('<p class="card-text"></p>').append("Version: " + elm.app_version))
|
|
||||||
body.append($('<p class="card-text"></p>').append("Updated: " + elm.updated))
|
|
||||||
|
|
||||||
let card = $("<div class='card'></div>").append(header).append(body);
|
|
||||||
|
|
||||||
card.data("chart", elm)
|
|
||||||
card.click(function () {
|
|
||||||
const self = $(this)
|
|
||||||
$("#sectionList").hide()
|
|
||||||
|
|
||||||
let chart = self.data("chart");
|
|
||||||
setHashParam("namespace", chart.namespace)
|
|
||||||
setHashParam("chart", chart.name)
|
|
||||||
loadChartHistory(chart.namespace, chart.name)
|
|
||||||
})
|
|
||||||
|
|
||||||
chartsCards.append($("<div class='col'></div>").append(card))
|
chartsCards.append($("<div class='col'></div>").append(card))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -250,7 +389,6 @@ function loadChartsList() {
|
|||||||
|
|
||||||
|
|
||||||
$(function () {
|
$(function () {
|
||||||
// cluster list
|
|
||||||
clusterSelect.change(function () {
|
clusterSelect.change(function () {
|
||||||
Cookies.set("context", clusterSelect.val())
|
Cookies.set("context", clusterSelect.val())
|
||||||
window.location.href = "/"
|
window.location.href = "/"
|
||||||
@@ -311,8 +449,8 @@ function getAge(obj1, obj2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showResources(namespace, chart, revision) {
|
function showResources(namespace, chart, revision) {
|
||||||
$("#nav-resources").empty().append("<i class='fa fa-spin fa-spinner fa-2x'></i>");
|
$("#nav-resources").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
|
||||||
let qstr = "chart=" + chart + "&namespace=" + namespace + "&revision=" + revision
|
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
|
||||||
let url = "/api/helm/charts/resources"
|
let url = "/api/helm/charts/resources"
|
||||||
url += "?" + qstr
|
url += "?" + qstr
|
||||||
$.getJSON(url).fail(function () {
|
$.getJSON(url).fail(function () {
|
||||||
@@ -325,7 +463,7 @@ function showResources(namespace, chart, revision) {
|
|||||||
<div class="input-group row">
|
<div class="input-group row">
|
||||||
<span class="input-group-text col-sm-2"><em class="text-muted small">` + res.kind + `</em></span>
|
<span class="input-group-text col-sm-2"><em class="text-muted small">` + res.kind + `</em></span>
|
||||||
<span class="input-group-text col-sm-6">` + res.metadata.name + `</span>
|
<span class="input-group-text col-sm-6">` + res.metadata.name + `</span>
|
||||||
<span class="form-control col-sm-4"><i class="fa fa-spinner fa-spin"></i> <span class="text-muted small">Getting status...</span></span>
|
<span class="form-control col-sm-4"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <span class="text-muted small">Getting status...</span></span>
|
||||||
</div>`)
|
</div>`)
|
||||||
$("#nav-resources").append(resBlock)
|
$("#nav-resources").append(resBlock)
|
||||||
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
|
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
|
||||||
@@ -337,23 +475,142 @@ function showResources(namespace, chart, revision) {
|
|||||||
badge.addClass("bg-success")
|
badge.addClass("bg-success")
|
||||||
} else if (["Exists"].includes(data.status.phase)) {
|
} else if (["Exists"].includes(data.status.phase)) {
|
||||||
badge.addClass("bg-success bg-opacity-50")
|
badge.addClass("bg-success bg-opacity-50")
|
||||||
|
} else if (["Progressing"].includes(data.status.phase)) {
|
||||||
|
badge.addClass("bg-warning")
|
||||||
} else {
|
} else {
|
||||||
badge.addClass("bg-danger")
|
badge.addClass("bg-danger")
|
||||||
}
|
}
|
||||||
|
|
||||||
resBlock.find(".form-control").empty().append(badge).append("<span class='text-muted small'>" + (data.status.message ? data.status.message : '') + "</span>")
|
const statusBlock = resBlock.find(".form-control.col-sm-4");
|
||||||
})
|
statusBlock.empty().append(badge).append("<span class='text-muted small'>" + (data.status.message ? data.status.message : '') + "</span>")
|
||||||
|
|
||||||
|
if (badge.text() !== "NotFound") {
|
||||||
|
statusBlock.prepend("<i class=\"btn bi-zoom-in float-end text-muted\"></i>")
|
||||||
|
statusBlock.find(".bi-zoom-in").click(function () {
|
||||||
|
showDescribe(ns, res.kind, res.metadata.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$(".fa-power-off").click(function () {
|
$(".bi-power").click(function () {
|
||||||
$(".fa-power-off").attr("disabled", "disabled").removeClass(".fa-power-off").addClass("fa-spin fa-spinner")
|
$(".bi-power").attr("disabled", "disabled").removeClass(".bi-power").append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/",
|
url: "/",
|
||||||
type: 'DELETE',
|
type: 'DELETE',
|
||||||
}).done(function () {
|
}).done(function () {
|
||||||
window.close();
|
window.close();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function showDescribe(ns, kind, name) {
|
||||||
|
$("#describeModalLabel").text("Describe " + kind + ": " + ns + " / " + name)
|
||||||
|
$("#describeModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
|
||||||
|
const myModal = new bootstrap.Modal(document.getElementById('describeModal'), {});
|
||||||
|
myModal.show()
|
||||||
|
$.get("/api/kube/describe/" + kind.toLowerCase() + "?name=" + name + "&namespace=" + ns).fail(function () {
|
||||||
|
reportError("Failed to describe resource")
|
||||||
|
}).done(function (data) {
|
||||||
|
data = hljs.highlight(data, {language: 'yaml'}).value
|
||||||
|
$("#describeModalBody").empty().append("<pre class='bg-white rounded p-3'></pre>").find("pre").html(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#btnUninstall").click(function () {
|
||||||
|
const chart = getHashParam('chart');
|
||||||
|
const namespace = getHashParam('namespace');
|
||||||
|
const revision = $("#specRev").data("last-rev")
|
||||||
|
$("#confirmModalLabel").html("Uninstall <b class='text-danger'>" + chart + "</b> from namespace <b class='text-danger'>" + namespace + "</b>")
|
||||||
|
$("#confirmModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
$("#confirmModal .btn-primary").prop("disabled", true).off('click').click(function () {
|
||||||
|
$("#confirmModal .btn-primary").prop("disabled", true).append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
const url = "/api/helm/charts?namespace=" + namespace + "&name=" + chart;
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'DELETE',
|
||||||
|
}).fail(function () {
|
||||||
|
reportError("Failed to delete the chart")
|
||||||
|
}).done(function () {
|
||||||
|
window.location.href = "/"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'), {});
|
||||||
|
myModal.show()
|
||||||
|
|
||||||
|
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
|
||||||
|
let url = "/api/helm/charts/resources"
|
||||||
|
url += "?" + qstr
|
||||||
|
$.getJSON(url).fail(function () {
|
||||||
|
reportError("Failed to get list of resources")
|
||||||
|
}).done(function (data) {
|
||||||
|
$("#confirmModalBody").empty().append("<p>Following resources will be deleted from the cluster:</p>");
|
||||||
|
$("#confirmModal .btn-primary").prop("disabled", false)
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const res = data[i]
|
||||||
|
$("#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('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
$("#confirmModal .btn-primary").prop("disabled", true).off('click').click(function () {
|
||||||
|
$("#confirmModal .btn-primary").prop("disabled", true).append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
const url = "/api/helm/charts/rollback?namespace=" + namespace + "&name=" + chart + "&revision=" + revisionNew;
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'POST',
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
reportError("Failed to rollback the chart", xhr)
|
||||||
|
}).done(function () {
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'), {});
|
||||||
|
myModal.show()
|
||||||
|
|
||||||
|
let qstr = "name=" + 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()
|
||||||
|
if (data) {
|
||||||
|
$("#confirmModalBody").prepend("<p>Following changes will happen to cluster:</p>")
|
||||||
|
} else {
|
||||||
|
$("#confirmModalBody").html("<p>No changes will happen to cluster</p>")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function isNewerVersion(oldVer, newVer) {
|
||||||
|
const oldParts = oldVer.split('.')
|
||||||
|
const newParts = newVer.split('.')
|
||||||
|
for (let i = 0; i < newParts.length; i++) {
|
||||||
|
const a = ~~newParts[i] // parse int
|
||||||
|
const b = ~~oldParts[i] // parse int
|
||||||
|
if (a > b) return true
|
||||||
|
if (a < b) return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -8,4 +8,8 @@
|
|||||||
|
|
||||||
.d2h-file-collapse, .d2h-tag {
|
.d2h-file-collapse, .d2h-tag {
|
||||||
opacity: 0; /* trollface */
|
opacity: 0; /* trollface */
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-none {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user