Skip to content

Commit d9a88fe

Browse files
authored
Initial features pt 2 (#3)
* Less logging when not in DEBUG * Check helm is fine * Display kube context switch * Cosmetics * Displays list of chartss * Linter stuff * Fix option name
1 parent 925cfa7 commit d9a88fe

File tree

8 files changed

+249
-19
lines changed

8 files changed

+249
-19
lines changed

go.mod

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ require (
66
github.com/gin-gonic/gin v1.8.1
77
github.com/sirupsen/logrus v1.8.1
88
github.com/toqueteos/webbrowser v1.2.0
9-
helm.sh/helm/v3 v3.9.3
109
)
1110

1211
require (
13-
github.com/Masterminds/semver/v3 v3.1.1 // indirect
1412
github.com/gin-contrib/sse v0.1.0 // indirect
1513
github.com/go-playground/locales v0.14.0 // indirect
1614
github.com/go-playground/universal-translator v0.18.0 // indirect
1715
github.com/go-playground/validator/v10 v10.11.0 // indirect
1816
github.com/goccy/go-json v0.9.11 // indirect
17+
github.com/google/go-cmp v0.5.6 // indirect
1918
github.com/json-iterator/go v1.1.12 // indirect
2019
github.com/leodido/go-urn v1.2.1 // indirect
2120
github.com/mattn/go-isatty v0.0.16 // indirect

go.sum

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
2-
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
31
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
42
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
53
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -21,6 +19,7 @@ github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
2119
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
2220
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
2321
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
22+
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
2423
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
2524
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
2625
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -101,5 +100,3 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
101100
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
102101
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
103102
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
104-
helm.sh/helm/v3 v3.9.3 h1:etd4Qc45/bnIkBofZIRwrAzYuG3bNWR1EdMN4fsfzoE=
105-
helm.sh/helm/v3 v3.9.3/go.mod h1:3eaWAIqzvlRSD06gR9MMwmp2KBKwlu9av1/1BZpjeWY=

pkg/dashboard/api.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ import (
1313
var staticFS embed.FS
1414

1515
func newRouter(abortWeb ControlChan, data DataLayer) *gin.Engine {
16-
api := gin.Default()
16+
var api *gin.Engine
17+
if os.Getenv("DEBUG") == "" {
18+
api = gin.New()
19+
api.Use(gin.Recovery())
20+
} else {
21+
api = gin.Default()
22+
}
23+
1724
fs := http.FS(staticFS)
1825

1926
// local dev speed-up
@@ -47,8 +54,22 @@ func newRouter(abortWeb ControlChan, data DataLayer) *gin.Engine {
4754
abortWeb <- struct{}{}
4855
})
4956

50-
api.GET("/api", func(c *gin.Context) {
51-
c.IndentedJSON(http.StatusOK, data.ListInstalled())
57+
api.GET("/api/helm/charts", func(c *gin.Context) {
58+
res, err := data.ListInstalled()
59+
if err != nil {
60+
_ = c.AbortWithError(http.StatusInternalServerError, err)
61+
return
62+
}
63+
c.IndentedJSON(http.StatusOK, res)
64+
})
65+
66+
api.GET("/api/kube/contexts", func(c *gin.Context) {
67+
res, err := data.ListContexts()
68+
if err != nil {
69+
_ = c.AbortWithError(http.StatusInternalServerError, err)
70+
return
71+
}
72+
c.IndentedJSON(http.StatusOK, res)
5273
})
5374

5475
return api

pkg/dashboard/main.go

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,125 @@
11
package dashboard
22

33
import (
4-
"helm.sh/helm/v3/pkg/release"
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
log "github.com/sirupsen/logrus"
9+
"os/exec"
10+
"regexp"
11+
"strings"
512
)
613

714
type DataLayer struct {
815
}
916

10-
func (l *DataLayer) CheckConnectivity() {
11-
// TODO: check that we can work with context and subcommands
17+
func (l *DataLayer) runCommand(cmd ...string) (string, error) {
18+
// TODO: --kube-context=context-name to juggle clusters
19+
log.Debugf("Starting command: %s", cmd)
20+
prog := exec.Command(cmd[0], cmd[1:]...)
21+
22+
var stdout bytes.Buffer
23+
prog.Stdout = &stdout
24+
25+
var stderr bytes.Buffer
26+
prog.Stderr = &stderr
27+
28+
//prog.Stdout, prog.Stderr = os.Stdout, os.Stderr
29+
if err := prog.Run(); err != nil {
30+
if eerr, ok := err.(*exec.ExitError); ok {
31+
return "", fmt.Errorf("failed to run command %s: %s", cmd, eerr)
32+
}
33+
return "", err
34+
}
35+
36+
sout := stdout.Bytes()
37+
serr := stderr.Bytes()
38+
log.Debugf("Command STDOUT:\n%s", sout)
39+
log.Debugf("Command STDERR:\n%s", serr)
40+
return string(sout), nil
41+
}
42+
43+
func (l *DataLayer) CheckConnectivity() error {
44+
contexts, err := l.ListContexts()
45+
if err != nil {
46+
return err
47+
}
48+
49+
if len(contexts) < 1 {
50+
return errors.New("did not find any kubectl contexts configured")
51+
}
52+
53+
_, err = l.runCommand("helm", "env")
54+
if err != nil {
55+
return err
56+
}
57+
58+
return nil
59+
}
60+
61+
type KubeContext struct {
62+
IsCurrent bool
63+
Name string
64+
Cluster string
65+
AuthInfo string
66+
Namespace string
1267
}
1368

14-
func (l *DataLayer) ListInstalled() []*release.Release {
15-
return nil // TODO
69+
func (l *DataLayer) ListContexts() (res []KubeContext, err error) {
70+
out, err := l.runCommand("kubectl", "config", "get-contexts")
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
// kubectl has no JSON output for it, we'll have to do custom text parsing
76+
lines := strings.Split(out, "\n")
77+
78+
// find field positions
79+
fields := regexp.MustCompile(`(\w+\s+)`).FindAllString(lines[0], -1)
80+
cur := len(fields[0])
81+
name := cur + len(fields[1])
82+
cluster := name + len(fields[2])
83+
auth := cluster + len(fields[3])
84+
85+
// read items
86+
for _, line := range lines[1:] {
87+
if strings.TrimSpace(line) == "" {
88+
continue
89+
}
90+
91+
res = append(res, KubeContext{
92+
IsCurrent: strings.TrimSpace(line[0:cur]) == "*",
93+
Name: strings.TrimSpace(line[cur:name]),
94+
Cluster: strings.TrimSpace(line[name:cluster]),
95+
AuthInfo: strings.TrimSpace(line[cluster:auth]),
96+
Namespace: strings.TrimSpace(line[auth:]),
97+
})
98+
}
99+
100+
return res, nil
101+
}
102+
103+
// unpleasant copy from Helm sources, where they have it non-public
104+
type releaseElement struct {
105+
Name string `json:"name"`
106+
Namespace string `json:"namespace"`
107+
Revision string `json:"revision"`
108+
Updated string `json:"updated"`
109+
Status string `json:"status"`
110+
Chart string `json:"chart"`
111+
AppVersion string `json:"app_version"`
112+
}
113+
114+
func (l *DataLayer) ListInstalled() (res []releaseElement, err error) {
115+
out, err := l.runCommand("helm", "ls", "--all", "--all-namespaces", "--output", "json")
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
err = json.Unmarshal([]byte(out), &res)
121+
if err != nil {
122+
return nil, err
123+
}
124+
return res, nil
16125
}

pkg/dashboard/server.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@ import (
66
log "github.com/sirupsen/logrus"
77
"net/http"
88
"os"
9-
"strings"
109
)
1110

1211
func StartServer() (string, ControlChan) {
1312
data := DataLayer{}
14-
data.CheckConnectivity()
13+
err := data.CheckConnectivity()
14+
if err != nil {
15+
log.Errorf("Failed to check that Helm is operational, cannot continue. The error was: %s", err)
16+
os.Exit(1) // TODO: propagate error instead?
17+
}
1518

1619
address := os.Getenv("HD_BIND")
20+
if address == "" {
21+
address = "localhost"
22+
}
23+
1724
if os.Getenv("HD_PORT") == "" {
1825
address += ":8080" // TODO: better default port to clash less?
1926
} else {
@@ -24,9 +31,6 @@ func StartServer() (string, ControlChan) {
2431
api := newRouter(abort, data)
2532
done := startBackgroundServer(address, api, abort)
2633

27-
if strings.HasPrefix(address, ":") {
28-
address = "localhost" + address
29-
}
3034
return "http://" + address, done
3135
}
3236

pkg/dashboard/static/index.html

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<!doctype html>
22
<html lang="en">
33
<head>
4+
<link rel="icon" href="https://komodor.com/wp-content/uploads/2021/05/favicon-50x50.png"/>
45
<meta charset="utf-8">
56
<meta name="viewport" content="width=device-width, initial-scale=1">
67
<title>Helm Dashboard</title>
@@ -10,10 +11,48 @@
1011
</head>
1112
<body>
1213

14+
<div class="container">
15+
<nav class="navbar navbar-expand-lg bg-light rounded" aria-label="Eleventh navbar example">
16+
<div class="container-fluid">
17+
<div style="line-height: 90%">
18+
<a class="navbar-brand" href="#"><b>Helm Dashboard</b></a><br/>
19+
<span style="font-size: smaller">by <a href="https://komodor.io">komodor.io</a></span>
20+
</div>
21+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarsExample09"
22+
aria-controls="navbarsExample09" aria-expanded="false" aria-label="Toggle navigation">
23+
<span class="navbar-toggler-icon"></span>
24+
</button>
25+
26+
<div class="collapse navbar-collapse" id="navbarsExample09">
27+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
28+
<li class="nav-item">
29+
<a class="nav-link active" aria-current="page" href="/">Charts List</a>
30+
</li>
31+
<li class="nav-item">
32+
<a class="nav-link disabled">Repositories</a>
33+
</li>
34+
</ul>
35+
<form class="d-flex flex-nowrap text-nowrap">
36+
<label for="cluster" class="">K8s Context:</label>
37+
<select id="cluster" class="form-control"></select>
38+
</form>
39+
</div>
40+
</div>
41+
</nav>
42+
43+
44+
<div class="bg-light p-5 rounded">
45+
<h1>Charts List</h1>
46+
<div id="charts" class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
47+
48+
</div>
49+
</div>
50+
</div>
1351

1452
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
1553
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
1654
crossorigin="anonymous"></script>
55+
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
1756
<script src="static/scripts.js"></script>
1857
</body>
1958
</html>

pkg/dashboard/static/scripts.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const clusterSelect = $("#cluster");
2+
const chartsCards = $("#charts");
3+
4+
function reportError(err) {
5+
alert(err) // TODO: nice modal/baloon/etc
6+
}
7+
8+
$(function () {
9+
// cluster list
10+
$.getJSON("/api/kube/contexts").fail(function () {
11+
reportError("Failed to get list of clusters")
12+
}).done(function (data) {
13+
data.forEach(function (elm) {
14+
// aws CLI uses complicated context names, the suffix does not work well
15+
// maybe we should have an `if` statement here
16+
let label = elm.Name //+ " (" + elm.Cluster + "/" + elm.AuthInfo + "/" + elm.Namespace + ")"
17+
let opt = $("<option></option>").val(elm.Name).text(label)
18+
if (elm.IsCurrent) {
19+
opt.attr("selected", "selected")
20+
}
21+
clusterSelect.append(opt)
22+
})
23+
})
24+
clusterSelect.change(function () {
25+
// TODO: remember it, respect it in the function above and in all other places
26+
})
27+
28+
// charts list
29+
$.getJSON("/api/helm/charts").fail(function () {
30+
reportError("Failed to get list of clusters")
31+
}).done(function (data) {
32+
chartsCards.empty()
33+
data.forEach(function (elm) {
34+
const header = $("<div class='card-header'></div>")
35+
header.append($('<div class="float-end"><h5 class="float-end text-muted text-end">#' + elm.revision + '</h5><br/><div class="badge bg-info">' + elm.status + "</div>"))
36+
header.append($('<h5 class="card-title"></h5>').text(elm.name))
37+
header.append($('<p class="card-text small text-muted"></p>').append("Version: " + elm.app_version))
38+
39+
const body = $("<div class='card-body'></div>")
40+
body.append($('<p class="card-text"></p>').append("Namespace: " + elm.namespace))
41+
body.append($('<p class="card-text"></p>').append("Chart: " + elm.chart))
42+
body.append($('<p class="card-text"></p>').append("Updated: " + elm.updated))
43+
44+
/*
45+
"namespace": "default",
46+
"revision": "4",
47+
"updated": "2022-08-16 17:11:26.73393511 +0300 IDT",
48+
"status": "deployed",
49+
"chart": "k8s-watcher-0.17.1",
50+
"app_version": "0.1.108"
51+
52+
*/
53+
54+
let card = $("<div class='card'></div>").append(header).append(body);
55+
chartsCards.append($("<div class='col'></div>").append(card))
56+
})
57+
})
58+
})

pkg/dashboard/static/styles.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#charts .card {
2+
cursor: pointer;
3+
}

0 commit comments

Comments
 (0)