mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 11:48:04 +00:00
Initial features 4 (#5)
* Select works fine * Context switching works * Going to expand fns * Diff and value * Progressing * fix click events * highlight code * Add revision age info * Values diff * remove forgotten * Refactor data layer, add flag * UDV flag works * Diff with prev works
This commit is contained in:
19
README.md
19
README.md
@@ -1,22 +1,26 @@
|
|||||||
# Helm Dashboard
|
# <img src="pkg/dashboard/static/logo.png" height=30 style="height: 2rem"> Helm Dashboard
|
||||||
|
|
||||||
A simplified way of working with Helm.
|
A simplified way of working with Helm.
|
||||||
|
|
||||||
|
[<img src="screenshot.png" style="width: 100%; border: 1px solid silver">](screenshot.png)
|
||||||
|
|
||||||
## Local Testing
|
## Local Testing
|
||||||
|
|
||||||
Until we make our repo public, we have to use a custom way to install the plugin.
|
Prerequisites: `helm` and `kubectl` binaries installed and operational.
|
||||||
|
|
||||||
To install, checkout the source code and run from source dir:
|
Until we make our repo public, we have to use a custom way to install the plugin.
|
||||||
```shell
|
|
||||||
helm plugin install .
|
|
||||||
```
|
|
||||||
|
|
||||||
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 .
|
||||||
```
|
```
|
||||||
|
|
||||||
Local install of plugin just creates a symlink, so making the changes and rebuilding the binary would not require reinstall of a plugin.
|
To install, checkout the source code and run from source dir:
|
||||||
|
```shell
|
||||||
|
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.
|
||||||
|
|
||||||
To use the plugin, run in your terminal:
|
To use the plugin, run in your terminal:
|
||||||
```shell
|
```shell
|
||||||
@@ -65,3 +69,4 @@ Adding new repository
|
|||||||
|
|
||||||
Recognise & show ArgoCD-originating charts/objects
|
Recognise & show ArgoCD-originating charts/objects
|
||||||
Have cleaner idea on the web API structure
|
Have cleaner idea on the web API structure
|
||||||
|
See if we can build in Chechov or Validkube validation
|
||||||
@@ -14,6 +14,11 @@ import (
|
|||||||
//go:embed static/*
|
//go:embed static/*
|
||||||
var staticFS embed.FS
|
var staticFS embed.FS
|
||||||
|
|
||||||
|
func noCache(c *gin.Context) {
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
func errorHandler(c *gin.Context) {
|
func errorHandler(c *gin.Context) {
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
||||||
@@ -28,7 +33,7 @@ func errorHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(abortWeb ControlChan, data DataLayer) *gin.Engine {
|
func NewRouter(abortWeb ControlChan, data *DataLayer) *gin.Engine {
|
||||||
var api *gin.Engine
|
var api *gin.Engine
|
||||||
if os.Getenv("DEBUG") == "" {
|
if os.Getenv("DEBUG") == "" {
|
||||||
api = gin.New()
|
api = gin.New()
|
||||||
@@ -37,6 +42,8 @@ func NewRouter(abortWeb ControlChan, data DataLayer) *gin.Engine {
|
|||||||
api = gin.Default()
|
api = gin.Default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api.Use(noCache)
|
||||||
|
api.Use(contextSetter(data))
|
||||||
api.Use(errorHandler)
|
api.Use(errorHandler)
|
||||||
configureStatic(api)
|
configureStatic(api)
|
||||||
|
|
||||||
@@ -44,7 +51,7 @@ func NewRouter(abortWeb ControlChan, data DataLayer) *gin.Engine {
|
|||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureRoutes(abortWeb ControlChan, data DataLayer, api *gin.Engine) {
|
func configureRoutes(abortWeb ControlChan, data *DataLayer, api *gin.Engine) {
|
||||||
// server shutdown handler
|
// server shutdown handler
|
||||||
api.DELETE("/", func(c *gin.Context) {
|
api.DELETE("/", func(c *gin.Context) {
|
||||||
abortWeb <- struct{}{}
|
abortWeb <- struct{}{}
|
||||||
@@ -84,7 +91,19 @@ func configureRoutes(abortWeb ControlChan, data DataLayer, api *gin.Engine) {
|
|||||||
c.IndentedJSON(http.StatusOK, res)
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
api.GET("/api/helm/charts/manifest/diff", func(c *gin.Context) {
|
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")
|
cName := c.Query("chart")
|
||||||
cNamespace := c.Query("namespace")
|
cNamespace := c.Query("namespace")
|
||||||
if cName == "" {
|
if cName == "" {
|
||||||
@@ -92,26 +111,40 @@ func configureRoutes(abortWeb ControlChan, data DataLayer, api *gin.Engine) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cRev1, err := strconv.Atoi(c.Query("revision1"))
|
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 {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cRev2, err := strconv.Atoi(c.Query("revision2"))
|
ext := ".yaml"
|
||||||
if err != nil {
|
if c.Param("section") == "notes" {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
ext = ".txt"
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := data.RevisionManifestsDiff(cNamespace, cName, cRev1, cRev2)
|
res, err := RevisionDiff(functor, ext, cNamespace, cName, cRevDiff, cRev, flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
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 configureStatic(api *gin.Engine) {
|
func configureStatic(api *gin.Engine) {
|
||||||
@@ -143,3 +176,13 @@ func configureStatic(api *gin.Engine) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func contextSetter(data *DataLayer) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if context, ok := c.Request.Header["X-Kubecontext"]; ok {
|
||||||
|
log.Debugf("Setting current context to: %s", context)
|
||||||
|
data.KubeContext = context[0]
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ type DataLayer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *DataLayer) runCommand(cmd ...string) (string, error) {
|
func (l *DataLayer) runCommand(cmd ...string) (string, error) {
|
||||||
// TODO: --kube-context=context-name to juggle clusters
|
|
||||||
log.Debugf("Starting command: %s", cmd)
|
log.Debugf("Starting command: %s", cmd)
|
||||||
prog := exec.Command(cmd[0], cmd[1:]...)
|
prog := exec.Command(cmd[0], cmd[1:]...)
|
||||||
prog.Env = os.Environ()
|
prog.Env = os.Environ()
|
||||||
@@ -44,7 +43,7 @@ func (l *DataLayer) runCommand(cmd ...string) (string, error) {
|
|||||||
log.Warnf("STDERR:\n%s", serr)
|
log.Warnf("STDERR:\n%s", serr)
|
||||||
}
|
}
|
||||||
if eerr, ok := err.(*exec.ExitError); ok {
|
if eerr, ok := err.(*exec.ExitError); ok {
|
||||||
return "", fmt.Errorf("failed to run command %s: %s", cmd, eerr)
|
return "", fmt.Errorf("failed to run command %s:\nError: %s\nSTDERR:%s", cmd, eerr, serr)
|
||||||
}
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -93,10 +92,12 @@ func (l *DataLayer) CheckConnectivity() error {
|
|||||||
return errors.New("did not find any kubectl contexts configured")
|
return errors.New("did not find any kubectl contexts configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = l.runCommandHelm("env")
|
/*
|
||||||
|
_, err = l.runCommandHelm("env") // no point in doing is, since the default context may be invalid
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -158,7 +159,7 @@ func (l *DataLayer) ListInstalled() (res []releaseElement, err error) {
|
|||||||
|
|
||||||
func (l *DataLayer) ChartHistory(namespace string, chartName string) (res []*historyElement, err error) {
|
func (l *DataLayer) ChartHistory(namespace string, chartName string) (res []*historyElement, err error) {
|
||||||
// TODO: there is `max` but there is no `offset`
|
// TODO: there is `max` but there is no `offset`
|
||||||
out, err := l.runCommandHelm("history", chartName, "--namespace", namespace, "--max", "5", "--output", "json")
|
out, err := l.runCommandHelm("history", chartName, "--namespace", namespace, "--output", "json", "--max", "18")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -221,7 +222,9 @@ func (l *DataLayer) ChartRepoVersions(chartName string) (res []repoChartElement,
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *DataLayer) RevisionManifests(namespace string, chartName string, revision int) (res string, err error) {
|
type SectionFn = func(string, string, int, bool) (string, error)
|
||||||
|
|
||||||
|
func (l *DataLayer) RevisionManifests(namespace string, chartName string, revision int, _ bool) (res string, err error) {
|
||||||
out, err := l.runCommandHelm("get", "manifest", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision))
|
out, err := l.runCommandHelm("get", "manifest", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -229,19 +232,45 @@ func (l *DataLayer) RevisionManifests(namespace string, chartName string, revisi
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *DataLayer) RevisionManifestsDiff(namespace string, name string, revision1 int, revision2 int) (string, error) {
|
func (l *DataLayer) RevisionNotes(namespace string, chartName string, revision int, _ bool) (res string, err error) {
|
||||||
manifest1, err := l.RevisionManifests(namespace, name, revision1)
|
out, err := l.runCommandHelm("get", "notes", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *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"}
|
||||||
|
if !onlyUserDefined {
|
||||||
|
cmd = append(cmd, "--all")
|
||||||
|
}
|
||||||
|
out, err := l.runCommandHelm(cmd...)
|
||||||
|
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) {
|
||||||
|
if revision1 == 0 || revision2 == 0 {
|
||||||
|
log.Debugf("One of revisions is zero: %d %d", revision1, revision2)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest2, err := l.RevisionManifests(namespace, name, revision2)
|
manifest1, err := functor(namespace, name, revision1, flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest2, err := functor(namespace, name, revision2, flag)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
edits := myers.ComputeEdits(span.URIFromPath(""), manifest1, manifest2)
|
edits := myers.ComputeEdits(span.URIFromPath(""), manifest1, manifest2)
|
||||||
unified := gotextdiff.ToUnified("a.txt", "b.txt", manifest1, edits)
|
unified := gotextdiff.ToUnified(strconv.Itoa(revision1)+ext, strconv.Itoa(revision2)+ext, manifest1, edits)
|
||||||
diff := fmt.Sprint(unified)
|
diff := fmt.Sprint(unified)
|
||||||
|
log.Debugf("The diff is: %s", diff)
|
||||||
return diff, nil
|
return diff, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,13 +55,13 @@ func TestFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
_ = upgrade
|
_ = upgrade
|
||||||
|
|
||||||
manifests, err := data.RevisionManifests(chart.Namespace, chart.Name, history[len(history)-1].Revision)
|
manifests, err := data.RevisionManifests(chart.Namespace, chart.Name, history[len(history)-1].Revision, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_ = manifests
|
_ = manifests
|
||||||
|
|
||||||
diff, err := data.RevisionManifestsDiff(chart.Namespace, chart.Name, history[len(history)-1].Revision, history[len(history)-2].Revision)
|
diff, err := RevisionDiff(data.RevisionManifests, ".yaml", chart.Namespace, chart.Name, history[len(history)-1].Revision, history[len(history)-2].Revision, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func StartServer() (string, ControlChan) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abort := make(ControlChan)
|
abort := make(ControlChan)
|
||||||
api := NewRouter(abort, data)
|
api := NewRouter(abort, &data)
|
||||||
done := startBackgroundServer(address, api, abort)
|
done := startBackgroundServer(address, api, abort)
|
||||||
|
|
||||||
return "http://" + address, done
|
return "http://" + address, done
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
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://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/>
|
||||||
<!-- CSS -->
|
<!-- CSS -->
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.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 href="static/styles.css" rel="stylesheet">
|
<link href="static/styles.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -33,12 +34,14 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" aria-current="page" href="/">Installed Charts</a>
|
<a class="nav-link active" aria-current="page" href="/">Installed Charts</a>
|
||||||
</li>
|
</li>
|
||||||
|
<!-- TODO
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link disabled">Provisional Charts</a>
|
<a class="nav-link disabled">Provisional Charts</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link disabled">Repositories</a>
|
<a class="nav-link disabled">Repositories</a>
|
||||||
</li>
|
</li>
|
||||||
|
-->
|
||||||
</ul>
|
</ul>
|
||||||
<form class="d-flex flex-nowrap text-nowrap">
|
<form class="d-flex flex-nowrap text-nowrap">
|
||||||
<label for="cluster" style="margin-top: 0.5rem">K8s Context:</label>
|
<label for="cluster" style="margin-top: 0.5rem">K8s Context:</label>
|
||||||
@@ -49,56 +52,60 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="bg-light p-5 pt-0 rounded" id="sectionDetails" style="display: none">
|
<div class="bg-light p-5 pt-0 rounded" id="sectionDetails" style="display: none">
|
||||||
<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>
|
<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>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<h1><a href="/" class="text-reset" style="text-decoration: none">⇐</a> Chart Details: <span class="name">chosen-one</span>,
|
<h1><span class="name"></span>,
|
||||||
revision <span class="rev"></span></h1>
|
revision <span class="rev"></span></h1>
|
||||||
|
Chart <b id="chartName"></b>: <i id="revDescr"></i>
|
||||||
|
|
||||||
<nav>
|
<nav class="mt-2">
|
||||||
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||||
<button class="nav-link active" id="nav-manifest-diff-tab" data-bs-toggle="tab" data-bs-target="#nav-manifest-diff"
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="manifests"
|
||||||
type="button" role="tab" aria-controls="nav-manifest-diff" aria-selected="true">Manifests
|
type="button" role="tab" aria-controls="nav-manifest-diff" aria-selected="false">Manifests
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-link" id="nav-disabled-tab" data-bs-toggle="tab" data-bs-target="#nav-disabled"
|
<!-- TODO
|
||||||
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-disabled" data-tab="resources"
|
||||||
type="button" role="tab" aria-controls="nav-disabled" aria-selected="false" disabled>Resources
|
type="button" role="tab" aria-controls="nav-disabled" aria-selected="false" disabled>Resources
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-link" id="nav-disabled-tab" data-bs-toggle="tab" data-bs-target="#nav-disabled"
|
-->
|
||||||
type="button" role="tab" aria-controls="nav-disabled" aria-selected="false" disabled>Parameterized Values
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="values"
|
||||||
|
type="button" role="tab" aria-controls="nav-disabled" aria-selected="false">
|
||||||
|
Parameterized Values
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-link" id="nav-disabled-tab" data-bs-toggle="tab" data-bs-target="#nav-disabled"
|
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="notes"
|
||||||
type="button" role="tab" aria-controls="nav-disabled" aria-selected="false" disabled>Notes
|
type="button" role="tab" aria-controls="nav-disabled" aria-selected="false">Notes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
<div class="tab-pane fade show active" id="nav-manifest-diff" role="tabpanel" aria-labelledby="nav-manifest-diff-tab"
|
<div class="tab-pane" id="nav-manifest" role="tabpanel"
|
||||||
tabindex="0">
|
tabindex="0">
|
||||||
<nav class="navbar bg-light">
|
<nav class="navbar bg-light">
|
||||||
<form class="container-fluid">
|
<form class="container-fluid" id="modePanel">
|
||||||
<label class="form-check-label" for="diffModeNone">
|
<label class="form-check-label" for="diffModeNone">
|
||||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModeNone" disabled>
|
<input class="form-check-input" type="radio" name="diffMode" id="diffModeNone" data-mode="view">
|
||||||
View Manifests
|
View Current
|
||||||
</label>
|
</label>
|
||||||
<label class="form-check-label" for="diffModePrev">
|
<label class="form-check-label" for="diffModePrev">
|
||||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModePrev">
|
<input class="form-check-input" type="radio" name="diffMode" id="diffModePrev" data-mode="diff-prev">
|
||||||
Diff with previous
|
Diff with previous
|
||||||
</label>
|
</label>
|
||||||
<label class="form-check-label" for="diffModeLatest">
|
|
||||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModeLatest" disabled>
|
|
||||||
Diff with latest <span class="text-muted">(#5)</span>
|
|
||||||
</label>
|
|
||||||
<label class="form-check-label" for="diffModeRev">
|
<label class="form-check-label" for="diffModeRev">
|
||||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModeRev" disabled>
|
<input class="form-check-input" type="radio" name="diffMode" id="diffModeRev" data-mode="diff-rev">
|
||||||
Diff with specific revision: <input class="form-input" size="3" disabled>
|
Diff with specific revision: <input class="form-input" size="3" id="specRev">
|
||||||
|
</label>
|
||||||
|
<label class="form-check-label" for="userDefinedVals">
|
||||||
|
<input class="form-check-input" type="checkbox" id="userDefinedVals"> User-defined only
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="manifestText" class="mt-2"></div>
|
<div id="manifestText" class="mt-2 bg-white"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="nav-disabled" role="tabpanel" aria-labelledby="nav-disabled-tab"
|
<div class="tab-pane" id="nav-disabled" role="tabpanel" aria-labelledby="nav-disabled-tab"
|
||||||
tabindex="0">...
|
tabindex="0">...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,12 +121,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<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"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
|
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js" integrity="sha512-gU7kztaQEl7SHJyraPfZLQCNnrKdaQi5ndOyt4L4UPL/FHDd/uB9Je6KDARIqwnNNE27hnqoWLBq+Kpe4iHfeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.1/dist/js.cookie.min.js"
|
||||||
|
integrity="sha256-0H3Nuz3aug3afVbUlsu12Puxva3CP4EhJtPExqs54Vg=" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/luxon@3.0.3/build/global/luxon.min.js" integrity="sha256-RH4TKnKcKyde0s2jc5BW3pXZl/5annY3fcZI9VrV5WQ=" crossorigin="anonymous"></script>
|
||||||
<script src="static/scripts.js"></script>
|
<script src="static/scripts.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,66 +1,173 @@
|
|||||||
const clusterSelect = $("#cluster");
|
const clusterSelect = $("#cluster");
|
||||||
const chartsCards = $("#charts");
|
const chartsCards = $("#charts");
|
||||||
|
const revRow = $("#sectionDetails .row");
|
||||||
|
|
||||||
function reportError(err) {
|
function reportError(err) {
|
||||||
alert(err) // TODO: nice modal/baloon/etc
|
alert(err) // TODO: nice modal/baloon/etc
|
||||||
}
|
}
|
||||||
|
|
||||||
function revisionClicked(namespace, name, self) {
|
function revisionClicked(namespace, name, self) {
|
||||||
|
let active = "active border-primary border-2 bg-opacity-25 bg-primary";
|
||||||
|
let inactive = "border-secondary bg-white";
|
||||||
|
revRow.find(".active").removeClass(active).addClass(inactive)
|
||||||
|
self.removeClass(inactive).addClass(active)
|
||||||
const elm = self.data("elm")
|
const elm = self.data("elm")
|
||||||
const parts = window.location.hash.split("&")
|
setHashParam("revision", elm.revision)
|
||||||
parts[2] = elm.revision
|
|
||||||
window.location.hash = parts.join("&")
|
|
||||||
$("#sectionDetails h1 span.rev").text(elm.revision)
|
$("#sectionDetails h1 span.rev").text(elm.revision)
|
||||||
let qstr = "chart=" + name + "&namespace=" + namespace + "&revision1=" + (elm.revision - 1) + "&revision2=" + elm.revision
|
$("#chartName").text(elm.chart)
|
||||||
let url = "/api/helm/charts/manifest/diff?" + qstr;
|
$("#revDescr").text(elm.description).removeClass("text-danger")
|
||||||
$.getJSON(url).fail(function () {
|
if (elm.status === "failed") {
|
||||||
reportError("Failed to get diff of manifests")
|
$("#revDescr").addClass("text-danger")
|
||||||
}).done(function (data) {
|
}
|
||||||
if (data === "") {
|
|
||||||
$("#manifestText").text("No differences to display")
|
const tab = getHashParam("tab")
|
||||||
|
if (!tab) {
|
||||||
|
$("#nav-tab [data-tab=manifests]").click()
|
||||||
} else {
|
} else {
|
||||||
|
$("#nav-tab [data-tab=" + tab + "]").click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#nav-tab [data-tab]").click(function () {
|
||||||
|
const self = $(this)
|
||||||
|
setHashParam("tab", self.data("tab"))
|
||||||
|
|
||||||
|
if (self.data("tab") === "values") {
|
||||||
|
$("#userDefinedVals").parent().show()
|
||||||
|
} else {
|
||||||
|
$("#userDefinedVals").parent().hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const flag = getHashParam("udv") === "true";
|
||||||
|
$("#userDefinedVals").prop("checked", flag)
|
||||||
|
|
||||||
|
const mode = getHashParam("mode")
|
||||||
|
if (!mode) {
|
||||||
|
$("#modePanel [data-mode=diff-prev]").trigger('click')
|
||||||
|
} else {
|
||||||
|
$("#modePanel [data-mode=" + mode + "]").trigger('click')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$("#modePanel [data-mode]").click(function () {
|
||||||
|
const self = $(this)
|
||||||
|
const mode = self.data("mode")
|
||||||
|
setHashParam("mode", mode)
|
||||||
|
loadContentWrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
$("#userDefinedVals").change(function () {
|
||||||
|
const self = $(this)
|
||||||
|
const flag = $("#userDefinedVals").prop("checked");
|
||||||
|
setHashParam("udv", flag)
|
||||||
|
loadContentWrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
function loadContentWrapper() {
|
||||||
|
let revDiff = 0
|
||||||
|
const revision = parseInt(getHashParam("revision"));
|
||||||
|
if (getHashParam("mode") === "diff-prev") {
|
||||||
|
revDiff = revision - 1
|
||||||
|
} else if (getHashParam("mode") === "diff-rev") {
|
||||||
|
revDiff = $("#specRev").val()
|
||||||
|
}
|
||||||
|
const flag = $("#userDefinedVals").prop("checked");
|
||||||
|
loadContent(getHashParam("tab"), getHashParam("namespace"), getHashParam("chart"), revision, revDiff, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadContent(mode, namespace, name, revision, revDiff, flag) {
|
||||||
|
let qstr = "chart=" + name + "&namespace=" + namespace + "&revision=" + revision
|
||||||
|
if (revDiff) {
|
||||||
|
qstr += "&revisionDiff=" + revDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flag) {
|
||||||
|
qstr += "&flag=" + flag
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = "/api/helm/charts/" + mode
|
||||||
|
url += "?" + qstr
|
||||||
|
const diffDisplay = $("#manifestText");
|
||||||
|
diffDisplay.empty().append("<i class='fa fa-spinner fa-spin fa-2x'></i>")
|
||||||
|
$.get(url).fail(function () {
|
||||||
|
reportError("Failed to get diff of " + mode)
|
||||||
|
}).done(function (data) {
|
||||||
|
diffDisplay.empty();
|
||||||
|
if (data === "") {
|
||||||
|
diffDisplay.text("No differences to display")
|
||||||
|
} else {
|
||||||
|
if (revDiff) {
|
||||||
const targetElement = document.getElementById('manifestText');
|
const targetElement = document.getElementById('manifestText');
|
||||||
const configuration = {
|
const configuration = {
|
||||||
inputFormat: 'diff',
|
inputFormat: 'diff', outputFormat: 'side-by-side',
|
||||||
outputFormat: 'side-by-side',
|
|
||||||
|
|
||||||
drawFileList: false,
|
drawFileList: false, showFiles: false, highlight: true, //matching: 'lines',
|
||||||
showFiles: false,
|
|
||||||
//matching: 'lines',
|
|
||||||
};
|
};
|
||||||
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
||||||
diff2htmlUi.draw()
|
diff2htmlUi.draw()
|
||||||
|
} else {
|
||||||
|
data = hljs.highlight(data, {language: 'yaml'}).value
|
||||||
|
const code = $("#manifestText").empty().append("<pre class='bg-white rounded p-3'></pre>").find("pre");
|
||||||
|
code.html(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillChartDetails(namespace, name) {
|
$('#specRev').keyup(function (event) {
|
||||||
|
let keycode = (event.keyCode ? event.keyCode : event.which);
|
||||||
|
if (keycode == '13') {
|
||||||
|
$("#diffModeRev").click()
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadChartHistory(namespace, name) {
|
||||||
$("#sectionDetails").show()
|
$("#sectionDetails").show()
|
||||||
$("#sectionDetails h1 span.name").text(name)
|
$("#sectionDetails h1 span.name").text(name)
|
||||||
|
revRow.empty().append("<div><i class='fa fa-spinner fa-spin fa-2x'></i></div>")
|
||||||
$.getJSON("/api/helm/charts/history?chart=" + name + "&namespace=" + namespace).fail(function () {
|
$.getJSON("/api/helm/charts/history?chart=" + name + "&namespace=" + namespace).fail(function () {
|
||||||
reportError("Failed to get list of clusters")
|
reportError("Failed to get list of clusters")
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
let revRow = $("#sectionDetails .row");
|
revRow.empty()
|
||||||
for (let x = 0; x < data.length; x++) {
|
for (let x = 0; x < data.length; x++) {
|
||||||
const elm = data[x]
|
const elm = data[x]
|
||||||
const rev = $(`<div class="col-md-2 rounded border border-secondary bg-gradient bg-white">
|
$("#specRev").val(elm.revision)
|
||||||
|
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:</span> <span class="app-ver"></span><br/>
|
<span class="text-muted">App ver:</span> <span class="app-ver"></span><br/>
|
||||||
<span class="text-muted small rev-date"></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>
|
||||||
</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-status").text(elm.status).attr("title", elm.action)
|
rev.find(".rev-age").text(getAge(elm, data[x + 1]))
|
||||||
|
rev.find(".rev-status").text(elm.status)
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elm.status === "deployed") {
|
switch (elm.action) {
|
||||||
//rev.removeClass("bg-white").addClass("text-light bg-primary")
|
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.data("elm", elm)
|
||||||
@@ -72,18 +179,29 @@ function fillChartDetails(namespace, name) {
|
|||||||
revRow.append(rev)
|
revRow.append(rev)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = window.location.hash.substring(1).split("&")
|
const rev = getHashParam("revision")
|
||||||
if (parts.length >= 3) {
|
if (rev) {
|
||||||
revRow.find(".rev-" + parts[2]).click()
|
revRow.find(".rev-" + rev).click()
|
||||||
} else {
|
} else {
|
||||||
revRow.find("div.col-md-2:last-child").click()
|
revRow.find("div.col-md-2:last-child").click()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHashParam(name) {
|
||||||
|
const params = new URLSearchParams(window.location.hash.substring(1))
|
||||||
|
return params.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHashParam(name, val) {
|
||||||
|
const params = new URLSearchParams(window.location.hash.substring(1))
|
||||||
|
params.set(name, val)
|
||||||
|
window.location.hash = new URLSearchParams(params).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadChartsList() {
|
function loadChartsList() {
|
||||||
$("#sectionList").show()
|
$("#sectionList").show()
|
||||||
|
chartsCards.empty().append("<div><i class='fa fa-spinner fa-spin fa-2x'></i> Loading...</div>")
|
||||||
$.getJSON("/api/helm/charts").fail(function () {
|
$.getJSON("/api/helm/charts").fail(function () {
|
||||||
reportError("Failed to get list of clusters")
|
reportError("Failed to get list of clusters")
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
@@ -116,8 +234,9 @@ function loadChartsList() {
|
|||||||
$("#sectionList").hide()
|
$("#sectionList").hide()
|
||||||
|
|
||||||
let chart = self.data("chart");
|
let chart = self.data("chart");
|
||||||
window.location.hash = chart.namespace + "&" + chart.name
|
setHashParam("namespace", chart.namespace)
|
||||||
fillChartDetails(chart.namespace, chart.name)
|
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))
|
||||||
@@ -125,30 +244,64 @@ function loadChartsList() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$(function () {
|
$(function () {
|
||||||
// cluster list
|
// cluster list
|
||||||
|
clusterSelect.change(function () {
|
||||||
|
Cookies.set("context", clusterSelect.val())
|
||||||
|
window.location.href = "/"
|
||||||
|
})
|
||||||
|
|
||||||
$.getJSON("/api/kube/contexts").fail(function () {
|
$.getJSON("/api/kube/contexts").fail(function () {
|
||||||
reportError("Failed to get list of clusters")
|
reportError("Failed to get list of clusters")
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
|
const context = Cookies.get("context")
|
||||||
|
|
||||||
data.forEach(function (elm) {
|
data.forEach(function (elm) {
|
||||||
// aws CLI uses complicated context names, the suffix does not work well
|
// aws CLI uses complicated context names, the suffix does not work well
|
||||||
// maybe we should have an `if` statement here
|
// maybe we should have an `if` statement here
|
||||||
let label = elm.Name //+ " (" + elm.Cluster + "/" + elm.AuthInfo + "/" + elm.Namespace + ")"
|
let label = elm.Name //+ " (" + elm.Cluster + "/" + elm.AuthInfo + "/" + elm.Namespace + ")"
|
||||||
let opt = $("<option></option>").val(elm.Name).text(label)
|
let opt = $("<option></option>").val(elm.Name).text(label)
|
||||||
if (elm.IsCurrent) {
|
if (elm.IsCurrent && !context) {
|
||||||
opt.attr("selected", "selected")
|
opt.attr("selected", "selected")
|
||||||
|
} else if (context && elm.Name === context) {
|
||||||
|
opt.attr("selected", "selected")
|
||||||
|
$.ajaxSetup({
|
||||||
|
headers: {
|
||||||
|
'x-kubecontext': context
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
clusterSelect.append(opt)
|
clusterSelect.append(opt)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
clusterSelect.change(function () {
|
|
||||||
// TODO: remember it, respect it in the function above and in all other places
|
|
||||||
})
|
|
||||||
|
|
||||||
const parts = window.location.hash.substring(1).split("&")
|
const namespace = getHashParam("namespace")
|
||||||
if (parts[0] === "") {
|
const chart = getHashParam("chart")
|
||||||
|
if (!chart) {
|
||||||
loadChartsList()
|
loadChartsList()
|
||||||
} else {
|
} else {
|
||||||
fillChartDetails(parts[0], parts[1])
|
loadChartHistory(namespace, chart)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function getAge(obj1, obj2) {
|
||||||
|
const date = luxon.DateTime.fromISO(obj1.updated);
|
||||||
|
let dateNext = luxon.DateTime.now()
|
||||||
|
if (obj2) {
|
||||||
|
dateNext = luxon.DateTime.fromISO(obj2.updated);
|
||||||
|
}
|
||||||
|
const diff = dateNext.diff(date);
|
||||||
|
|
||||||
|
const map = {
|
||||||
|
"years": "yr", "months": "mo", "days": "d", "hours": "h", "minutes": "m", "seconds": "s", "milliseconds": "ms"
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let unit of ["years", "months", "days", "hours", "minutes", "seconds", "milliseconds"]) {
|
||||||
|
const val = diff.as(unit);
|
||||||
|
if (val >= 1) {
|
||||||
|
return Math.round(val) + map[unit]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "n/a"
|
||||||
|
}
|
||||||
@@ -6,6 +6,6 @@
|
|||||||
color: white!important;
|
color: white!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d2h-file-header {
|
.d2h-file-collapse, .d2h-tag {
|
||||||
display: none;
|
opacity: 0; /* trollface */
|
||||||
}
|
}
|
||||||
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
Reference in New Issue
Block a user