15 Commits

Author SHA1 Message Date
ElisarEisenbach
76e4fe51b5 Refactoring analytics (#311)
* exampple

* send call type in function

* remove heap check

---------

Co-authored-by: itielshwartz <itielshw@gmail.com>
2023-03-30 14:42:35 +03:00
ElisarEisenbach
95ea5e4d6d Send analytics to server (#310)
* start adding functions

* refactoring analytics functions

* formatting analytics
2023-03-30 09:29:01 +01:00
Andrei Pohilko
c139f3941d Merge branch 'main' of github.com:komodorio/helm-dashboard 2023-03-24 17:39:49 +00:00
Andrei Pohilko
80022c3ef8 Don't fail on single resource status error
Relates to #301
2023-03-24 17:39:29 +00:00
Viet Nguyen
a07cfcdbb4 Fix typo on readme doc (#265) 2023-03-23 10:00:06 +00:00
Andrei Pohilko
8826124f70 Merge branch 'main' of github.com:komodorio/helm-dashboard 2023-03-23 09:55:29 +00:00
Andrei Pohilko
703b4029de Cosmetic cleanup 2023-03-23 09:55:11 +00:00
Harshit Mehta
a2dc1ed96b update README (#270) 2023-03-17 14:06:14 +00:00
Harshit Mehta
29c1682bbb Removing redundant table header (#268) 2023-03-16 12:31:43 +00:00
Andrei Pohilko
c7d18a7fb7 Count API docs in user analytics 2023-03-13 17:11:20 +00:00
Andrei Pohilko
e9ee10287b Improve console message in case no k8s connection possible 2023-03-13 15:26:32 +00:00
Andrey Pokhilko
57d4d073e9 Display resource health aggregate icons on list of releases (#235)
* Display aggregate resource health status

* Reuse old API request, show icons

* Take progress indication from deployment conditions

* Improve status

* Cleanup

* Fixups

* Squares approach
2023-03-13 12:56:31 +00:00
Harshit Mehta
47dae4d35a Add username and password support to Repo add feature (#228)
* Add username and password support to Repo add in UI

* Add support for Username and Passowrd in Add Repo API
2023-03-09 13:34:05 +00:00
Harshit Mehta
0ac8eec368 Minor UI fixes (#234) 2023-03-09 08:45:41 +00:00
komodor-bot
aec46d43f7 Increment chart versions [skip ci] 2023-03-08 09:27:49 +00:00
20 changed files with 333 additions and 100 deletions

View File

@@ -117,7 +117,7 @@ For all the release(s) (installed helm charts), you can execute helm tests for t
You can execute `helm test` for the specific release as below:
![](images/screenshot_run_test.png)
The result of executed `helm test` for the release will be disapled as below:
The result of executed `helm test` for the release will be displayed as below:
![](images/screenshot_run_test_result.png)
### Scanner Integrations
@@ -151,15 +151,25 @@ Kindly read our [Contributing Guide](CONTRIBUTING.md) to learn and understand ab
## Local Dev Testing
Prerequisites: `helm` and `kubectl` binaries installed and operational.
Prerequisites, binaries installed and operational:
- [Go](https://go.dev/doc/install)
There is a need to build binary for plugin to function, run:
### Linux
```shell
go build -o bin/dashboard .
```
You can just run the `bin/dashboard` binary directly, it will just work.
### Windows
```bat
go build -o bin\dashboard.exe .
```
You can just run the `dashboard` or `dashboard.exe` binary directly, it will just work.
To install, checkout the source code and run from source dir:

View File

@@ -5,5 +5,5 @@ name: helm-dashboard
description: A GUI Dashboard for Helm by Komodor
icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/main/pkg/dashboard/static/logo.svg"
version: 0.1.5
appVersion: "1.1.1"
version: 0.1.6
appVersion: "1.2.0"

4
go.mod
View File

@@ -61,7 +61,7 @@ require (
github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-gorp/gorp/v3 v3.0.2 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
@@ -96,7 +96,7 @@ require (
github.com/lib/pq v1.10.7 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect

15
go.sum
View File

@@ -195,8 +195,9 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4=
github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -424,8 +425,8 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -456,8 +457,8 @@ github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@@ -468,8 +469,8 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@@ -555,8 +556,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg=
github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
@@ -665,7 +666,6 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
@@ -882,7 +882,6 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"github.com/joomcode/errorx"
"os"
"os/signal"
"strings"
@@ -73,7 +74,13 @@ func main() {
address, webServerDone, err := server.StartServer(ctx, cancel)
if err != nil {
log.Fatalf("Failed to start Helm Dashboard: %+v", err)
if errorx.IsOfType(err, errorx.InitializationFailed) {
log.Debugf("Full error: %+v", err)
log.Errorf("No Kubernetes cluster connection possible. Make sure you have valid kubeconfig file or run dashboard from inside cluster. See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/")
os.Exit(1)
} else {
log.Fatalf("Failed to start Helm Dashboard: %+v", err)
}
}
if !opts.NoTracking {

View File

@@ -4,6 +4,12 @@ import (
"encoding/json"
"errors"
"fmt"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"net/http"
"sort"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
@@ -19,10 +25,6 @@ import (
"helm.sh/helm/v3/pkg/repo"
helmtime "helm.sh/helm/v3/pkg/time"
"k8s.io/utils/strings/slices"
"net/http"
"sort"
"strconv"
"strings"
)
type HelmHandler struct {
@@ -135,6 +137,26 @@ func (h *HelmHandler) Resources(c *gin.Context) {
return
}
if c.Query("health") != "" { // we need to query k8s for health status
app := h.GetApp(c)
if app == nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
for _, obj := range res {
ns := obj.Namespace
if ns == "" {
ns = c.Param("ns")
}
info, err := app.K8s.GetResourceInfo(obj.Kind, ns, obj.Name)
if err != nil {
log.Warnf("Failed to get resource info for %s %s/%s: %+v", obj.Name, ns, obj.Name, err)
info = &v1.Carp{}
}
obj.Status = *EnhanceStatus(info, err)
}
}
c.IndentedJSON(http.StatusOK, res)
}
@@ -494,7 +516,7 @@ func (h *HelmHandler) RepoAdd(c *gin.Context) {
}
// TODO: more repo options to accept
err := app.Repositories.Add(c.PostForm("name"), c.PostForm("url"))
err := app.Repositories.Add(c.PostForm("name"), c.PostForm("url"), c.PostForm("username"), c.PostForm("password"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -586,23 +608,24 @@ func (h *HelmHandler) repoFromArtifactHub(name string) (string, error) {
return true
}
// more popular
if ri.Stars != rj.Stars {
return ri.Stars > rj.Stars
}
// or from verified publishers
if ri.Repository.VerifiedPublisher && !rj.Repository.VerifiedPublisher {
return true
}
// or just more popular
if ri.Stars > rj.Stars {
return true
}
// or with more recent app version
if semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion) > 0 {
return true
c := semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion)
if c != 0 {
return c > 0
}
return false
// shorter repo name is usually closer to officials
return len(ri.Repository.Name) < len(rj.Repository.Name)
})
r := results[0]

View File

@@ -1,15 +1,21 @@
package handlers
import (
"github.com/joomcode/errorx"
"k8s.io/apimachinery/pkg/api/errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/errors"
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"k8s.io/utils/strings/slices"
"net/http"
)
const Unknown = "Unknown"
const Healthy = "Healthy"
const Unhealthy = "Unhealthy"
const Progressing = "Progressing"
type KubeHandler struct {
*Contexted
}
@@ -50,25 +56,67 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
return
}
EnhanceStatus(res)
EnhanceStatus(res, nil)
c.IndentedJSON(http.StatusOK, res)
}
func EnhanceStatus(res *v12.Carp) {
// custom logic to provide most meaningful status for the resource
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"
func EnhanceStatus(res *v12.Carp, err error) *v12.CarpStatus {
s := res.Status
if s.Conditions == nil {
s.Conditions = []v12.CarpCondition{}
}
c := v12.CarpCondition{
Type: "hdHealth",
Status: Unknown,
Reason: s.Reason,
Message: s.Message,
}
// custom logic to provide most meaningful status for the resource
if err != nil {
c.Reason = "ErrorGettingStatus"
c.Message = err.Error()
} else if s.Phase == "Error" {
c.Status = Unhealthy
} else if slices.Contains([]string{"Available", "Active", "Established", "Bound", "Ready"}, string(s.Phase)) {
c.Status = Healthy
} else if s.Phase == "" && len(s.Conditions) > 0 {
for _, cond := range s.Conditions {
if cond.Type == "Progressing" { // https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
if cond.Status == "False" {
c.Status = Unhealthy
c.Reason = cond.Reason
c.Message = cond.Message
} else if cond.Reason != "NewReplicaSetAvailable" {
c.Status = Progressing
c.Reason = cond.Reason
c.Message = cond.Message
}
} else if cond.Type == "Available" && c.Status == Unknown {
if cond.Status == "False" {
c.Status = Unhealthy
} else {
c.Status = Healthy
}
c.Reason = cond.Reason
c.Message = cond.Message
}
}
} else if s.Phase == "Pending" {
c.Status = Progressing
c.Reason = string(s.Phase)
} else if s.Phase == "" {
c.Status = Healthy
c.Reason = "Exists"
} else {
log.Warnf("Unhandled status: %v", s)
c.Reason = string(s.Phase)
}
s.Conditions = append(s.Conditions, c)
return &s
}
func (h *KubeHandler) Describe(c *gin.Context) {

View File

@@ -126,7 +126,14 @@ func (k *K8s) DescribeResource(kind string, ns string, name string) (string, err
func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.Object, error) {
builder := k.Factory.NewBuilder()
resp := builder.Unstructured().NamespaceParam(namespace).Flatten().ResourceNames(kind, name).Do()
builder = builder.Unstructured().SingleResourceType()
if namespace != "" {
builder = builder.NamespaceParam(namespace)
} else {
builder = builder.DefaultNamespace()
}
resp := builder.Flatten().ResourceNames(kind, name).Do()
if resp.Err() != nil {
return nil, errorx.Decorate(resp.Err(), "failed to get k8s resource")
}
@@ -139,6 +146,7 @@ func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.
}
func (k *K8s) GetResourceInfo(kind string, namespace string, name string) (*testapiv1.Carp, error) {
// TODO: mutex to avoid a lot of requests?
obj, err := k.GetResource(kind, namespace, name)
if err != nil {
return nil, errorx.Decorate(err, "failed to get k8s object")

View File

@@ -65,11 +65,15 @@ func (r *Repositories) List() ([]Repository, error) {
return res, nil
}
func (r *Repositories) Add(name string, url string) error {
func (r *Repositories) Add(name string, url string, username string, password string) error {
if name == "" || url == "" {
return errors.New("Name and URL are required parameters to add the repository")
}
if (username != "" && password == "") || (username == "" && password != "") {
return errors.New("Username and Password, both are required parameters to add the repository with authentication")
}
// copied from cmd/helm/repo_add.go
repoFile := r.Settings.RepositoryConfig
@@ -88,10 +92,10 @@ func (r *Repositories) Add(name string, url string) error {
defer r.mx.Unlock()
c := repo.Entry{
Name: name,
URL: url,
//Username: o.username,
//Password: o.password,
Name: name,
URL: url,
Username: username,
Password: password,
//PassCredentialsAll: o.passCredentialsAll,
//CertFile: o.certFile,
//KeyFile: o.keyFile,

View File

@@ -75,7 +75,7 @@ func TestFlow(t *testing.T) {
testRepoUrl := "https://helm.github.io/examples"
// add repo
err = testRepository.Add(testRepoName, testRepoUrl)
err = testRepository.Add(testRepoName, testRepoUrl, "", "")
assert.NilError(t, err)
// get repo

View File

@@ -46,7 +46,7 @@ func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (st
err = s.detectClusterMode(data)
if err != nil {
return "", nil, err
return "", nil, errorx.Decorate(err, "Failed to detect cluster mode")
}
go checkUpgrade(data.StatusInfo)
@@ -80,7 +80,7 @@ func (s *Server) detectClusterMode(data *objects.DataLayer) error {
}
ns, err := app.K8s.GetNameSpaces()
if err != nil { // no point in continuing without kubectl context and k8s connection
return err
return errorx.InitializationFailed.Wrap(err, "No k8s cluster connection")
}
log.Debugf("Got %d namespaces listed", len(ns.Items))
data.StatusInfo.ClusterMode = true

View File

@@ -1,11 +1,28 @@
const xhr = new XMLHttpRequest();
const TRACK_EVENT_TYPE = "track"
const IDENTIFY_EVENT_TYPE = "identify"
const BASE_ANALYTIC_MSG = {
method: "POST",
mode: "cors",
cache: "no-cache",
headers: {
"Content-Type": "application/json",
"api-key": "komodor.analytics@admin.com",
},
redirect: "follow",
referrerPolicy: "no-referrer"
}
xhr.onload = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
const status = JSON.parse(xhr.responseText);
const version = status.CurVer
if (status.Analytics) {
enableDD(version)
enableHeap(version, status.ClusterMode)
enableSegmentBackend(version, status.ClusterMode)
} else {
console.log("Analytics is disabled in this session")
}
}
}
@@ -58,12 +75,57 @@ function enableHeap(version, inCluster) {
heap.load("4249623943");
window.heap.addEventProperties({
'version': version,
'installationMode': inCluster?"cluster":"local"
'installationMode': inCluster ? "cluster" : "local"
});
}
function sendStats(name, prop){
function sendStats(name, prop) {
if (window.heap) {
window.heap.track(name, prop);
}
}
function enableSegmentBackend(version, ClusterMode) {
sendToSegmentThroughAPI("helm dashboard loaded", {version, 'installationMode': ClusterMode ? "cluster" : "local"}, TRACK_EVENT_TYPE)
}
function sendToSegmentThroughAPI(eventName, properties, segmentCallType) {
const userId = getUserId();
try {
sendData(properties, segmentCallType, userId, eventName);
} catch (e) {
console.log("failed sending data to segment", e);
}
}
function sendData(data, eventType, userId, eventName) {
const body = createBody(eventType, userId, data, eventName);
return fetch(`https://api.komodor.com/analytics/segment/${eventType}`, {
...BASE_ANALYTIC_MSG,
body: JSON.stringify(body),
});
}
function createBody(segmentCallType, userId, params, eventName) {
const data = {userId: userId};
if (segmentCallType === IDENTIFY_EVENT_TYPE) {
data["traits"] = params;
} else if (segmentCallType === TRACK_EVENT_TYPE) {
if (!eventName) {
throw new Error("no eventName parameter on segment track call");
}
data["properties"] = params;
data["eventName"] = eventName;
}
return data;
}
const getUserId = (() => {
let userId = null;
return () => {
if (!userId) {
userId = crypto.randomUUID();
}
return userId;
};
})();

View File

@@ -35,12 +35,12 @@
const data = JSON.parse(request.responseText);
display(data);
} else {
alert("Failed to get "+ swaggerUrl)
alert("Failed to get " + swaggerUrl)
}
};
request.onerror = function () {
alert("Failed to get "+ swaggerUrl)
alert("Failed to get " + swaggerUrl)
};
request.send();
@@ -67,4 +67,5 @@
reqOas();
});
</script>
<script src="analytics.js"></script>
</html>

View File

@@ -149,7 +149,7 @@ function showResources(namespace, chart, revision) {
const resBody = $("#nav-resources .body");
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources"
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources?health=true"
$.getJSON(url).fail(function (xhr) {
reportError("Failed to get list of resources", xhr)
}).done(function (data) {
@@ -180,22 +180,31 @@ function showResources(namespace, chart, revision) {
resBody.append(resBlock)
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
$.getJSON("/api/k8s/" + res.kind.toLowerCase() + "/get?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
//reportError("Failed to get list of resources")
}).done(function (data) {
const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase);
if (["Available", "Active", "Established", "Bound", "Ready"].includes(data.status.phase)) {
for (let k = 0; k < res.status.conditions.length; k++) {
if (res.status.conditions[k].type !== "hdHealth") { // it's our custom condition type
continue
}
const cond = res.status.conditions[k]
const badge = $("<span class='badge me-2 fw-normal'></span>").text(cond.reason);
if (cond.status === "Unknown") {
badge.addClass("bg-secondary text-danger")
} else if (cond.status === "Healthy") {
badge.addClass("bg-success text-dark")
} else if (["Exists"].includes(data.status.phase)) {
badge.addClass("bg-success text-dark bg-opacity-50")
} else if (["Progressing"].includes(data.status.phase)) {
} else if (cond.status === "Progressing") {
badge.addClass("bg-warning")
} else {
badge.addClass("bg-danger")
}
if (["Exists"].includes(cond.reason)) {
badge.addClass("bg-opacity-50")
}
const statusBlock = resBlock.find(".res-status");
statusBlock.empty().append(badge).attr("title", data.status.phase)
const statusMessage = getStatusMessage(data.status)
statusBlock.empty().append(badge).attr("title", cond.reason)
const statusMessage = cond.message
resBlock.find(".res-statusmsg").html("<span class='text-muted small me-2'>" + (statusMessage ? statusMessage : '') + "</span>")
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
@@ -216,24 +225,14 @@ function showResources(namespace, chart, revision) {
}
}
if (badge.hasClass("bg-danger")) {
if (badge.hasClass("bg-danger") || badge.hasClass("bg-warning")) {
resBlock.find(".res-statusmsg").append("<a href='" + KomodorCTALink + "' class='btn btn-primary btn-sm fw-normal fs-80' target='_blank'>Troubleshoot in Komodor <i class='bi-box-arrow-up-right'></i></a>")
}
})
}
}
})
}
function getStatusMessage(status) {
if (!status) {
return
}
if (status.conditions) {
return status.conditions[0].message || status.conditions[0].reason
}
return status.message || status.reason
}
function showDescribe(ns, kind, name, badge) {
$("#describeModal .offcanvas-header p").text(kind)
$("#describeModalLabel").text(name).append(badge.addClass("ms-3 small fw-normal"))

View File

@@ -73,7 +73,7 @@
<img alt="Komodor" src="https://raw.githubusercontent.com/komodorio/helm-charts/master/k8s-watcher.svg" class="me-2" style="width: 42px; height: 42px"/>
<span class="text-nowrap">
<a href="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash"
class="link text-primary fw-bold text-decoration-none">Upgrade your HELM experience - Free
class="link text-primary fw-bold text-decoration-none" target="_blank">Upgrade your HELM experience - Free
<i class="bi-box-arrow-up-right ms-1"></i></a><br/>
Auth & RBAC, k8s events, troubleshooting and more
</span>
@@ -162,14 +162,6 @@
placeholder="Filter..."/>
</div>
</div>
<div class="bg-secondary rounded-bottom m-0 row p-2">
<div class="col-4 hdr-name">Name</div>
<div class="col-3">Release Status</div>
<div class="col-2">Chart</div>
<div class="col-1">Revision</div>
<div class="col-1">Namespace</div>
<div class="col-1">Updated</div>
</div>
</div>
<div class="body"></div>
@@ -362,8 +354,26 @@
</div>
<div class="modal-body">
<form enctype="application/x-www-form-urlencoded">
<label class="form-label">Name: <input class="form-control" name="name"></label>
<label class="form-label">URL: <input class="form-control" name="url"></label>
<div class="row mb-4">
<div class="col">
<label class="form-label required">Name</label>
<input class="form-control" type="text" name="name" placeholder="Komodorio">
</div>
<div class="col">
<label class="form-label required">URL</label>
<input class="form-control" type="text" name="url" placeholder="https://helm-charts.komodor.io">
</div>
</div>
<div class="row">
<div class="col">
<label class="form-label">Username</label>
<input class="form-control" type="text" name="username">
</div>
<div class="col">
<label class="form-label">Password</label>
<input class="form-control" type="password" name="password">
</div>
</div>
</form>
</div>
<div class="modal-footer">

View File

@@ -89,11 +89,11 @@ function buildChartCard(elm) {
}
if (isNewerVersion(elm.chartVersion, data[0].version) || data[0].isSuggestedRepo) {
const icon = $("<br/><span class='ms-2 fw-bold' data-bs-toggle='tooltip' data-bs-placement='bottom'></span>")
const icon = $("<br/><span class='fw-bold' data-bs-toggle='tooltip' data-bs-placement='bottom'></span>")
if (data[0].isSuggestedRepo) {
icon.addClass("bi-plus-circle-fill text-primary")
icon.text(" ADD REPO")
icon.attr("data-bs-title", "Add '" + data[0].repository+"' to list of known repositories")
icon.attr("data-bs-title", "Add '" + data[0].repository + "' to list of known repositories")
} else {
icon.addClass("bi-arrow-up-circle-fill text-primary")
icon.text(" UPGRADE")
@@ -101,12 +101,41 @@ function buildChartCard(elm) {
}
card.find(".rel-chart div").append(icon)
const tooltipTriggerList = card.find('[data-bs-toggle="tooltip"]')
const tooltipTriggerList = card.find('.rel-chart [data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
sendStats('upgradeIconShown', {'isProbable': data[0].isSuggestedRepo})
}
})
// check resource health status
$.getJSON("/api/helm/releases/" + elm.namespace + "/" + elm.name + "/resources?health=true").fail(function (xhr) {
reportError("Failed to find chart in repo", xhr)
}).done(function (data) {
for (let i = 0; i < data.length; i++) {
const res = data[i]
for (let k = 0; k < res.status.conditions.length; k++) {
if (res.status.conditions[k].type !== "hdHealth") { // it's our custom condition type
continue
}
const cond = res.status.conditions[k]
const square=$("<span class='me-1 mb-1 square rounded rounded-1' data-bs-toggle='tooltip'>&nbsp;</span>")
if (cond.status === "Healthy") {
square.addClass("bg-success")
} else if (cond.status === "Progressing") {
square.addClass("bg-warning")
} else {
square.addClass("bg-danger")
}
square.attr("data-bs-title", cond.status+" "+res.kind+" '"+res.metadata.name+"'")
card.find(".rel-status div").append(square)
}
}
const tooltipTriggerList = card.find('.rel-status [data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
})
return card;
}

View File

@@ -302,6 +302,11 @@
"name": "name",
"in": "path",
"description": "Name of Helm release"
},
{
"name": "health",
"in": "query",
"description": "Flag to query k8s health status of resources"
}
],
"get": {

View File

@@ -79,3 +79,8 @@
.fs-80 {
font-size: 0.8rem!important;
}
.required::after {
content: " *";
color: red;
}

View File

@@ -301,3 +301,26 @@ nav .nav-tabs .nav-link.active {
.test-result {
font-size: 1rem;
}
.square {
width: 0.55rem;
height: 0.55rem;
display: inline-block;
border-radius: 0.1rem!important;
}
.square.bg-danger {
background-color: #ff0072!important;
}
.square.bg-warning {
background-color: #ffa800!important;
}
.square.bg-success {
background-color: #00c2ab!important;
}
.hljs {
overflow-x: inherit;
}

View File

@@ -1,5 +1,5 @@
name: "dashboard"
version: "1.1.1"
version: "1.2.0"
usage: "A simplified way of working with Helm"
description: "View HELM situation in nice web UI"
command: "$HELM_PLUGIN_DIR/bin/helm-dashboard"