From 1580c2e9a03e2651c56503663e0271fc6e186458 Mon Sep 17 00:00:00 2001 From: Andrey Pokhilko Date: Wed, 31 Aug 2022 12:12:08 +0100 Subject: [PATCH] Chart details draft (#4) * Add logo * Refactor out structs * Data layer context-awareness * Mod * Data layer improvements * Progress * Progress * Progress * Figured the time format shorter * Statuses colors * Sticky URL * Calculate some diffs inside * Separate checks * Scrap gofmt * Skip custom test in GH * Shows some colorful diff --- .github/workflows/build.yml | 15 +- .gitignore | 3 +- README.md | 6 + go.mod | 4 +- go.sum | 7 +- pkg/dashboard/api.go | 121 ++++++++++++---- pkg/dashboard/data.go | 247 ++++++++++++++++++++++++++++++++ pkg/dashboard/data_test.go | 69 +++++++++ pkg/dashboard/helmTypes.go | 36 +++++ pkg/dashboard/main.go | 125 ---------------- pkg/dashboard/server.go | 2 +- pkg/dashboard/static/index.html | 87 +++++++++-- pkg/dashboard/static/logo.png | Bin 0 -> 26093 bytes pkg/dashboard/static/scripts.js | 156 ++++++++++++++++---- pkg/dashboard/static/styles.css | 10 +- pkg/dashboard/utils.go | 14 ++ 16 files changed, 699 insertions(+), 203 deletions(-) create mode 100644 pkg/dashboard/data.go create mode 100644 pkg/dashboard/data_test.go create mode 100644 pkg/dashboard/helmTypes.go delete mode 100644 pkg/dashboard/main.go create mode 100644 pkg/dashboard/static/logo.png diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4021e6..b32ce4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,17 +16,16 @@ jobs: uses: actions/setup-go@v3 with: go-version: 1.18 - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - - name: Series of tests + - name: Unit tests run: | - GO_FILES=$(find . -iname '*.go' -type f) # All the .go files, excluding vendor/ - test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt go test -v -race ./... # Run all the tests with the race detector enabled + - name: Static analysis + run: | go vet ./... # go vet is the official Go static analyzer + - name: Cyclomatic complexity + run: | go install github.com/fzipp/gocyclo/cmd/gocyclo@latest - /home/runner/go/bin/gocyclo -over 19 cmd pkg # forbid code with huge/complex functions - go build main.go + /home/runner/go/bin/gocyclo -over 19 main.go pkg # forbid code with huge/complex functions - name: Dry Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: @@ -34,3 +33,5 @@ jobs: args: release --snapshot --rm-dist - name: Test Binary is Runnable run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help" + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 diff --git a/.gitignore b/.gitignore index 3720f56..d02d94c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ # Go workspace file go.work -/bin \ No newline at end of file +/bin +/.idea/ diff --git a/README.md b/README.md index 2ed7f12..a94240a 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ To uninstall, run: helm plugin uninstall dashboard ``` +## 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. ## Roadmap @@ -59,3 +62,6 @@ Validate manifests before deploy and get better errors Switch clusters (?) Browsing repositories Adding new repository + +Recognise & show ArgoCD-originating charts/objects +Have cleaner idea on the web API structure \ No newline at end of file diff --git a/go.mod b/go.mod index daeb84d..7a70340 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,12 @@ module github.com/komodorio/helm-dashboard go 1.18 require ( + github.com/Masterminds/semver/v3 v3.1.1 github.com/gin-gonic/gin v1.8.1 + github.com/hexops/gotextdiff v1.0.3 github.com/sirupsen/logrus v1.8.1 github.com/toqueteos/webbrowser v1.2.0 + helm.sh/helm/v3 v3.9.4 ) require ( @@ -14,7 +17,6 @@ require ( github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/validator/v10 v10.11.0 // indirect github.com/goccy/go-json v0.9.11 // indirect - github.com/google/go-cmp v0.5.6 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-isatty v0.0.16 // indirect diff --git a/go.sum b/go.sum index d8ac2b7..af53406 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -19,8 +21,9 @@ github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -100,3 +103,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +helm.sh/helm/v3 v3.9.4 h1:TCI1QhJUeLVOdccfdw+vnSEO3Td6gNqibptB04QtExY= +helm.sh/helm/v3 v3.9.4/go.mod h1:3eaWAIqzvlRSD06gR9MMwmp2KBKwlu9av1/1BZpjeWY= diff --git a/pkg/dashboard/api.go b/pkg/dashboard/api.go index db3b285..5b8d94c 100644 --- a/pkg/dashboard/api.go +++ b/pkg/dashboard/api.go @@ -2,17 +2,33 @@ package dashboard import ( "embed" + "errors" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "net/http" "os" "path" + "strconv" ) //go:embed static/* var staticFS embed.FS -func newRouter(abortWeb ControlChan, data DataLayer) *gin.Engine { +func errorHandler(c *gin.Context) { + c.Next() + + errs := "" + for _, err := range c.Errors { + log.Debugf("Error: %s", err) + errs += err.Error() + "\n" + } + + if errs != "" { + c.String(http.StatusInternalServerError, errs) + } +} + +func NewRouter(abortWeb ControlChan, data DataLayer) *gin.Engine { var api *gin.Engine if os.Getenv("DEBUG") == "" { api = gin.New() @@ -21,6 +37,84 @@ func newRouter(abortWeb ControlChan, data DataLayer) *gin.Engine { api = gin.Default() } + api.Use(errorHandler) + configureStatic(api) + + configureRoutes(abortWeb, data, api) + return api +} + +func configureRoutes(abortWeb ControlChan, data DataLayer, api *gin.Engine) { + // server shutdown handler + api.DELETE("/", func(c *gin.Context) { + abortWeb <- struct{}{} + }) + + api.GET("/api/helm/charts", func(c *gin.Context) { + res, err := data.ListInstalled() + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + c.IndentedJSON(http.StatusOK, res) + }) + + api.GET("/api/kube/contexts", func(c *gin.Context) { + res, err := data.ListContexts() + 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/manifest/diff", 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 + } + + cRev1, err := strconv.Atoi(c.Query("revision1")) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + + cRev2, err := strconv.Atoi(c.Query("revision2")) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + + res, err := data.RevisionManifestsDiff(cNamespace, cName, cRev1, cRev2) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + c.IndentedJSON(http.StatusOK, res) + }) + +} + +func configureStatic(api *gin.Engine) { fs := http.FS(staticFS) // local dev speed-up @@ -48,29 +142,4 @@ func newRouter(abortWeb ControlChan, data DataLayer) *gin.Engine { c.FileFromFS(c.Request.URL.Path, fs) }) } - - // server shutdown handler - api.DELETE("/", func(c *gin.Context) { - abortWeb <- struct{}{} - }) - - api.GET("/api/helm/charts", func(c *gin.Context) { - res, err := data.ListInstalled() - if err != nil { - _ = c.AbortWithError(http.StatusInternalServerError, err) - return - } - c.IndentedJSON(http.StatusOK, res) - }) - - api.GET("/api/kube/contexts", func(c *gin.Context) { - res, err := data.ListContexts() - if err != nil { - _ = c.AbortWithError(http.StatusInternalServerError, err) - return - } - c.IndentedJSON(http.StatusOK, res) - }) - - return api } diff --git a/pkg/dashboard/data.go b/pkg/dashboard/data.go new file mode 100644 index 0000000..73a0b4c --- /dev/null +++ b/pkg/dashboard/data.go @@ -0,0 +1,247 @@ +package dashboard + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/Masterminds/semver/v3" + "github.com/hexops/gotextdiff" + "github.com/hexops/gotextdiff/myers" + "github.com/hexops/gotextdiff/span" + log "github.com/sirupsen/logrus" + "os" + "os/exec" + "regexp" + "strconv" + "strings" + "time" +) + +type DataLayer struct { + KubeContext string + Helm string + Kubectl string +} + +func (l *DataLayer) runCommand(cmd ...string) (string, error) { + // TODO: --kube-context=context-name to juggle clusters + log.Debugf("Starting command: %s", cmd) + prog := exec.Command(cmd[0], cmd[1:]...) + prog.Env = os.Environ() + prog.Env = append(prog.Env, "HELM_KUBECONTEXT="+l.KubeContext) + + var stdout bytes.Buffer + prog.Stdout = &stdout + + var stderr bytes.Buffer + prog.Stderr = &stderr + + if err := prog.Run(); err != nil { + log.Warnf("Failed command: %s", cmd) + serr := stderr.Bytes() + if serr != nil { + log.Warnf("STDERR:\n%s", serr) + } + if eerr, ok := err.(*exec.ExitError); ok { + return "", fmt.Errorf("failed to run command %s: %s", cmd, eerr) + } + return "", err + } + + sout := stdout.Bytes() + serr := stderr.Bytes() + log.Debugf("Command STDOUT:\n%s", sout) + log.Debugf("Command STDERR:\n%s", serr) + return string(sout), nil +} + +func (l *DataLayer) runCommandHelm(cmd ...string) (string, error) { + if l.Helm == "" { + l.Helm = "helm" + } + + cmd = append([]string{l.Helm}, cmd...) + if l.KubeContext != "" { + cmd = append(cmd, "--kube-context", l.KubeContext) + } + + return l.runCommand(cmd...) +} + +func (l *DataLayer) runCommandKubectl(cmd ...string) (string, error) { + if l.Kubectl == "" { + l.Kubectl = "kubectl" + } + + cmd = append([]string{l.Kubectl}, cmd...) + + if l.KubeContext != "" { + cmd = append(cmd, "--context", l.KubeContext) + } + + return l.runCommand(cmd...) +} + +func (l *DataLayer) CheckConnectivity() error { + contexts, err := l.ListContexts() + if err != nil { + return err + } + + if len(contexts) < 1 { + return errors.New("did not find any kubectl contexts configured") + } + + _, err = l.runCommandHelm("env") + if err != nil { + return err + } + + return nil +} + +type KubeContext struct { + IsCurrent bool + Name string + Cluster string + AuthInfo string + Namespace string +} + +func (l *DataLayer) ListContexts() (res []KubeContext, err error) { + out, err := l.runCommandKubectl("config", "get-contexts") + if err != nil { + return nil, err + } + + // kubectl has no JSON output for it, we'll have to do custom text parsing + lines := strings.Split(out, "\n") + + // find field positions + fields := regexp.MustCompile(`(\w+\s+)`).FindAllString(lines[0], -1) + cur := len(fields[0]) + name := cur + len(fields[1]) + cluster := name + len(fields[2]) + auth := cluster + len(fields[3]) + + // read items + for _, line := range lines[1:] { + if strings.TrimSpace(line) == "" { + continue + } + + res = append(res, KubeContext{ + IsCurrent: strings.TrimSpace(line[0:cur]) == "*", + Name: strings.TrimSpace(line[cur:name]), + Cluster: strings.TrimSpace(line[name:cluster]), + AuthInfo: strings.TrimSpace(line[cluster:auth]), + Namespace: strings.TrimSpace(line[auth:]), + }) + } + + return res, nil +} + +func (l *DataLayer) ListInstalled() (res []releaseElement, err error) { + out, err := l.runCommandHelm("ls", "--all", "--all-namespaces", "--output", "json", "--time-format", time.RFC3339) + if err != nil { + return nil, err + } + + err = json.Unmarshal([]byte(out), &res) + if err != nil { + return nil, err + } + return res, nil +} + +func (l *DataLayer) ChartHistory(namespace string, chartName string) (res []*historyElement, err error) { + // TODO: there is `max` but there is no `offset` + out, err := l.runCommandHelm("history", chartName, "--namespace", namespace, "--max", "5", "--output", "json") + if err != nil { + return nil, err + } + + err = json.Unmarshal([]byte(out), &res) + if err != nil { + return nil, err + } + + var aprev *semver.Version + var cprev *semver.Version + for _, elm := range res { + chartRepoName, curVer, err := chartAndVersion(elm.Chart) + if err != nil { + return nil, err + } + elm.ChartName = chartRepoName + elm.ChartVer = curVer + elm.Action = "" + elm.Updated.Time = elm.Updated.Time.Round(time.Second) + + cver, err1 := semver.NewVersion(elm.ChartVer) + aver, err2 := semver.NewVersion(elm.AppVersion) + if err1 == nil && err2 == nil { + if aprev != nil && cprev != nil { + switch { + case aprev.LessThan(aver): + elm.Action = "app_upgrade" + case aprev.GreaterThan(aver): + elm.Action = "app_downgrade" + case cprev.LessThan(cver): + elm.Action = "chart_upgrade" + case cprev.GreaterThan(cver): + elm.Action = "chart_downgrade" + default: + elm.Action = "reconfigure" + } + } + } else { + log.Debugf("Semver parsing errors: %s=%s, %s=%s", elm.ChartVer, err1, elm.AppVersion, err2) + } + + aprev = aver + cprev = cver + } + + return res, nil +} + +func (l *DataLayer) ChartRepoVersions(chartName string) (res []repoChartElement, err error) { + out, err := l.runCommandHelm("search", "repo", "--regexp", "/"+chartName+"\v", "--versions", "--output", "json") + if err != nil { + return nil, err + } + + err = json.Unmarshal([]byte(out), &res) + if err != nil { + return nil, err + } + return res, nil +} + +func (l *DataLayer) RevisionManifests(namespace string, chartName string, revision int) (res string, err error) { + out, err := l.runCommandHelm("get", "manifest", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision)) + if err != nil { + return "", err + } + return out, nil +} + +func (l *DataLayer) RevisionManifestsDiff(namespace string, name string, revision1 int, revision2 int) (string, error) { + manifest1, err := l.RevisionManifests(namespace, name, revision1) + if err != nil { + return "", nil + } + + manifest2, err := l.RevisionManifests(namespace, name, revision2) + if err != nil { + return "", nil + } + + edits := myers.ComputeEdits(span.URIFromPath(""), manifest1, manifest2) + unified := gotextdiff.ToUnified("a.txt", "b.txt", manifest1, edits) + diff := fmt.Sprint(unified) + return diff, nil +} diff --git a/pkg/dashboard/data_test.go b/pkg/dashboard/data_test.go new file mode 100644 index 0000000..32f94f4 --- /dev/null +++ b/pkg/dashboard/data_test.go @@ -0,0 +1,69 @@ +package dashboard + +import ( + log "github.com/sirupsen/logrus" + "helm.sh/helm/v3/pkg/release" + "testing" +) + +func TestFlow(t *testing.T) { + log.SetLevel(log.DebugLevel) + + var _ release.Status + data := DataLayer{} + err := data.CheckConnectivity() + if err != nil { + if err.Error() == "did not find any kubectl contexts configured" { + t.Skip() + } else { + t.Fatal(err) + } + } + + ctxses, err := data.ListContexts() + if err != nil { + t.Fatal(err) + } + + for _, ctx := range ctxses { + if ctx.IsCurrent { + data.KubeContext = ctx.Name + } + } + + installed, err := data.ListInstalled() + if err != nil { + t.Fatal(err) + } + + chart := installed[1] + history, err := data.ChartHistory(chart.Namespace, chart.Name) + if err != nil { + t.Fatal(err) + } + _ = history + + chartRepoName, curVer, err := chartAndVersion(chart.Chart) + if err != nil { + t.Fatal(err) + } + _ = curVer + + upgrade, err := data.ChartRepoVersions(chartRepoName) + if err != nil { + t.Fatal(err) + } + _ = upgrade + + manifests, err := data.RevisionManifests(chart.Namespace, chart.Name, history[len(history)-1].Revision) + if err != nil { + t.Fatal(err) + } + _ = manifests + + diff, err := data.RevisionManifestsDiff(chart.Namespace, chart.Name, history[len(history)-1].Revision, history[len(history)-2].Revision) + if err != nil { + t.Fatal(err) + } + _ = diff +} diff --git a/pkg/dashboard/helmTypes.go b/pkg/dashboard/helmTypes.go new file mode 100644 index 0000000..031ef9d --- /dev/null +++ b/pkg/dashboard/helmTypes.go @@ -0,0 +1,36 @@ +package dashboard + +import ( + "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" +) + +// unpleasant copy from Helm sources, where they have it non-public +type releaseElement struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Revision string `json:"revision"` + Updated helmtime.Time `json:"updated"` + Status release.Status `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` +} + +type historyElement struct { + Revision int `json:"revision"` + Updated helmtime.Time `json:"updated"` + Status release.Status `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` + Description string `json:"description"` + ChartName string `json:"chart_name"` + ChartVer string `json:"chart_ver"` + Action string `json:"action"` +} + +type repoChartElement struct { + Name string `json:"name"` + Version string `json:"version"` + AppVersion string `json:"app_version"` + Description string `json:"description"` +} diff --git a/pkg/dashboard/main.go b/pkg/dashboard/main.go deleted file mode 100644 index fbf8133..0000000 --- a/pkg/dashboard/main.go +++ /dev/null @@ -1,125 +0,0 @@ -package dashboard - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - log "github.com/sirupsen/logrus" - "os/exec" - "regexp" - "strings" -) - -type DataLayer struct { -} - -func (l *DataLayer) runCommand(cmd ...string) (string, error) { - // TODO: --kube-context=context-name to juggle clusters - log.Debugf("Starting command: %s", cmd) - prog := exec.Command(cmd[0], cmd[1:]...) - - var stdout bytes.Buffer - prog.Stdout = &stdout - - var stderr bytes.Buffer - prog.Stderr = &stderr - - //prog.Stdout, prog.Stderr = os.Stdout, os.Stderr - if err := prog.Run(); err != nil { - if eerr, ok := err.(*exec.ExitError); ok { - return "", fmt.Errorf("failed to run command %s: %s", cmd, eerr) - } - return "", err - } - - sout := stdout.Bytes() - serr := stderr.Bytes() - log.Debugf("Command STDOUT:\n%s", sout) - log.Debugf("Command STDERR:\n%s", serr) - return string(sout), nil -} - -func (l *DataLayer) CheckConnectivity() error { - contexts, err := l.ListContexts() - if err != nil { - return err - } - - if len(contexts) < 1 { - return errors.New("did not find any kubectl contexts configured") - } - - _, err = l.runCommand("helm", "env") - if err != nil { - return err - } - - return nil -} - -type KubeContext struct { - IsCurrent bool - Name string - Cluster string - AuthInfo string - Namespace string -} - -func (l *DataLayer) ListContexts() (res []KubeContext, err error) { - out, err := l.runCommand("kubectl", "config", "get-contexts") - if err != nil { - return nil, err - } - - // kubectl has no JSON output for it, we'll have to do custom text parsing - lines := strings.Split(out, "\n") - - // find field positions - fields := regexp.MustCompile(`(\w+\s+)`).FindAllString(lines[0], -1) - cur := len(fields[0]) - name := cur + len(fields[1]) - cluster := name + len(fields[2]) - auth := cluster + len(fields[3]) - - // read items - for _, line := range lines[1:] { - if strings.TrimSpace(line) == "" { - continue - } - - res = append(res, KubeContext{ - IsCurrent: strings.TrimSpace(line[0:cur]) == "*", - Name: strings.TrimSpace(line[cur:name]), - Cluster: strings.TrimSpace(line[name:cluster]), - AuthInfo: strings.TrimSpace(line[cluster:auth]), - Namespace: strings.TrimSpace(line[auth:]), - }) - } - - return res, nil -} - -// unpleasant copy from Helm sources, where they have it non-public -type releaseElement struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Revision string `json:"revision"` - Updated string `json:"updated"` - Status string `json:"status"` - Chart string `json:"chart"` - AppVersion string `json:"app_version"` -} - -func (l *DataLayer) ListInstalled() (res []releaseElement, err error) { - out, err := l.runCommand("helm", "ls", "--all", "--all-namespaces", "--output", "json") - if err != nil { - return nil, err - } - - err = json.Unmarshal([]byte(out), &res) - if err != nil { - return nil, err - } - return res, nil -} diff --git a/pkg/dashboard/server.go b/pkg/dashboard/server.go index ac4b33d..f91900d 100644 --- a/pkg/dashboard/server.go +++ b/pkg/dashboard/server.go @@ -28,7 +28,7 @@ func StartServer() (string, ControlChan) { } abort := make(ControlChan) - api := newRouter(abort, data) + api := NewRouter(abort, data) done := startBackgroundServer(address, api, abort) return "http://" + address, done diff --git a/pkg/dashboard/static/index.html b/pkg/dashboard/static/index.html index c4c3cb6..6cd31f8 100644 --- a/pkg/dashboard/static/index.html +++ b/pkg/dashboard/static/index.html @@ -1,58 +1,125 @@ - + Helm Dashboard + + +
-