mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 11:48:04 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7de7c85426 | ||
|
|
8e65c555e0 | ||
|
|
2557e6b73d | ||
|
|
4f75ee06a0 | ||
|
|
717adc9e9c | ||
|
|
15adeb7cfa | ||
|
|
0b06036a39 | ||
|
|
f7d4dcbff4 | ||
|
|
8334f2b0b2 | ||
|
|
5cccb1caa0 | ||
|
|
db9cdeb1c9 | ||
|
|
3abae8e49e | ||
|
|
bedb356b02 | ||
|
|
34a7dc57b2 | ||
|
|
d0dbb42492 | ||
|
|
1393b117cf | ||
|
|
cf407c63a2 | ||
|
|
3f00e8ef6d | ||
|
|
758b03de36 | ||
|
|
76d55f8e44 | ||
|
|
b9392ab4c9 | ||
|
|
dadf2d1bde | ||
|
|
74c2a3d6e7 | ||
|
|
2454fcf47c | ||
|
|
96a7a429e1 | ||
|
|
f29800ed5b | ||
|
|
15ce9170f3 | ||
|
|
9a144c1c6f | ||
|
|
f897c0f197 | ||
|
|
671fa949df | ||
|
|
612352d69f | ||
|
|
ef31263797 | ||
|
|
f64fbd4a2e | ||
|
|
dffb8a726b | ||
|
|
14d4886e61 | ||
|
|
0012b0a797 | ||
|
|
f6b2a8c66d | ||
|
|
c0a1d31c8d | ||
|
|
329ae055ee | ||
|
|
7ab0f33201 | ||
|
|
2e8ba39b8f | ||
|
|
c5f9f71e45 | ||
|
|
9bb597f366 | ||
|
|
786bddc478 | ||
|
|
d9edcf2f48 | ||
|
|
2262445b75 | ||
|
|
0c486e76c0 | ||
|
|
7d50f4e620 | ||
|
|
b2ec371709 | ||
|
|
549cdd9bfb | ||
|
|
44787b31cf | ||
|
|
e75e653c58 | ||
|
|
3eae013286 | ||
|
|
2221fb22a0 | ||
|
|
9dc3e6a12d | ||
|
|
de0024cd03 | ||
|
|
ed4e970194 | ||
|
|
b0067e31ba | ||
|
|
7e8ba4709e | ||
|
|
be7b2642fc | ||
|
|
896d9e3f72 | ||
|
|
be6666373b | ||
|
|
91df9392c0 | ||
|
|
bd058ee912 | ||
|
|
997f951d0c |
26
.github/workflows/release.yaml
vendored
26
.github/workflows/release.yaml
vendored
@@ -6,9 +6,33 @@ on:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
pre_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get plugin version
|
||||
id: get_plugin_version
|
||||
run: echo "PLUGIN_VERSION=$(cat plugin.yaml | grep "version" | cut -d '"' -f 2)" >> $GITHUB_OUTPUT
|
||||
- name: Get tag name
|
||||
id: get_tag_name
|
||||
run: echo "TAG_NAME=$(echo ${{ github.ref_name }} | cut -d 'v' -f2)" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
plugin_version: ${{ steps.get_plugin_version.outputs.PLUGIN_VERSION }}
|
||||
tag_name: ${{ steps.get_tag_name.outputs.TAG_NAME }}
|
||||
|
||||
release:
|
||||
needs: pre_release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
#- name: Plugin version/Tag name Check
|
||||
# if: needs.pre_release.outputs.release_tag != needs.pre_release.outputs.plugin_version
|
||||
# uses: actions/github-script@v3
|
||||
# with:
|
||||
# script: |
|
||||
# core.setFailed('Plugin version and tag name are not equivalent!')
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
|
||||
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
https://komodorkommunity.slack.com/archives/C044U1B0265.
|
||||
itiel@komodor.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
||||
27
Makefile
27
Makefile
@@ -1,9 +1,24 @@
|
||||
pull:
|
||||
git pull
|
||||
DATE ?= $(shell date +%FT%T%z)
|
||||
VERSION ?= $(git describe --tags --always --dirty --match=v* 2> /dev/null || \
|
||||
cat $(CURDIR)/.version 2> /dev/null || echo "v0")
|
||||
|
||||
build:
|
||||
go build -o bin/dashboard .
|
||||
.PHONY: test
|
||||
test: ; $(info $(M) start unit testing...) @
|
||||
@go test $$(go list ./... | grep -v /mocks/) --race -v -short -coverprofile=profile.cov
|
||||
@echo "\n*****************************"
|
||||
@echo "** TOTAL COVERAGE: $$(go tool cover -func profile.cov | grep total | grep -Eo '[0-9]+\.[0-9]+')% **"
|
||||
@echo "*****************************\n"
|
||||
|
||||
.PHONY: pull
|
||||
pull: ; $(info $(M) Pulling source...) @
|
||||
@git pull
|
||||
|
||||
debug:
|
||||
DEBUG=1 ./bin/dashboard
|
||||
.PHONY: build
|
||||
build: $(BIN) ; $(info $(M) Building executable...) @ ## Build program binary
|
||||
go build \
|
||||
-ldflags '-X main.version=$(VERSION) -X main.buildDate=$(DATE)' \
|
||||
-o bin/dashboard .
|
||||
|
||||
.PHONY: debug
|
||||
debug: ; $(info $(M) Running dashboard in debug mode...) @
|
||||
@DEBUG=1 ./bin/dashboard
|
||||
22
README.md
22
README.md
@@ -1,4 +1,4 @@
|
||||
# <img src="pkg/dashboard/static/logo.png" height=30 style="height: 2rem"> Helm Dashboard
|
||||
# 
|
||||
|
||||
A simplified way of working with Helm.
|
||||
|
||||
@@ -42,9 +42,11 @@ To uninstall, run:
|
||||
helm plugin uninstall dashboard
|
||||
```
|
||||
|
||||
> In case standard Helm plugin way did not work for you, you can just download the appropriate [release package](https://github.com/komodorio/helm-dashboard/releases) for your platform, unpack it and just run `dashboard` binary from it.
|
||||
|
||||
## Running
|
||||
|
||||
To use the plugin, your machine needs to have working `helm` and also `kubectl` commands.
|
||||
To use the plugin, your machine needs to have working `helm` and also `kubectl` commands. Helm version 3.4.0+ is required.
|
||||
|
||||
After installing, start the UI by running:
|
||||
|
||||
@@ -55,14 +57,22 @@ helm dashboard
|
||||
The command above will launch the local Web server and will open the UI in new browser tab. The command will hang
|
||||
waiting for you to terminate it in command-line or web UI.
|
||||
|
||||
You can see the list of available command-line flags by running `helm dashboard --help`.
|
||||
|
||||
By default, the web server is only available locally. You can change that by specifying `HD_BIND` environment variable
|
||||
to the desired value. For example, `0.0.0.0` would bind to all IPv4 addresses or `[::0]` would be all IPv6 addresses.
|
||||
This can also be specified using flag `--bind <host>`, for example `--bind=0.0.0.0` or `--bind 0.0.0.0`.
|
||||
> Precedence order: flag `--bind=<host>` > env `HD_BIND=<host>` > default value `localhost`
|
||||
|
||||
If your port 8080 is busy, you can specify a different port to use via `HD_PORT` environment variable.
|
||||
If your port 8080 is busy, you can specify a different port to use via `--port <number>` command-line flag.
|
||||
|
||||
If you don't want browser tab to automatically open, set `HD_NOBROWSER=1` in your environment variables.
|
||||
If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line.
|
||||
|
||||
If you want to increase the logging verbosity and see all the debug info, set `DEBUG=1` environment variable.
|
||||
If you don't want browser tab to automatically open, add `--no-browser` flag in your command line.
|
||||
|
||||
If you want to increase the logging verbosity and see all the debug info, use the `--verbose` flag.
|
||||
|
||||
> Disclaimer: For the sake of improving the project quality, there is user analytics collected by the tool. You can disable this collecting with `--no-analytics` option. The collection is done via DataDog RUM and Heap Analytics. Only the anonymous data is collected, no sensitive information is used.
|
||||
|
||||
## Scanner Integrations
|
||||
|
||||
@@ -80,7 +90,7 @@ button at the bottom of the dialog:
|
||||
## Support Channels
|
||||
|
||||
We have two main channels for supporting the Helm Dashboard
|
||||
users: [Slack community](https://komodorkommunity.slack.com/archives/C044U1B0265) for general conversations
|
||||
users: [Slack community](https://join.slack.com/t/komodorkommunity/shared_invite/zt-1dm3cnkue-ov1Yh~_95teA35QNx5yuMg) for general conversations
|
||||
and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
|
||||
|
||||
|
||||
|
||||
105
go.mod
105
go.mod
@@ -3,122 +3,69 @@ module github.com/komodorio/helm-dashboard
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/aquasecurity/trivy v0.32.1
|
||||
github.com/eko/gocache/v3 v3.1.1
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/hexops/gotextdiff v1.0.3
|
||||
github.com/jessevdk/go-flags v1.5.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
helm.sh/helm/v3 v3.9.4
|
||||
k8s.io/apimachinery v0.25.0-alpha.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/CycloneDX/cyclonedx-go v0.6.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||
github.com/Sirupsen/logrus v1.0.6 // indirect
|
||||
github.com/aquasecurity/go-dep-parser v0.0.0-20220928105313-d3a51fe400e4 // indirect
|
||||
github.com/aquasecurity/table v1.8.0 // indirect
|
||||
github.com/aquasecurity/trivy-db v0.0.0-20220627104749-930461748b63 // indirect
|
||||
github.com/aquasecurity/trivy-kubernetes v0.3.1-0.20220823151349-b90b48958b91 // indirect
|
||||
github.com/caarlos0/env/v6 v6.10.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/libnetwork v0.5.6 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // 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
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/go-containerregistry v0.11.0 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/liamg/tml v0.6.0 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/owenrumney/go-sarif/v2 v2.1.2 // indirect
|
||||
github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a // indirect
|
||||
github.com/pegasus-kv/thrift v0.13.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.3 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/samber/lo v1.27.1 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/spdx/tools-golang v0.3.0 // indirect
|
||||
github.com/prometheus/client_golang v1.12.2 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.33.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.5.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.4.0 // indirect
|
||||
github.com/stretchr/testify v1.8.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/vishvananda/netlink v1.1.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20220913150850-18c4f4234207 // indirect
|
||||
github.com/xlab/treeprint v1.1.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
||||
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
|
||||
golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect
|
||||
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf // indirect
|
||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 // indirect
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
|
||||
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
k8s.io/api v0.25.0-alpha.2 // indirect
|
||||
k8s.io/cli-runtime v0.24.4 // indirect
|
||||
k8s.io/client-go v0.25.0-alpha.2 // indirect
|
||||
k8s.io/klog/v2 v2.70.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect
|
||||
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.11.4 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
99
main.go
99
main.go
@@ -1,50 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard"
|
||||
"github.com/pkg/browser"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
version = "0.0.0"
|
||||
commit = "none"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
setupLogging()
|
||||
type options struct {
|
||||
Version bool `long:"version" description:"Show tool version"`
|
||||
Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"`
|
||||
NoBrowser bool `short:"b" long:"no-browser" description:"Do not attempt to open Web browser upon start"`
|
||||
NoTracking bool `long:"no-analytics" description:"Disable user analytics (GA, DataDog etc.)"`
|
||||
BindHost string `long:"bind" description:"Host binding to start server (default: localhost)"` // default should be printed but not assigned as the precedence: flag > env > default
|
||||
Port uint `short:"p" long:"port" description:"Port to start server on" default:"8080"` // TODO: better default port to clash less?
|
||||
Namespace string `short:"n" long:"namespace" description:"Limit operations to a specific namespace"`
|
||||
}
|
||||
|
||||
// TODO: proper command-line parsing
|
||||
if len(os.Args) > 1 { // dirty thing to allow --help to work
|
||||
os.Exit(0)
|
||||
func main() {
|
||||
opts := parseFlags()
|
||||
if opts.BindHost == "" {
|
||||
host := os.Getenv("HD_BIND")
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
opts.BindHost = host
|
||||
}
|
||||
|
||||
address, webServerDone := dashboard.StartServer(version)
|
||||
setupLogging(opts.Verbose)
|
||||
|
||||
if os.Getenv("HD_NOBROWSER") == "" {
|
||||
server := dashboard.Server{
|
||||
Version: version,
|
||||
Namespace: opts.Namespace,
|
||||
Address: fmt.Sprintf("%s:%d", opts.BindHost, opts.Port),
|
||||
Debug: opts.Verbose,
|
||||
NoTracking: opts.NoTracking,
|
||||
}
|
||||
address, webServerDone := server.StartServer()
|
||||
|
||||
if !opts.NoTracking {
|
||||
log.Infof("User analytics is collected to improve the quality, disable it with --no-analytics")
|
||||
}
|
||||
|
||||
if opts.NoBrowser {
|
||||
log.Infof("Access web UI at: %s", address)
|
||||
} else {
|
||||
log.Infof("Opening web UI: %s", address)
|
||||
err := webbrowser.Open(address)
|
||||
err := browser.OpenURL(address)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to open Web browser for URL: %s", err)
|
||||
}
|
||||
} else {
|
||||
log.Infof("Access web UI at: %s", address)
|
||||
}
|
||||
|
||||
<-webServerDone
|
||||
log.Infof("Done.")
|
||||
}
|
||||
|
||||
func setupLogging() {
|
||||
if os.Getenv("DEBUG") == "" {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
} else {
|
||||
func parseFlags() options {
|
||||
ns := os.Getenv("HELM_NAMESPACE")
|
||||
if ns == "default" {
|
||||
ns = ""
|
||||
}
|
||||
|
||||
opts := options{Namespace: ns}
|
||||
args, err := flags.Parse(&opts)
|
||||
if err != nil {
|
||||
if e, ok := err.(*flags.Error); ok {
|
||||
if e.Type == flags.ErrHelp {
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// we rely on default behavior to print the problem inside `flags` library
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if opts.Version {
|
||||
fmt.Println(version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
panic("The program does not take argumants, see --help for usage")
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func setupLogging(verbose bool) {
|
||||
if verbose {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
gin.SetMode(gin.DebugMode)
|
||||
log.Debugf("Debug logging is enabled")
|
||||
} else {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
log.Infof("Helm Dashboard by Komodor, version %s (%s @ %s)", version, commit, date)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ package dashboard
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/handlers"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
@@ -36,17 +37,24 @@ func errorHandler(c *gin.Context) {
|
||||
|
||||
func contextSetter(data *subproc.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]
|
||||
if ctx, ok := c.Request.Header["X-Kubecontext"]; ok {
|
||||
log.Debugf("Setting current context to: %s", ctx)
|
||||
if data.KubeContext != ctx[0] {
|
||||
err := data.Cache.Clear()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
data.KubeContext = ctx[0]
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, version string) *gin.Engine {
|
||||
func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, debug bool) *gin.Engine {
|
||||
var api *gin.Engine
|
||||
if os.Getenv("DEBUG") == "" {
|
||||
if debug {
|
||||
api = gin.New()
|
||||
api.Use(gin.Recovery())
|
||||
} else {
|
||||
@@ -58,12 +66,12 @@ func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, version stri
|
||||
api.Use(errorHandler)
|
||||
|
||||
configureStatic(api)
|
||||
configureRoutes(abortWeb, data, api, version)
|
||||
configureRoutes(abortWeb, data, api)
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *gin.Engine, version string) {
|
||||
func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *gin.Engine) {
|
||||
// server shutdown handler
|
||||
api.DELETE("/", func(c *gin.Context) {
|
||||
abortWeb <- struct{}{}
|
||||
@@ -71,7 +79,21 @@ func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *g
|
||||
})
|
||||
|
||||
api.GET("/status", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, version)
|
||||
c.Header("X-Application-Name", "Helm Dashboard by Komodor.io") // to identify ourselves by ourselves
|
||||
c.IndentedJSON(http.StatusOK, data.GetStatus())
|
||||
})
|
||||
|
||||
api.GET("/api/cache", func(c *gin.Context) {
|
||||
c.IndentedJSON(http.StatusOK, data.Cache)
|
||||
})
|
||||
|
||||
api.DELETE("/api/cache", func(c *gin.Context) {
|
||||
err := data.Cache.Clear()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusAccepted)
|
||||
})
|
||||
|
||||
configureHelms(api.Group("/api/helm"), data)
|
||||
@@ -88,6 +110,7 @@ func configureHelms(api *gin.RouterGroup, data *subproc.DataLayer) {
|
||||
api.GET("/charts/history", h.History)
|
||||
api.GET("/charts/resources", h.Resources)
|
||||
api.GET("/charts/:section", h.GetInfoSection)
|
||||
api.GET("/charts/show", h.Show)
|
||||
api.POST("/charts/install", h.Install)
|
||||
api.POST("/charts/rollback", h.Rollback)
|
||||
|
||||
@@ -105,6 +128,7 @@ func configureKubectls(api *gin.RouterGroup, data *subproc.DataLayer) {
|
||||
api.GET("/contexts", h.GetContexts)
|
||||
api.GET("/resources/:kind", h.GetResourceInfo)
|
||||
api.GET("/describe/:kind", h.Describe)
|
||||
api.GET("/namespaces", h.GetNameSpaces)
|
||||
}
|
||||
|
||||
func configureStatic(api *gin.Engine) {
|
||||
|
||||
@@ -2,12 +2,13 @@ package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
)
|
||||
|
||||
type HelmHandler struct {
|
||||
@@ -23,15 +24,13 @@ func (h *HelmHandler) GetCharts(c *gin.Context) {
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
// TODO: helm show chart komodorio/k8s-watcher to get the icon URL
|
||||
|
||||
func (h *HelmHandler) Uninstall(c *gin.Context) {
|
||||
qp, err := utils.GetQueryProps(c, false)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
err = h.Data.ChartUninstall(qp.Namespace, qp.Name)
|
||||
err = h.Data.ReleaseUninstall(qp.Namespace, qp.Name)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
@@ -46,7 +45,7 @@ func (h *HelmHandler) Rollback(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Data.Revert(qp.Namespace, qp.Name, qp.Revision)
|
||||
err = h.Data.Rollback(qp.Namespace, qp.Name, qp.Revision)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
@@ -61,7 +60,7 @@ func (h *HelmHandler) History(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := h.Data.ChartHistory(qp.Namespace, qp.Name)
|
||||
res, err := h.Data.ReleaseHistory(qp.Namespace, qp.Name)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
@@ -129,6 +128,22 @@ func (h *HelmHandler) RepoUpdate(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) Show(c *gin.Context) {
|
||||
qp, err := utils.GetQueryProps(c, false)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := h.Data.ShowChart(qp.Name)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) Install(c *gin.Context) {
|
||||
qp, err := utils.GetQueryProps(c, false)
|
||||
if err != nil {
|
||||
@@ -248,11 +263,11 @@ func handleGetSection(data *subproc.DataLayer, section string, rDiff string, qp
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
} else {
|
||||
res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type KubeHandler struct {
|
||||
@@ -38,6 +39,7 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -69,3 +71,13 @@ func (h *KubeHandler) Describe(c *gin.Context) {
|
||||
|
||||
c.String(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *KubeHandler) GetNameSpaces(c *gin.Context) {
|
||||
res, err := h.Data.GetNameSpaces()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
@@ -12,11 +12,18 @@ type ScannersHandler struct {
|
||||
}
|
||||
|
||||
func (h *ScannersHandler) List(c *gin.Context) {
|
||||
var res []string
|
||||
for _, scanner := range h.Data.Scanners {
|
||||
res = append(res, scanner.Name())
|
||||
type ScannerInfo struct {
|
||||
SupportedResourceKinds []string
|
||||
ManifestScannable bool
|
||||
}
|
||||
c.JSON(http.StatusOK, res)
|
||||
res := map[string]ScannerInfo{}
|
||||
for _, scanner := range h.Data.Scanners {
|
||||
res[scanner.Name()] = ScannerInfo{
|
||||
SupportedResourceKinds: scanner.SupportedResourceKinds(),
|
||||
ManifestScannable: scanner.ManifestIsScannable(),
|
||||
}
|
||||
}
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package scanners
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -13,11 +14,46 @@ type Checkov struct {
|
||||
Data *subproc.DataLayer
|
||||
}
|
||||
|
||||
func (c *Checkov) ManifestIsScannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Checkov) SupportedResourceKinds() []string {
|
||||
// from https://github.com/bridgecrewio/checkov//blob/master/docs/5.Policy%20Index/kubernetes.md
|
||||
return []string{
|
||||
"AdmissionConfiguration",
|
||||
"ClusterRole",
|
||||
"ClusterRoleBinding",
|
||||
"ConfigMap",
|
||||
"CronJob",
|
||||
"DaemonSet",
|
||||
"Deployment",
|
||||
"DeploymentConfig",
|
||||
"Ingress",
|
||||
"Job",
|
||||
"Pod",
|
||||
"PodSecurityPolicy",
|
||||
"PodTemplate",
|
||||
"Policy",
|
||||
"ReplicaSet",
|
||||
"ReplicationController",
|
||||
"Role",
|
||||
"RoleBinding",
|
||||
"Secret",
|
||||
"Service",
|
||||
"ServiceAccount",
|
||||
"StatefulSet",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Checkov) Name() string {
|
||||
return "Checkov"
|
||||
}
|
||||
|
||||
func (c *Checkov) Test() bool {
|
||||
utils.FailLogLevel = log.DebugLevel
|
||||
defer func() { utils.FailLogLevel = log.WarnLevel }()
|
||||
|
||||
res, err := utils.RunCommand([]string{"checkov", "--version"}, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -33,7 +69,7 @@ func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) {
|
||||
}
|
||||
defer fclose()
|
||||
|
||||
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "cli", "--file", fname}
|
||||
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "json", "--file", fname}
|
||||
out, err := utils.RunCommand(cmd, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -41,7 +77,10 @@ func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) {
|
||||
|
||||
res := &subproc.ScanResults{}
|
||||
|
||||
res.OrigReport = out
|
||||
err = json.Unmarshal([]byte(out), res.OrigReport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -61,41 +100,46 @@ func (c *Checkov) ScanResource(ns string, kind string, name string) (*subproc.Sc
|
||||
}
|
||||
defer fclose()
|
||||
|
||||
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "cli", "--file", fname}
|
||||
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "json", "--file", fname}
|
||||
out, err := utils.RunCommand(cmd, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := subproc.ScanResults{}
|
||||
_, out, _ = strings.Cut(out, "\n") // kubernetes scan results:
|
||||
_, out, _ = strings.Cut(out, "\n") // empty line
|
||||
line, out, found := strings.Cut(out, "\n") // status line
|
||||
if found {
|
||||
parts := strings.FieldsFunc(line, func(r rune) bool {
|
||||
return r == ':' || r == ','
|
||||
})
|
||||
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
|
||||
res.PassedCount = cnt
|
||||
} else {
|
||||
log.Warnf("Failed to parse Checkov output: %s", err)
|
||||
}
|
||||
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[3])); err == nil {
|
||||
res.FailedCount = cnt
|
||||
} else {
|
||||
log.Warnf("Failed to parse Checkov output: %s", err)
|
||||
}
|
||||
} else {
|
||||
log.Warnf("Failed to parse Checkov output")
|
||||
cr := CheckovReport{}
|
||||
err = json.Unmarshal([]byte(out), &cr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.OrigReport = strings.TrimSpace(out)
|
||||
res := &subproc.ScanResults{
|
||||
PassedCount: cr.Summary.Passed,
|
||||
FailedCount: cr.Summary.Failed,
|
||||
OrigReport: checkovReportTable(&cr),
|
||||
}
|
||||
|
||||
return &res, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type CheckovResults struct {
|
||||
Summary CheckovSummary
|
||||
func checkovReportTable(c *CheckovReport) string {
|
||||
data := [][]string{}
|
||||
for _, item := range c.Results.FailedChecks {
|
||||
data = append(data, []string{item.Id, item.Name + "\n", item.Guideline})
|
||||
}
|
||||
|
||||
tableString := &strings.Builder{}
|
||||
table := tablewriter.NewWriter(tableString)
|
||||
table.SetHeader([]string{"ID", "Name", "Guideline"})
|
||||
table.SetBorder(false)
|
||||
table.SetColWidth(64)
|
||||
table.AppendBulk(data)
|
||||
table.Render()
|
||||
return tableString.String()
|
||||
}
|
||||
|
||||
type CheckovReport struct {
|
||||
Summary CheckovSummary `json:"summary"`
|
||||
Results CheckovResults `json:"results"`
|
||||
}
|
||||
|
||||
type CheckovSummary struct {
|
||||
@@ -105,3 +149,16 @@ type CheckovSummary struct {
|
||||
// parsing errors?
|
||||
// skipped ?
|
||||
}
|
||||
|
||||
type CheckovResults struct {
|
||||
FailedChecks []CheckovCheck `json:"failed_checks"`
|
||||
}
|
||||
|
||||
type CheckovCheck struct {
|
||||
Id string `json:"check_id"`
|
||||
BcId string `json:"bc_check_id"`
|
||||
Name string `json:"check_name"`
|
||||
Resource string `json:"resource"`
|
||||
Guideline string `json:"guideline"`
|
||||
FileLineRange []int `json:"file_line_range"`
|
||||
}
|
||||
|
||||
@@ -12,11 +12,31 @@ type Trivy struct {
|
||||
Data *subproc.DataLayer
|
||||
}
|
||||
|
||||
func (c *Trivy) ManifestIsScannable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Trivy) SupportedResourceKinds() []string {
|
||||
// from https://github.com/aquasecurity/trivy-kubernetes/blob/main/pkg/k8s/k8s.go#L190
|
||||
return []string{
|
||||
"ReplicaSet",
|
||||
"ReplicationController",
|
||||
"StatefulSet",
|
||||
"Deployment",
|
||||
"CronJob",
|
||||
"DaemonSet",
|
||||
"Job",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Trivy) Name() string {
|
||||
return "Trivy"
|
||||
}
|
||||
|
||||
func (c *Trivy) Test() bool {
|
||||
utils.FailLogLevel = log.DebugLevel
|
||||
defer func() { utils.FailLogLevel = log.WarnLevel }()
|
||||
|
||||
res, err := utils.RunCommand([]string{"trivy", "--version"}, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
@@ -2,51 +2,73 @@ package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/scanners"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func StartServer(version string) (string, utils.ControlChan) {
|
||||
data := subproc.DataLayer{}
|
||||
type Server struct {
|
||||
Version string
|
||||
Namespace string
|
||||
Address string
|
||||
Debug bool
|
||||
NoTracking bool
|
||||
}
|
||||
|
||||
func (s Server) StartServer() (string, utils.ControlChan) {
|
||||
data := subproc.DataLayer{
|
||||
Namespace: s.Namespace,
|
||||
Cache: subproc.NewCache(),
|
||||
}
|
||||
err := data.CheckConnectivity()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to check that Helm is operational, cannot continue. The error was: %s", err)
|
||||
os.Exit(1) // TODO: propagate error instead?
|
||||
}
|
||||
isDevModeWithAnalytics := os.Getenv("HD_DEV_ANALYTICS") == "true"
|
||||
enableAnalytics := (!s.NoTracking && s.Version != "0.0.0") || isDevModeWithAnalytics
|
||||
data.StatusInfo = &subproc.StatusInfo{
|
||||
CurVer: s.Version,
|
||||
Analytics: enableAnalytics,
|
||||
LimitedToNamespace: s.Namespace,
|
||||
}
|
||||
go checkUpgrade(data.StatusInfo)
|
||||
|
||||
discoverScanners(&data)
|
||||
|
||||
address := os.Getenv("HD_BIND")
|
||||
if address == "" {
|
||||
address = "localhost"
|
||||
}
|
||||
|
||||
if os.Getenv("HD_PORT") == "" {
|
||||
address += ":8080" // TODO: better default port to clash less?
|
||||
} else {
|
||||
address += ":" + os.Getenv("HD_PORT")
|
||||
}
|
||||
|
||||
abort := make(utils.ControlChan)
|
||||
api := NewRouter(abort, &data, version)
|
||||
done := startBackgroundServer(address, api, abort)
|
||||
api := NewRouter(abort, &data, s.Debug)
|
||||
done := s.startBackgroundServer(api, abort)
|
||||
|
||||
return "http://" + address, done
|
||||
return "http://" + s.Address, done
|
||||
}
|
||||
|
||||
func startBackgroundServer(addr string, routes *gin.Engine, abort utils.ControlChan) utils.ControlChan {
|
||||
func (s Server) startBackgroundServer(routes *gin.Engine, abort utils.ControlChan) utils.ControlChan {
|
||||
done := make(utils.ControlChan)
|
||||
server := &http.Server{Addr: addr, Handler: routes}
|
||||
server := &http.Server{
|
||||
Addr: s.Address,
|
||||
Handler: routes,
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
panic(err) // TODO: in case of "port busy", check that it's another instance of us and just open browser
|
||||
log.Warnf("Looks like port is busy for %s, checking if it's us...", s.Address)
|
||||
if s.itIsUs() {
|
||||
log.Infof("Yes, it's another instance of us. Just reuse it.")
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
@@ -62,6 +84,21 @@ func startBackgroundServer(addr string, routes *gin.Engine, abort utils.ControlC
|
||||
return done
|
||||
}
|
||||
|
||||
func (s Server) itIsUs() bool {
|
||||
url := fmt.Sprintf("http://%s/status", s.Address)
|
||||
var myClient = &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
r, err := myClient.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("It's not us on %s: %s", s.Address, err)
|
||||
return false
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
return strings.HasPrefix(r.Header.Get("X-Application-Name"), "Helm Dashboard")
|
||||
}
|
||||
|
||||
func discoverScanners(data *subproc.DataLayer) {
|
||||
potential := []subproc.Scanner{
|
||||
&scanners.Checkov{Data: data},
|
||||
@@ -75,3 +112,44 @@ func discoverScanners(data *subproc.DataLayer) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkUpgrade(d *subproc.StatusInfo) {
|
||||
url := "https://api.github.com/repos/komodorio/helm-dashboard/releases/latest"
|
||||
type GHRelease struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var myClient = &http.Client{Timeout: 5 * time.Second}
|
||||
r, err := myClient.Get(url)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to check for new version: %s", err)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
target := new(GHRelease)
|
||||
err = json.NewDecoder(r.Body).Decode(target)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to decode new release version: %s", err)
|
||||
return
|
||||
}
|
||||
d.LatestVer = target.Name
|
||||
|
||||
v1, err := version.NewVersion(d.CurVer)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to parse CurVer: %s", err)
|
||||
v1 = &version.Version{}
|
||||
}
|
||||
|
||||
v2, err := version.NewVersion(d.LatestVer)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to parse LatestVer: %s", err)
|
||||
} else {
|
||||
if v1.LessThan(v2) {
|
||||
log.Warnf("Newer Helm Dashboard version is available: %s", d.LatestVer)
|
||||
log.Warnf("Upgrade instructions: https://github.com/komodorio/helm-dashboard#installing")
|
||||
} else {
|
||||
log.Debugf("Got latest version from GH: %s", d.LatestVer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,11 @@ function checkUpgradeable(name) {
|
||||
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-x-octagon")
|
||||
$("#btnUpgrade").prop("disabled", true)
|
||||
$("#btnUpgradeCheck").prop("disabled", true)
|
||||
$("#btnAddRepository").text("Add repository for it")
|
||||
return
|
||||
}
|
||||
|
||||
$("#btnUpgradeCheck").text("Check for new version")
|
||||
const verCur = $("#specRev").data("last-chart-ver");
|
||||
const elm = data[0]
|
||||
$("#btnUpgradeCheck").data("repo", elm.name.split('/').shift())
|
||||
@@ -57,14 +59,18 @@ function popUpUpgrade(elm, ns, name, verCur, lastRev) {
|
||||
|
||||
$("#upgradeModalLabel .name").text(elm.name)
|
||||
|
||||
$("#upgradeModal .rel-cluster").text(getHashParam("context"))
|
||||
|
||||
if (verCur) {
|
||||
$("#upgradeModalLabel .type").text("Upgrade")
|
||||
$("#upgradeModal .ver-old").show().find("span").text(verCur)
|
||||
$("#upgradeModal .rel-name").prop("disabled", true).val(name)
|
||||
$("#upgradeModal .rel-ns").prop("disabled", true).val(ns)
|
||||
} else {
|
||||
$("#upgradeModalLabel .type").text("Install")
|
||||
$("#upgradeModal .ver-old").hide()
|
||||
$("#upgradeModal .rel-name").prop("disabled", false).val(elm.name.split("/").pop())
|
||||
$("#upgradeModal .rel-ns").prop("disabled", false).val("")
|
||||
$("#upgradeModal .rel-ns").prop("disabled", false).val(ns)
|
||||
}
|
||||
|
||||
$.getJSON("/api/helm/repo/search?name=" + elm.name).fail(function (xhr) {
|
||||
@@ -112,7 +118,8 @@ $("#upgradeModal .btn-confirm").click(function () {
|
||||
}).done(function (data) {
|
||||
if (data.version) {
|
||||
setHashParam("section", null)
|
||||
setHashParam("namespace", $("#upgradeModal .rel-ns").val())
|
||||
const ns = $("#upgradeModal .rel-ns").val();
|
||||
setHashParam("namespace", ns ? ns : "default") // TODO: relaets issue #51
|
||||
setHashParam("chart", $("#upgradeModal .rel-name").val())
|
||||
setHashParam("revision", data.version)
|
||||
window.location.reload()
|
||||
@@ -329,3 +336,7 @@ $("#btnRollback").click(function () {
|
||||
})
|
||||
})
|
||||
|
||||
$("#btnAddRepository").click(function () {
|
||||
setHashParam("section", "repository")
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
66
pkg/dashboard/static/analytics.js
Normal file
66
pkg/dashboard/static/analytics.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const xhr = new XMLHttpRequest();
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.open('GET', '/status', true);
|
||||
xhr.send(null);
|
||||
|
||||
|
||||
function enableDD(version) {
|
||||
(function (h, o, u, n, d) {
|
||||
h = h[d] = h[d] || {
|
||||
q: [], onReady: function (c) {
|
||||
h.q.push(c)
|
||||
}
|
||||
}
|
||||
d = o.createElement(u);
|
||||
d.async = true;
|
||||
d.src = n
|
||||
n = o.getElementsByTagName(u)[0];
|
||||
n.parentNode.insertBefore(d, n)
|
||||
})(window, document, 'script', 'https://www.datadoghq-browser-agent.com/datadog-rum-v4.js', 'DD_RUM')
|
||||
DD_RUM.onReady(function () {
|
||||
DD_RUM.init({
|
||||
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
|
||||
applicationId: 'e75439e5-e1b3-46ba-a9e9-a2e58579a2e2',
|
||||
site: 'datadoghq.com',
|
||||
service: 'helm-dashboard',
|
||||
version: version,
|
||||
trackInteractions: true,
|
||||
trackResources: true,
|
||||
trackLongTasks: true,
|
||||
defaultPrivacyLevel: 'mask',
|
||||
sessionReplaySampleRate: 0
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function enableHeap(version) {
|
||||
window.heap = window.heap || [], heap.load = function (e, t) {
|
||||
window.heap.appid = e, window.heap.config = t = t || {};
|
||||
let r = document.createElement("script");
|
||||
r.type = "text/javascript", r.async = !0, r.src = "https://cdn.heapanalytics.com/js/heap-" + e + ".js";
|
||||
let a = document.getElementsByTagName("script")[0];
|
||||
a.parentNode.insertBefore(r, a);
|
||||
for (let n = function (e) {
|
||||
return function () {
|
||||
heap.push([e].concat(Array.prototype.slice.call(arguments, 0)))
|
||||
}
|
||||
}, p = ["addEventProperties", "addUserProperties", "clearEventProperties", "identify", "resetIdentity", "removeEventProperty", "setEventProperties", "track", "unsetEventProperty"], o = 0; o < p.length; o++) heap[p[o]] = n(p[o])
|
||||
};
|
||||
heap.load("4249623943");
|
||||
window.heap.addEventProperties({'version': version});
|
||||
}
|
||||
|
||||
function sendStats(name, prop){
|
||||
if (window.heap) {
|
||||
window.heap.track(name, prop);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
(function(h,o,u,n,d) {
|
||||
h=h[d]=h[d]||{q:[],onReady:function(c){h.q.push(c)}}
|
||||
d=o.createElement(u);d.async=1;d.src=n
|
||||
n=o.getElementsByTagName(u)[0];n.parentNode.insertBefore(d,n)
|
||||
})(window,document,'script','https://www.datadoghq-browser-agent.com/datadog-rum-v4.js','DD_RUM')
|
||||
DD_RUM.onReady(function() {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
const version = xhr.responseText;
|
||||
if (xhr.readyState === XMLHttpRequest.DONE && version!=="dev") {
|
||||
DD_RUM.init({
|
||||
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
|
||||
applicationId: 'e75439e5-e1b3-46ba-a9e9-a2e58579a2e2',
|
||||
site: 'datadoghq.com',
|
||||
service: 'helm-dashboard',
|
||||
version: version,
|
||||
trackInteractions: true,
|
||||
trackResources: true,
|
||||
trackLongTasks: true,
|
||||
defaultPrivacyLevel: 'mask'
|
||||
})
|
||||
}
|
||||
}
|
||||
xhr.open('GET', '/status', true);
|
||||
xhr.send(null);
|
||||
})
|
||||
@@ -143,6 +143,7 @@ $("#nav-tab [data-tab]").click(function () {
|
||||
|
||||
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 qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
|
||||
let url = "/api/helm/charts/resources"
|
||||
@@ -150,7 +151,14 @@ function showResources(namespace, chart, revision) {
|
||||
$.getJSON(url).fail(function (xhr) {
|
||||
reportError("Failed to get list of resources", xhr)
|
||||
}).done(function (data) {
|
||||
const scanners = $("body").data("scanners");
|
||||
const scannableResKinds = new Set();
|
||||
for (let k in scanners) {
|
||||
scanners[k].SupportedResourceKinds.forEach(scannableResKinds.add, scannableResKinds)
|
||||
}
|
||||
|
||||
resBody.empty();
|
||||
data = data.sort(function(a, b){return interestingResources.indexOf(a.kind.toUpperCase()) - interestingResources.indexOf(b.kind.toUpperCase())}).reverse();
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const res = data[i]
|
||||
const resBlock = $(`
|
||||
@@ -159,7 +167,7 @@ function showResources(namespace, chart, revision) {
|
||||
<div class="col-3 res-name text-break fw-bold"></div>
|
||||
<div class="col-1 res-status overflow-hidden"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span></div>
|
||||
<div class="col-4 res-statusmsg"><span class="text-muted small">Getting status...</span></div>
|
||||
<div class="col-2 res-actions"></div>
|
||||
<div class="col-2 res-actions"><button class='btn btn-sm ms-2 visually-hidden'>Vertical-sizer</button></div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
@@ -172,7 +180,7 @@ function showResources(namespace, chart, revision) {
|
||||
//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"].includes(data.status.phase)) {
|
||||
if (["Available", "Active", "Established", "Bound", "Ready"].includes(data.status.phase)) {
|
||||
badge.addClass("bg-success text-dark")
|
||||
} else if (["Exists"].includes(data.status.phase)) {
|
||||
badge.addClass("bg-success text-dark bg-opacity-50")
|
||||
@@ -186,7 +194,7 @@ function showResources(namespace, chart, revision) {
|
||||
statusBlock.empty().append(badge).attr("title", data.status.phase)
|
||||
resBlock.find(".res-statusmsg").html("<span class='text-muted small'>" + (data.status.message ? data.status.message : '') + "</span>")
|
||||
|
||||
if (badge.text() !== "NotFound") {
|
||||
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
|
||||
resBlock.find(".res-actions")
|
||||
|
||||
const btn = $("<button class=\"btn btn-sm btn-white border-secondary\">Describe</button>");
|
||||
@@ -195,11 +203,13 @@ function showResources(namespace, chart, revision) {
|
||||
showDescribe(ns, res.kind, res.metadata.name, badge.clone())
|
||||
})
|
||||
|
||||
const btn2 = $("<button class='btn btn-sm btn-white border-secondary ms-2'>Scan</button>");
|
||||
resBlock.find(".res-actions").append(btn2)
|
||||
btn2.click(function () {
|
||||
scanResource(ns, res.kind, res.metadata.name, badge.clone())
|
||||
})
|
||||
if (scannableResKinds.has(res.kind)) {
|
||||
const btn2 = $("<button class='btn btn-sm btn-white border-secondary ms-2'>Scan</button>");
|
||||
resBlock.find(".res-actions").append(btn2)
|
||||
btn2.click(function () {
|
||||
scanResource(ns, res.kind, res.metadata.name, badge.clone())
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -237,25 +247,36 @@ function scanResource(ns, kind, name, badge) {
|
||||
body.append("No information from scanners. Make sure you have installed some and scanned object is supported.")
|
||||
}
|
||||
|
||||
const tabs = $('<ul class="nav nav-tabs mt-3" role="tablist"></ul>')
|
||||
const content = $('<div class="tab-content"></div>')
|
||||
|
||||
for (let name in data) {
|
||||
const res = data[name]
|
||||
|
||||
if (!res.OrigReport) continue
|
||||
const hdr = $("<h3>" + name + " Scan Results</h3>");
|
||||
if (!res.OrigReport && !res.PassedCount) continue
|
||||
|
||||
const hdr = $(`<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="` + name + `-tab" data-bs-toggle="tab" data-bs-target="#` + name + `-tab-pane" type="button" role="tab">` + name + `</button>
|
||||
</li>`)
|
||||
|
||||
if (res.FailedCount) {
|
||||
hdr.append("<span class='badge bg-danger ms-3'>" + res.FailedCount + " failed</span>")
|
||||
hdr.find('button').append("<span class='badge bg-danger ms-2'>" + res.FailedCount + " failed</span>")
|
||||
}
|
||||
|
||||
if (res.PassedCount) {
|
||||
hdr.append("<span class='badge bg-info ms-3'>" + res.PassedCount + " passed</span>")
|
||||
hdr.find('button').append("<span class='badge bg-info ms-2'>" + res.PassedCount + " passed</span>")
|
||||
}
|
||||
|
||||
body.append(hdr)
|
||||
|
||||
const hl = hljs.highlight(res.OrigReport, {language: 'yaml'}).value
|
||||
const pre = $("<pre class='bg-white rounded p-3' style='font-size: inherit; overflow: unset'></pre>").html(hl)
|
||||
body.append(pre)
|
||||
const div = $('<div class="tab-pane fade" id="' + name + '-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0"></div>').append(pre)
|
||||
|
||||
tabs.append(hdr)
|
||||
content.append(div)
|
||||
}
|
||||
|
||||
body.append(tabs)
|
||||
body.append(content)
|
||||
tabs.find('li').first().find('button').click()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,31 +5,16 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Helm Dashboard</title>
|
||||
<script src="static/datadog.js"></script>
|
||||
<script src="static/analytics.js"></script>
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto&family=Inter&family=Poppins:wght@600&family=Poppins:wght@500&family=Inter:wght@500&family=Roboto+Slab:wght@400&family=Roboto+Slab:wght@700&family=Roboto:wght@700&family=Roboto:wght@500"/>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.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-base.css" rel="stylesheet">
|
||||
<link href="static/styles.css" rel="stylesheet">
|
||||
<script type="text/javascript">
|
||||
window.heap = window.heap || [], heap.load = function (e, t) {
|
||||
window.heap.appid = e, window.heap.config = t = t || {};
|
||||
var r = document.createElement("script");
|
||||
r.type = "text/javascript", r.async = !0, r.src = "https://cdn.heapanalytics.com/js/heap-" + e + ".js";
|
||||
var a = document.getElementsByTagName("script")[0];
|
||||
a.parentNode.insertBefore(r, a);
|
||||
for (var n = function (e) {
|
||||
return function () {
|
||||
heap.push([e].concat(Array.prototype.slice.call(arguments, 0)))
|
||||
}
|
||||
}, p = ["addEventProperties", "addUserProperties", "clearEventProperties", "identify", "resetIdentity", "removeEventProperty", "setEventProperties", "track", "unsetEventProperty"], o = 0; o < p.length; o++) heap[p[o]] = n(p[o])
|
||||
};
|
||||
heap.load("3615793373");
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -37,11 +22,8 @@
|
||||
<!-- TOP BAR -->
|
||||
<nav class="navbar navbar-expand bg-white mb-0 p-0 b-shadow" id="topNav">
|
||||
<div class="container-fluid m-0 p-0">
|
||||
<div class="navbar-brand">
|
||||
<a href="/"><img src="static/logo.png" alt="Logo"></a>
|
||||
<div>
|
||||
<h1><a href="/">Helm Dashboard</a></h1>
|
||||
</div>
|
||||
<div class="navbar-brand me-0">
|
||||
<a href="/"><img src="static/logo-header.svg" alt="Helm Dashboard"></a>
|
||||
</div>
|
||||
|
||||
<div class="separator-vertical mx-3"><span></span></div>
|
||||
@@ -53,14 +35,43 @@
|
||||
<li class="nav-item mx-2">
|
||||
<a class="nav-link px-3 section-repo">Repository</a>
|
||||
</li>
|
||||
<li class="nav-item mx-2 dropdown">
|
||||
<a class="nav-link dropdown-toggle section-help" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
Help
|
||||
</a>
|
||||
<ul class="dropdown-menu fs-80">
|
||||
<li><a class="dropdown-item"
|
||||
href="https://join.slack.com/t/komodorkommunity/shared_invite/zt-1dm3cnkue-ov1Yh~_95teA35QNx5yuMg"
|
||||
target="_blank"><i class="bi-slack"></i> Support Chat</a></li>
|
||||
<li><a class="dropdown-item" href="https://github.com/komodorio/helm-dashboard" target="_blank"><i
|
||||
class="bi-github"></i> Project Page</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<!-- TODO: this should go under the "user menu" -->
|
||||
<button class="dropdown-item" id="cacheClear"><i
|
||||
class="bi-arrow-repeat"></i> Reset Cache
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item disabled" href="#">Version <span id="toolVersion"></span></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item mx-2 display-none upgrade-possible">
|
||||
<a class="nav-link position-relative text-danger"
|
||||
href="https://github.com/komodorio/helm-dashboard#installing" target="_blank">
|
||||
Upgrade to <span id="toolVersionUpgrade"></span>
|
||||
</a></li>
|
||||
|
||||
</ul>
|
||||
<div>
|
||||
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img
|
||||
src="https://komodor.com/wp-content/uploads/2021/05/favicon.png" alt="komodor.io"
|
||||
src="static/komodor-logo.svg" alt="komodor.io"
|
||||
style="height: 1.2rem; vertical-align: text-bottom; filter: grayscale(00%);"></a>
|
||||
|
||||
<a class="btn me-2 text-muted" href="https://github.com/komodorio/helm-dashboard"
|
||||
title="Project page on GitHub"><i class="bi-github"></i></a>
|
||||
</div>
|
||||
<div class="separator-vertical"><span></span></div>
|
||||
<i class="btn bi-power text-muted p-2 m-1 mx-2" title="Shut down the Helm Dashboard application"></i>
|
||||
@@ -88,15 +99,16 @@
|
||||
<button class="btn btn-sm btn-light bg-white border border-secondary btn-remove">
|
||||
<i class="bi-trash3"></i> Remove
|
||||
</button>
|
||||
<p class="my-3"><input class="form-control form-control-sm" type="text" placeholder="Filter..."
|
||||
id="inputSearch"></p>
|
||||
</div>
|
||||
<div><span class="text-muted small fw-bold me-3">REPOSITORY</span></div>
|
||||
<h2 class="mb-3">name-of-repo</h2>
|
||||
<div class="mb-5">
|
||||
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">URL: <span class="url fw-bold">http://somerepo/somepath</span></span>
|
||||
</div>
|
||||
<div class="py-2 mb-3 float-end">
|
||||
|
||||
<div class="float-end">
|
||||
<!-- TODO <input class="form-control form-control-sm" type="text" placeholder="Filter..."> -->
|
||||
</div>
|
||||
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
|
||||
style="text-transform: uppercase">
|
||||
@@ -105,7 +117,7 @@
|
||||
<div class="col-1">Version</div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
<ul class="list-unstyled mt-4"></ul>
|
||||
<ul class="list-unstyled mt-4 charts"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,11 +130,10 @@
|
||||
<ul class="list-unstyled" id="cluster">
|
||||
</ul>
|
||||
|
||||
<!-- TODO
|
||||
<h4 class="mt-4">Namespaces</h4>
|
||||
<ul class="list-unstyled" id="namespaces">
|
||||
<p id="limitNamespace" class="display-none ps-3"><span class="fw-bold"></span> (forced)</p>
|
||||
<ul class="list-unstyled" id="namespace">
|
||||
</ul>
|
||||
-->
|
||||
</form>
|
||||
</div>
|
||||
<!-- /FILTER BLOCK -->
|
||||
@@ -132,9 +143,12 @@
|
||||
<!-- INSTALLED LIST -->
|
||||
<div class="col ms-2" id="installedList">
|
||||
<div class="col rounded rounded-1 b-shadow header">
|
||||
<div class="bg-white rounded-top m-0">
|
||||
<div class="bg-white rounded-top m-0 spaced-out">
|
||||
<h2 class="m-0 p-1"><img class="m-2 mx-3 me-2" src="static/helm-gray.svg" alt="Installed Charts">Installed
|
||||
Charts (<span></span>)</h2>
|
||||
<div class="form-outline w-25">
|
||||
<input type="text" id="installedSearch" class="form-control form-control-sm" placeholder="Filter..." />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-secondary rounded-bottom m-0 row p-2">
|
||||
<div class="col-4 hdr-name">Name</div>
|
||||
@@ -147,6 +161,11 @@
|
||||
</div>
|
||||
|
||||
<div class="body"></div>
|
||||
<div class="bg-white rounded shadow p-3 display-none no-charts">Looks like you don't have any charts
|
||||
installed. "Repository" section may be a good place to start.
|
||||
</div>
|
||||
<div class="bg-white rounded shadow p-3 display-none all-filtered">There are no releases matching your filter criteria. Reset your filters or install more charts.
|
||||
</div>
|
||||
</div>
|
||||
<!-- /INSTALLED LIST -->
|
||||
</div>
|
||||
@@ -175,7 +194,11 @@
|
||||
title="Uninstall the chart"><i class="bi-trash3"></i> Uninstall
|
||||
</button>
|
||||
<br/>
|
||||
<a class="link small" id="btnUpgradeCheck">Check for new version
|
||||
<a class="link small" id="btnUpgradeCheck">
|
||||
<span class="spinner-border spinner-border-sm" style="display: none" role="status"
|
||||
aria-hidden="true"></span>
|
||||
</a>
|
||||
<a class="link small" id="btnAddRepository">
|
||||
<span class="spinner-border spinner-border-sm" style="display: none" role="status"
|
||||
aria-hidden="true"></span>
|
||||
</a>
|
||||
@@ -251,6 +274,9 @@
|
||||
data-mode="diff-rev">
|
||||
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>
|
||||
</form>
|
||||
</nav>
|
||||
|
||||
@@ -329,7 +355,7 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="upgradeModalLabel">
|
||||
Install <b class='text-success name'></b>
|
||||
<span class="type"></span> <b class='text-success name'></b>
|
||||
</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
@@ -344,7 +370,12 @@
|
||||
Release Name: <input class="form-control rel-name">
|
||||
</label>
|
||||
<label class="form-label me-4 text-dark">
|
||||
Namespace (optional): <input class="form-control rel-ns">
|
||||
Namespace (optional):
|
||||
<input type="text" class="form-control rel-ns" list="ns-datalist"/>
|
||||
<datalist id="ns-datalist"></datalist>
|
||||
</label>
|
||||
<label class="form-label me-4 text-dark">
|
||||
Cluster: <span class="form-label rel-cluster"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -374,7 +405,8 @@
|
||||
<div id="upgradeModalBody" class="small"></div>
|
||||
</form>
|
||||
<div class="modal-footer d-flex">
|
||||
<button type="button" class="btn btn-scan bg-white border-secondary">Scan for Problems</button>
|
||||
<button type="button" class="btn btn-scan bg-white border-secondary display-none">Scan for Problems
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-confirm">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,20 +4,39 @@ function loadChartsList() {
|
||||
const chartsCards = $("#installedList .body")
|
||||
chartsCards.empty().append("<div><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span> Loading...</div>")
|
||||
$.getJSON("/api/helm/charts").fail(function (xhr) {
|
||||
sendStats('Get releases', {'status': 'failed'});
|
||||
reportError("Failed to get list of charts", xhr)
|
||||
}).done(function (data) {
|
||||
chartsCards.empty()
|
||||
chartsCards.empty().hide()
|
||||
$("#installedList .header h2 span").text(data.length)
|
||||
const usedNS = {}
|
||||
data.forEach(function (elm) {
|
||||
let card = buildChartCard(elm);
|
||||
chartsCards.append(card)
|
||||
usedNS[elm.namespace] = usedNS[elm.namespace] ? usedNS[elm.namespace] + 1 : 1
|
||||
})
|
||||
sendStats('Get releases', {'status': 'success', length:data.length});
|
||||
filterInstalledList(chartsCards.find(".row"))
|
||||
$("#namespace li").each(function (ix, obj) {
|
||||
obj = $(obj)
|
||||
const objNS = obj.find("input").val();
|
||||
if (usedNS[objNS]) {
|
||||
obj.find("label .text-muted").text('['+usedNS[objNS]+']')
|
||||
obj.show()
|
||||
} else {
|
||||
obj.hide()
|
||||
}
|
||||
})
|
||||
chartsCards.show()
|
||||
if (!data.length) {
|
||||
$("#installedList .no-charts").show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function buildChartCard(elm) {
|
||||
const card = $(`<div class="row m-0 py-3 bg-white rounded-1 b-shadow border-4 border-start">
|
||||
<div class="col-4 rel-name"><span class="link">release-name</span><div></div></div>
|
||||
const card = $(`<div class="row m-0 py-4 bg-white rounded-1 b-shadow border-4 border-start link">
|
||||
<div class="col-4 rel-name"><span>release-name</span><div></div></div>
|
||||
<div class="col-3 rel-status"><span></span><div></div></div>
|
||||
<div class="col-2 rel-chart text-nowrap"><span></span><div>Chart Version</div></div>
|
||||
<div class="col-1 rel-rev"><span>#0</span><div>Revision</div></div>
|
||||
@@ -25,17 +44,49 @@ function buildChartCard(elm) {
|
||||
<div class="col-1 rel-date text-nowrap"><span>today</span><div>Updated</div></div>
|
||||
</div>`)
|
||||
|
||||
// semver2 regex , add optional v prefix
|
||||
const chartNameRegex = 'v?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?'
|
||||
const chartName = elm.chart.substring(0, elm.chart.match(chartNameRegex).index - 1)
|
||||
$.getJSON("/api/helm/repo/search?name=" + chartName).fail(function (xhr) {
|
||||
// we're ok if we can't show icon and description
|
||||
console.log("Failed to get repo name for charts", xhr)
|
||||
}).done(function (data) {
|
||||
if (data.length > 0) {
|
||||
$.getJSON("/api/helm/charts/show?name=" + data[0].name).fail(function (xhr) {
|
||||
console.log("Failed to get chart", xhr)
|
||||
}).done(function (data) {
|
||||
if (data) {
|
||||
const res = data[0];
|
||||
if (res.icon) {
|
||||
card.find(".rel-name").attr("style", "background-image: url(" + res.icon + ")")
|
||||
}
|
||||
if (res.description) {
|
||||
card.find(".rel-name div").text(res.description)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
card.find(".rel-name span").text(elm.name)
|
||||
card.find(".rel-rev span").text("#" + elm.revision)
|
||||
card.find(".rel-ns span").text(elm.namespace)
|
||||
card.find(".rel-chart span").text(elm.chart)
|
||||
card.find(".rel-date span").text(getAge(elm))
|
||||
|
||||
card.data("namespace", elm.namespace)
|
||||
card.data("name", elm.name)
|
||||
card.data("chart", elm.chart)
|
||||
|
||||
statusStyle(elm.status, card, card.find(".rel-status span"))
|
||||
|
||||
card.find("a").attr("href", '#context=' + getHashParam('context') + '&namespace=' + elm.namespace + '&name=' + elm.name)
|
||||
|
||||
card.find(".rel-name span").data("chart", elm).click(function () {
|
||||
card.data("chart", elm).click(function () {
|
||||
if (window.getSelection().toString()) {
|
||||
return
|
||||
}
|
||||
const self = $(this)
|
||||
$("#sectionList").hide()
|
||||
|
||||
@@ -48,3 +99,6 @@ function buildChartCard(elm) {
|
||||
return card;
|
||||
}
|
||||
|
||||
$("#installedSearch").keyup(function () {
|
||||
filterInstalledList($("#installedList .body .row"))
|
||||
})
|
||||
|
||||
16
pkg/dashboard/static/logo-header.svg
Normal file
16
pkg/dashboard/static/logo-header.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.5 KiB |
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="32"
|
||||
height="37"
|
||||
viewBox="0 0 32 37"
|
||||
width="179"
|
||||
height="164"
|
||||
viewBox="0 0 179 164"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
id="svg20"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
@@ -13,9 +13,9 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs8" />
|
||||
id="defs24" />
|
||||
<sodipodi:namedview
|
||||
id="namedview6"
|
||||
id="namedview22"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
@@ -23,27 +23,70 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="34.810811"
|
||||
inkscape:cx="13.228649"
|
||||
inkscape:cy="17.552019"
|
||||
inkscape:zoom="3.9268293"
|
||||
inkscape:cx="39.090062"
|
||||
inkscape:cy="109.24845"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2059"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg4" />
|
||||
inkscape:current-layer="svg20" />
|
||||
<rect
|
||||
style="opacity:0.455635;fill:#ffffff;stroke-width:18.0094;fill-opacity:1"
|
||||
id="rect1031"
|
||||
width="17.224331"
|
||||
height="10.929513"
|
||||
x="7.3572245"
|
||||
y="12.500892"
|
||||
ry="0" />
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:22.5327"
|
||||
id="rect967"
|
||||
width="113.34328"
|
||||
height="68.482964"
|
||||
x="32.284447"
|
||||
y="47.989918" />
|
||||
<path
|
||||
d="M106.129 93.4776C112.242 93.4776 117.336 86.7533 117.336 78.1951C117.336 69.637 112.242 62.9127 106.129 62.9127C100.016 62.9127 94.9216 69.637 94.9216 78.1951C94.9216 86.7533 100.016 93.4776 106.129 93.4776Z"
|
||||
fill="#1347FF"
|
||||
id="path2" />
|
||||
<path
|
||||
d="M84.1221 78.1951C84.1221 86.5495 79.0279 93.4776 72.915 93.4776C66.802 93.4776 61.7078 86.7533 61.7078 78.1951C61.7078 69.8408 66.802 62.9127 72.915 62.9127C79.0279 62.9127 84.1221 69.8408 84.1221 78.1951Z"
|
||||
fill="#1347FF"
|
||||
id="path4" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M18 5V2.29761C18 1.02617 17.1002 0 15.9958 0C14.8915 0 14 1.03576 14 2.29761V5H18ZM28.8285 11.364L30.7394 9.45308C31.6384 8.55404 31.7278 7.19222 30.9469 6.4113C30.166 5.63038 28.8032 5.73239 27.9109 6.62466L26.0001 8.53553L28.8285 11.364ZM1.62465 9.45308L3.53553 11.364L6.36396 8.53553L4.45308 6.62466C3.56081 5.73239 2.19804 5.63038 1.41712 6.4113C0.636208 7.19222 0.72561 8.55404 1.62465 9.45308ZM6 12.8164L7.78385 11H24.2163L26 12.8085V23.1915L24.2163 25H7.78372L6 23.1915V12.8164ZM8.94831 12.977L8.25128 13.6683V22.3438L8.94831 23.0351H23.0733L23.7446 22.3426V13.6694L23.0733 12.977H8.94831ZM20.1736 17.4485C20.1736 18.7679 19.4222 19.8375 18.4953 19.8375C17.5684 19.8375 16.817 18.7679 16.817 17.4485C16.817 16.1291 17.5684 15.0595 18.4953 15.0595C19.4222 15.0595 20.1736 16.1291 20.1736 17.4485ZM13.5045 19.8375C14.4314 19.8375 15.1828 18.7679 15.1828 17.4485C15.1828 16.1291 14.4314 15.0595 13.5045 15.0595C12.5776 15.0595 11.8262 16.1291 11.8262 17.4485C11.8262 18.7679 12.5776 19.8375 13.5045 19.8375ZM14.3641 34.0662V31.3638H18.3641V34.0662C18.3641 35.328 17.4726 36.3638 16.3682 36.3638C15.2638 36.3638 14.3641 35.3376 14.3641 34.0662ZM1.62465 26.9107L3.53553 24.9998L6.36396 27.8282L4.45308 29.7391C3.56081 30.6314 2.19804 30.7334 1.41712 29.9525C0.636204 29.1716 0.725608 27.8097 1.62465 26.9107ZM28.8285 24.9998L30.7394 26.9107C31.6384 27.8097 31.7278 29.1716 30.9469 29.9525C30.166 30.7334 28.8032 30.6314 27.9109 29.7391L26.0001 27.8282L28.8285 24.9998Z"
|
||||
d="M22.5848 48.8529L34.607 37.2383H144.437L156.459 48.6492V114.873L144.437 126.488H34.607L22.5848 114.873V48.8529ZM42.3501 49.8717L37.6635 54.3546V109.575L42.3501 113.854H136.897L141.38 109.371V54.1508L136.897 49.668H42.3501V49.8717Z"
|
||||
fill="#1347FF"
|
||||
id="path2" />
|
||||
id="path6" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M175.817 28.68L167.87 36.8306L155.848 25.2159L163.794 17.0653C172.353 8.09963 184.579 19.918 175.817 28.68Z"
|
||||
fill="#1347FF"
|
||||
id="path8" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M97.7744 9.32228V20.5294H81.0656V9.32228C81.0656 -3.10743 97.9781 -3.10743 97.7744 9.32228Z"
|
||||
fill="#1347FF"
|
||||
id="path10" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M81.2693 154.2V142.993H97.9781V154.2C97.9781 166.629 81.0655 166.629 81.2693 154.2Z"
|
||||
fill="#1347FF"
|
||||
id="path12" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.2493 17.0653L23.1961 25.2159L11.1739 36.8306L3.22708 28.68C-5.53484 19.918 6.6911 8.09963 15.2493 17.0653Z"
|
||||
fill="#1347FF"
|
||||
id="path14" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.0233 134.842L10.9702 126.691L22.9923 138.306L15.0455 146.457C6.48732 155.422 -5.73862 143.604 3.0233 134.842Z"
|
||||
fill="#1347FF"
|
||||
id="path16" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M163.591 146.457L155.644 138.306L167.666 126.691L175.613 134.842C184.375 143.604 172.149 155.422 163.591 146.457Z"
|
||||
fill="#1347FF"
|
||||
id="path18" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -4,9 +4,11 @@ function loadRepoView() {
|
||||
|
||||
$.getJSON("/api/helm/repo").fail(function (xhr) {
|
||||
reportError("Failed to get list of repositories", xhr)
|
||||
sendStats('Get repo', {'status': 'fail'});
|
||||
}).done(function (data) {
|
||||
const items = $("#sectionRepo .repo-list ul").empty()
|
||||
|
||||
data.sort((a, b) => (a.name > b.name) - (a.name < b.name))
|
||||
|
||||
data.forEach(function (elm) {
|
||||
let opt = $('<li class="mb-2"><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
|
||||
opt.attr('title', elm.url)
|
||||
@@ -18,8 +20,9 @@ function loadRepoView() {
|
||||
if (!data.length) {
|
||||
items.text("No repositories found, try adding one")
|
||||
}
|
||||
|
||||
sendStats('Get repo', {'status': 'success', length:data.length});
|
||||
items.find("input").click(function () {
|
||||
$("#inputSearch").val('')
|
||||
const self = $(this)
|
||||
const elm = self.data("item");
|
||||
setHashParam("repo", elm.name)
|
||||
@@ -61,6 +64,19 @@ function loadRepoView() {
|
||||
})
|
||||
}
|
||||
|
||||
$("#inputSearch").keyup(function () {
|
||||
let val = $(this).val().toLowerCase();
|
||||
|
||||
$(".charts li").each(function () {
|
||||
let chartName = $(this.firstElementChild).text().toLowerCase()
|
||||
if (chartName.indexOf(val) >= 0) {
|
||||
$(this).show()
|
||||
} else {
|
||||
$(this).hide()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
$("#sectionRepo .repo-list .btn").click(function () {
|
||||
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
|
||||
myModal.show()
|
||||
@@ -115,6 +131,8 @@ function repoChartClicked() {
|
||||
setHashParam("chart", elm.installed_name)
|
||||
window.location.reload()
|
||||
} else {
|
||||
popUpUpgrade(elm)
|
||||
const contexts = $("body").data("contexts")
|
||||
const contextNamespace = contexts.filter(obj => {return obj.Name === getHashParam("context")})[0].Namespace
|
||||
popUpUpgrade(elm, contextNamespace)
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ function fillChartHistory(data, namespace, name) {
|
||||
$("#specRev").val(elm.revision).data("last-rev", elm.revision).data("last-chart-ver", elm.chart_ver)
|
||||
}
|
||||
|
||||
const rev = $(`<li class="px-2 pt-5 pb-4 mb-2 rounded border border-secondary bg-secondary position-relative">
|
||||
const rev = $(`<li class="px-2 pt-5 pb-4 mb-2 rounded border border-secondary bg-secondary position-relative link">
|
||||
<div class="rev-status position-absolute top-0 m-2 mb-5 start-0 fw-bold"></div>
|
||||
<div class="rev-number position-absolute top-0 m-2 mb-5 end-0 fw-bold fs-6"></div>
|
||||
<div class="rev-changes position-absolute bottom-0 start-0 m-2 text-muted small"></div>
|
||||
@@ -48,22 +48,26 @@ function fillChartHistory(data, namespace, name) {
|
||||
|
||||
if (elm.description.startsWith("Rollback to ")) {
|
||||
//rev.find(".rev-status").append(" <span class='small fw-normal text-lowercase'>(rollback)</span>")
|
||||
rev.find(".rev-status").append(" <i class='bi-arrow-counterclockwise text-muted' title='"+elm.description+"'></i>")
|
||||
rev.find(".rev-status").append(" <i class='bi-arrow-counterclockwise text-muted' title='" + elm.description + "'></i>")
|
||||
}
|
||||
|
||||
const nxt = data[x + 1];
|
||||
if (nxt && isNewerVersion(elm.chart_ver, nxt.chart_ver)) {
|
||||
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='text-danger bi-arrow-down-right'></i> " + elm.chart_ver)
|
||||
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='bi-arrow-down-right'></i> " + elm.chart_ver)
|
||||
} else if (nxt && isNewerVersion(nxt.chart_ver, elm.chart_ver)) {
|
||||
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='text-success bi-arrow-up-right'></i> " + elm.chart_ver)
|
||||
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='bi-arrow-up-right'></i> " + elm.chart_ver)
|
||||
}
|
||||
|
||||
rev.data("elm", elm)
|
||||
rev.addClass("rev-" + elm.revision)
|
||||
rev.click(function () {
|
||||
if (window.getSelection().toString()) {
|
||||
return
|
||||
}
|
||||
revisionClicked(namespace, name, $(this))
|
||||
})
|
||||
|
||||
// revRow.attr("class", "link")
|
||||
revRow.append(rev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,72 @@
|
||||
$(function () {
|
||||
const clusterSelect = $("#cluster");
|
||||
clusterSelect.change(function () {
|
||||
window.location.href = "/#context=" + clusterSelect.find("input:radio:checked").val()
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
$.getJSON("/api/kube/contexts").fail(function (xhr) {
|
||||
reportError("Failed to get list of clusters", xhr)
|
||||
let limNS = null
|
||||
$.getJSON("/status").fail(function (xhr) { // maybe /options call in the future
|
||||
reportError("Failed to get tool version", xhr)
|
||||
}).done(function (data) {
|
||||
const context = getHashParam("context")
|
||||
fillClusterList(data, context);
|
||||
|
||||
initView(); // can only do it after loading cluster list
|
||||
fillToolVersion(data)
|
||||
limNS = data.LimitedToNamespace
|
||||
if (limNS) {
|
||||
$("#limitNamespace").show().find("span").text(limNS)
|
||||
}
|
||||
fillClusters(limNS)
|
||||
})
|
||||
|
||||
$.getJSON("/api/scanners").fail(function (xhr) {
|
||||
reportError("Failed to get list of scanners", xhr)
|
||||
}).done(function (data) {
|
||||
if (!data.length) {
|
||||
$("#upgradeModal .btn-scan").hide()
|
||||
$("body").data("scanners", data)
|
||||
for (let k in data) {
|
||||
if (data[k].ManifestScannable) {
|
||||
$("#upgradeModal .btn-scan").show() // TODO: move this to install flow
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function fillClusters(limNS) {
|
||||
const clusterSelect = $("#cluster");
|
||||
clusterSelect.change(function () {
|
||||
window.location.href = "/#context=" + clusterSelect.find("input:radio:checked").val()
|
||||
window.location.reload()
|
||||
})
|
||||
const namespaceSelect = $("#namespace");
|
||||
namespaceSelect.change(function () {
|
||||
let filteredNamespaces = []
|
||||
namespaceSelect.find("input:checkbox:checked").each(function () {
|
||||
filteredNamespaces.push($(this).val());
|
||||
})
|
||||
setFilteredNamespaces(filteredNamespaces)
|
||||
filterInstalledList($("#installedList .body .row"))
|
||||
})
|
||||
|
||||
$.getJSON("/api/kube/contexts").fail(function (xhr) {
|
||||
sendStats('contexts', {'status': 'fail'});
|
||||
reportError("Failed to get list of clusters", xhr)
|
||||
}).done(function (data) {
|
||||
$("body").data("contexts", data)
|
||||
const context = getHashParam("context")
|
||||
data.sort((a, b) => (getCleanClusterName(a.Name) > getCleanClusterName(b.Name)) - (getCleanClusterName(a.Name) < getCleanClusterName(b.Name)))
|
||||
fillClusterList(data, context);
|
||||
sendStats('contexts', {'status': 'success', length:data.length});
|
||||
$.getJSON("/api/kube/namespaces").fail(function (xhr) {
|
||||
reportError("Failed to get namespaces", xhr)
|
||||
}).done(function (res) {
|
||||
const ns = res.items.map(i => i.metadata.name)
|
||||
$.each(ns, function (i, item) {
|
||||
$("#upgradeModal #ns-datalist").append($("<option>", {
|
||||
value: item,
|
||||
text: item
|
||||
}))
|
||||
})
|
||||
if (!limNS) {
|
||||
fillNamespaceList(res.items)
|
||||
}
|
||||
}).always(function () {
|
||||
initView(); // can only do it after loading cluster and namespace lists
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$(".section").hide()
|
||||
|
||||
@@ -44,17 +88,24 @@ function initView() {
|
||||
|
||||
$("#topNav ul a").click(function () {
|
||||
const self = $(this)
|
||||
if (self.hasClass("section-help")) {
|
||||
return;
|
||||
}
|
||||
|
||||
$("#topNav ul a").removeClass("active")
|
||||
|
||||
const ctx = getHashParam("context")
|
||||
const filteredNamespace = getHashParam("filteredNamespace")
|
||||
setHashParam(null, null)
|
||||
setHashParam("context", ctx)
|
||||
setHashParam("filteredNamespace", filteredNamespace)
|
||||
|
||||
if (self.hasClass("section-repo")) {
|
||||
setHashParam("section", "repository")
|
||||
} else {
|
||||
} else if (self.hasClass("section-installed")) {
|
||||
setHashParam("section", null)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
initView()
|
||||
@@ -116,28 +167,33 @@ function getCleanClusterName(rawClusterName) {
|
||||
if (rawClusterName.indexOf('arn') === 0) {
|
||||
// AWS cluster
|
||||
const clusterSplit = rawClusterName.split(':')
|
||||
const clusterName = clusterSplit.at(-1).split("/").at(-1)
|
||||
const clusterName = clusterSplit.slice(-1)[0].replace('cluster/', '')
|
||||
const region = clusterSplit.at(-3)
|
||||
return region + "/" + clusterName + ' [AWS]'
|
||||
}
|
||||
|
||||
if (rawClusterName.indexOf('gke') === 0) {
|
||||
// GKE cluster
|
||||
return rawClusterName.split('_').at(-2) + '/' + rawClusterName.split('_').at(-1) + ' [GKE]'
|
||||
}
|
||||
|
||||
return rawClusterName
|
||||
}
|
||||
|
||||
function fillClusterList(data, context) {
|
||||
if (!data || !data.length) {
|
||||
$("#cluster").append("No clusters listed in kubectl config, please configure some")
|
||||
return
|
||||
}
|
||||
data.forEach(function (elm) {
|
||||
let label = getCleanClusterName(elm.Name)
|
||||
let opt = $('<li><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
|
||||
opt.attr('title', elm.Name)
|
||||
opt.find("input").val(elm.Name).text(label)
|
||||
opt.find("span").text(label)
|
||||
if (elm.IsCurrent && !context) {
|
||||
opt.find("input").prop("checked", true)
|
||||
setCurrentContext(elm.Name)
|
||||
} else if (context && elm.Name === context) {
|
||||
const isCurrent = elm.IsCurrent && !context;
|
||||
const isSelected = context && elm.Name === context
|
||||
if (isCurrent || isSelected) {
|
||||
opt.find("input").prop("checked", true)
|
||||
setCurrentContext(elm.Name)
|
||||
}
|
||||
@@ -145,6 +201,33 @@ function fillClusterList(data, context) {
|
||||
})
|
||||
}
|
||||
|
||||
function fillNamespaceList(data) {
|
||||
const curContextNamespaces = $("body").data("contexts").filter(obj => {
|
||||
return obj.IsCurrent
|
||||
})
|
||||
|
||||
if (!data || !data.length) {
|
||||
$("#namespace").append("default")
|
||||
return
|
||||
}
|
||||
Array.from(data).forEach(function (elm) {
|
||||
const filteredNamespace = getHashParam("filteredNamespace")
|
||||
let opt = $('<li class="display-none"><label><input type="checkbox" name="namespace" class="me-2"/><span></span><span class="text-muted ms-2"></span></label></li>');
|
||||
opt.attr('title', elm.metadata.name)
|
||||
opt.find("input").val(elm.metadata.name).text(elm.metadata.name)
|
||||
opt.find("span").text(elm.metadata.name)
|
||||
if (filteredNamespace) {
|
||||
if (filteredNamespace.split('+').includes(elm.metadata.name)) {
|
||||
opt.find("input").prop("checked", true)
|
||||
}
|
||||
} else if (curContextNamespaces && curContextNamespaces[0].Namespace === elm.metadata.name) {
|
||||
opt.find("input").prop("checked", true)
|
||||
setFilteredNamespaces([elm.metadata.name])
|
||||
}
|
||||
$("#namespace").append(opt)
|
||||
})
|
||||
}
|
||||
|
||||
function setCurrentContext(ctx) {
|
||||
setHashParam("context", ctx)
|
||||
$.ajaxSetup({
|
||||
@@ -181,11 +264,20 @@ $(".bi-power").click(function () {
|
||||
url: "/",
|
||||
type: 'DELETE',
|
||||
}).done(function () {
|
||||
// TODO: display explanation overlay here
|
||||
window.close();
|
||||
})
|
||||
})
|
||||
|
||||
function isNewerVersion(oldVer, newVer) {
|
||||
if (oldVer && oldVer[0] === 'v') {
|
||||
oldVer = oldVer.substring(1)
|
||||
}
|
||||
|
||||
if (newVer && newVer[0] === 'v') {
|
||||
newVer = newVer.substring(1)
|
||||
}
|
||||
|
||||
const oldParts = oldVer.split('.')
|
||||
const newParts = newVer.split('.')
|
||||
for (let i = 0; i < newParts.length; i++) {
|
||||
@@ -195,4 +287,60 @@ function isNewerVersion(oldVer, newVer) {
|
||||
if (a < b) return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function fillToolVersion(data) {
|
||||
$("#toolVersion").text(data.CurVer)
|
||||
if (isNewerVersion(data.CurVer, data.LatestVer)) {
|
||||
$("#toolVersionUpgrade").text(data.LatestVer)
|
||||
$(".upgrade-possible").show()
|
||||
}
|
||||
}
|
||||
|
||||
$("#cacheClear").click(function () {
|
||||
$.ajax({
|
||||
url: "/api/cache",
|
||||
type: 'DELETE',
|
||||
}).done(function () {
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
|
||||
function showHideInstalledRelease(card, filteredNamespaces, filterStr) {
|
||||
let releaseNamespace = card.data("namespace")
|
||||
let releaseName = card.data("name")
|
||||
let chartName = card.data("chart").chart
|
||||
const shownByNS = !filteredNamespaces || filteredNamespaces.split('+').includes(releaseNamespace);
|
||||
const shownByStr = releaseName.indexOf(filterStr) >= 0 || chartName.indexOf(filterStr) >= 0
|
||||
if (shownByNS && shownByStr) {
|
||||
card.show()
|
||||
return true
|
||||
} else {
|
||||
card.hide()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function filterInstalledList(list) {
|
||||
const warnMsg = $("#installedList .all-filtered").hide();
|
||||
|
||||
let filterStr = $("#installedSearch").val().toLowerCase();
|
||||
let filteredNamespaces = getHashParam("filteredNamespace")
|
||||
let anyShown = false;
|
||||
list.each(function (ix, card) {
|
||||
anyShown |= showHideInstalledRelease($(card), filteredNamespaces, filterStr)
|
||||
})
|
||||
|
||||
if (list.length && !anyShown) {
|
||||
warnMsg.show()
|
||||
}
|
||||
}
|
||||
|
||||
function setFilteredNamespaces(filteredNamespaces) {
|
||||
if (filteredNamespaces.length === 0 && getHashParam("filteredNamespace")) {
|
||||
setHashParam("filteredNamespace")
|
||||
} else if (filteredNamespaces.length !== 0) {
|
||||
setHashParam("filteredNamespace", filteredNamespaces.join('+'))
|
||||
}
|
||||
}
|
||||
81
pkg/dashboard/static/styles-base.css
Normal file
81
pkg/dashboard/static/styles-base.css
Normal file
@@ -0,0 +1,81 @@
|
||||
.link, .nav-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.strike {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #707583 !important;
|
||||
}
|
||||
|
||||
.border-other {
|
||||
border-color: #9195A1 !important;
|
||||
}
|
||||
|
||||
.text-other {
|
||||
color: #9195A1 !important;
|
||||
}
|
||||
|
||||
.border-failed {
|
||||
border-color: #FC1683 !important;
|
||||
}
|
||||
|
||||
.text-failed {
|
||||
color: #FC1683 !important;
|
||||
}
|
||||
|
||||
.border-deployed {
|
||||
border-color: #1BE99A !important;
|
||||
}
|
||||
|
||||
.text-deployed {
|
||||
color: #1FA470 !important;
|
||||
}
|
||||
|
||||
.border-pending {
|
||||
border-color: #5AB0FF !important;
|
||||
}
|
||||
|
||||
.text-pending {
|
||||
color: #5AB0FF !important;
|
||||
}
|
||||
|
||||
.bg-tag {
|
||||
background-color: #D6EFFE;
|
||||
}
|
||||
|
||||
.bg-tag.text-dark {
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: #FC1683 !important;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: #A4F8D7 !important;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #1347FF;
|
||||
}
|
||||
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.offcanvas {
|
||||
width: auto !important;
|
||||
max-width: 90%;
|
||||
min-width: 60%;
|
||||
}
|
||||
|
||||
.fs-80 {
|
||||
font-size: 0.8rem!important;
|
||||
}
|
||||
@@ -1,81 +1,3 @@
|
||||
.link, .nav-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.strike {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #707583 !important;
|
||||
}
|
||||
|
||||
.border-other {
|
||||
border-color: #9195A1 !important;
|
||||
}
|
||||
|
||||
.text-other {
|
||||
color: #9195A1 !important;
|
||||
}
|
||||
|
||||
.border-failed {
|
||||
border-color: #FC1683 !important;
|
||||
}
|
||||
|
||||
.text-failed {
|
||||
color: #FC1683 !important;
|
||||
}
|
||||
|
||||
.border-deployed {
|
||||
border-color: #1BE99A !important;
|
||||
}
|
||||
|
||||
.text-deployed {
|
||||
color: #1FA470 !important;
|
||||
}
|
||||
|
||||
.border-pending {
|
||||
border-color: #5AB0FF !important;
|
||||
}
|
||||
|
||||
.text-pending {
|
||||
color: #5AB0FF !important;
|
||||
}
|
||||
|
||||
.bg-tag {
|
||||
background-color: #D6EFFE;
|
||||
}
|
||||
|
||||
.bg-tag.text-dark {
|
||||
color: #333333 !important;
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: #FC1683 !important;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: #A4F8D7 !important;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #1347FF;
|
||||
}
|
||||
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.offcanvas {
|
||||
width: auto !important;
|
||||
max-width: 90%;
|
||||
min-width: 60%;
|
||||
}
|
||||
|
||||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
@@ -104,22 +26,11 @@ body > .container-fluid {
|
||||
min-height: 100% !important;
|
||||
}
|
||||
|
||||
#topNav.navbar {
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-family: Poppins, serif;
|
||||
font-size: 0.6rem !important;
|
||||
color: #707583;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar-brand > a > img {
|
||||
vertical-align: middle;
|
||||
height: 2.5rem;
|
||||
height: 3rem;
|
||||
display: inline-block;
|
||||
margin: 0.5rem 0.8rem
|
||||
margin: 0.25rem 0.75rem
|
||||
}
|
||||
|
||||
.navbar-brand > div {
|
||||
@@ -156,6 +67,10 @@ body > .container-fluid {
|
||||
color: #3B3D45 !important;
|
||||
}
|
||||
|
||||
#topNav .nav-link.text-danger {
|
||||
color: #FC1683 !important;
|
||||
}
|
||||
|
||||
#topNav .nav-link.active {
|
||||
background: #EBEFFF;
|
||||
border-radius: 2px;
|
||||
@@ -188,6 +103,13 @@ body > .container-fluid {
|
||||
margin-bottom: 0.75rem !important;
|
||||
}
|
||||
|
||||
#installedList .header .spaced-out {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
#installedList h2 {
|
||||
font-family: Inter, serif;
|
||||
font-weight: 700;
|
||||
@@ -234,6 +156,10 @@ span.link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#installedList .body .row div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#installedList .rel-name {
|
||||
padding-left: 5.5rem;
|
||||
background-image: url("helm-gray-50.svg");
|
||||
@@ -243,6 +169,10 @@ span.link {
|
||||
background-size: 3rem;
|
||||
}
|
||||
|
||||
#installedList .rel-name div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#installedList .rel-name span {
|
||||
font-family: Roboto Slab, sans-serif;
|
||||
font-weight: 700;
|
||||
@@ -288,23 +218,23 @@ span.link {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
nav .nav-tabs {
|
||||
border: none;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
nav .nav-tabs .nav-link {
|
||||
padding-bottom: 0.25rem;
|
||||
color: #3B3D45;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
nav .nav-tabs .nav-link.active {
|
||||
border: none;
|
||||
border-bottom: 3px solid #3B3D45;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#installedList .b-shadow:hover {
|
||||
#installedList .body .b-shadow:hover {
|
||||
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
|
||||
100
pkg/dashboard/subproc/cache.go
Normal file
100
pkg/dashboard/subproc/cache.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package subproc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/eko/gocache/v3/marshaler"
|
||||
"github.com/eko/gocache/v3/store"
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CacheKey = string
|
||||
|
||||
const CacheKeyRelList CacheKey = "installed-releases-list"
|
||||
const CacheKeyShowChart CacheKey = "show-chart"
|
||||
const CacheKeyRelHistory CacheKey = "release-history"
|
||||
const CacheKeyRevManifests CacheKey = "rev-manifests"
|
||||
const CacheKeyRevNotes CacheKey = "rev-notes"
|
||||
const CacheKeyRevValues CacheKey = "rev-values"
|
||||
const CacheKeyRepoChartValues CacheKey = "chart-values"
|
||||
const CacheKeyAllRepos CacheKey = "all-repos"
|
||||
|
||||
type Cache struct {
|
||||
Marshaler *marshaler.Marshaler `json:"-"`
|
||||
HitCount int
|
||||
MissCount int
|
||||
}
|
||||
|
||||
func NewCache() *Cache {
|
||||
gocacheClient := gocache.New(60*time.Minute, 10*time.Minute)
|
||||
gocacheStore := store.NewGoCache(gocacheClient)
|
||||
|
||||
// TODO: use tiered cache with some disk backend, allow configuring that static cache folder
|
||||
|
||||
// Initializes marshaler
|
||||
marshal := marshaler.New(gocacheStore)
|
||||
return &Cache{
|
||||
Marshaler: marshal,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) String(key CacheKey, tags []string, callback func() (string, error)) (string, error) {
|
||||
if tags == nil {
|
||||
tags = make([]string, 0)
|
||||
}
|
||||
tags = append(tags, key)
|
||||
|
||||
ctx := context.Background()
|
||||
out := ""
|
||||
_, err := c.Marshaler.Get(ctx, key, &out)
|
||||
if err == nil {
|
||||
log.Debugf("Using cached value for %s", key)
|
||||
c.HitCount++
|
||||
return out, nil
|
||||
} else if !errors.Is(err, store.NotFound{}) {
|
||||
return "", err
|
||||
}
|
||||
c.MissCount++
|
||||
|
||||
out, err = callback()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = c.Marshaler.Set(ctx, key, out, store.WithTags(tags))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Cache) Invalidate(tags ...CacheKey) {
|
||||
log.Debugf("Invalidating tags %v", tags)
|
||||
err := c.Marshaler.Invalidate(context.Background(), store.WithInvalidateTags(tags))
|
||||
if err != nil {
|
||||
log.Warnf("Failed to invalidate tags %v: %s", tags, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) Clear() error {
|
||||
c.HitCount = 0
|
||||
c.MissCount = 0
|
||||
return c.Marshaler.Clear(context.Background())
|
||||
}
|
||||
|
||||
func cacheTagRelease(namespace string, name string) CacheKey {
|
||||
return "release" + "\v" + namespace + "\v" + name
|
||||
}
|
||||
func cacheTagRepoVers(chartName string) CacheKey {
|
||||
return "repo-versions" + "\v" + chartName
|
||||
}
|
||||
|
||||
func cacheTagRepoCharts(name string) CacheKey {
|
||||
return "repo-charts" + "\v" + name
|
||||
}
|
||||
|
||||
func cacheTagRepoName(name string) CacheKey {
|
||||
return "repo-name" + "\v" + name
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hexops/gotextdiff"
|
||||
"github.com/hexops/gotextdiff/myers"
|
||||
"github.com/hexops/gotextdiff/span"
|
||||
@@ -13,11 +17,6 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DataLayer struct {
|
||||
@@ -25,10 +24,26 @@ type DataLayer struct {
|
||||
Helm string
|
||||
Kubectl string
|
||||
Scanners []Scanner
|
||||
StatusInfo *StatusInfo
|
||||
Namespace string
|
||||
Cache *Cache
|
||||
}
|
||||
|
||||
type StatusInfo struct {
|
||||
CurVer string
|
||||
LatestVer string
|
||||
Analytics bool
|
||||
LimitedToNamespace string
|
||||
CacheHitRatio float64
|
||||
}
|
||||
|
||||
func (d *DataLayer) runCommand(cmd ...string) (string, error) {
|
||||
log.Debugf("Starting command: %s", cmd)
|
||||
for i, c := range cmd {
|
||||
// TODO: remove namespace parameter if it's empty
|
||||
if c == "--namespace" && i < len(cmd) { // TODO: in case it's not found - add it?
|
||||
d.forceNamespace(&cmd[i+1])
|
||||
}
|
||||
}
|
||||
|
||||
return utils.RunCommand(cmd, map[string]string{"HELM_KUBECONTEXT": d.KubeContext})
|
||||
}
|
||||
@@ -46,19 +61,10 @@ func (d *DataLayer) runCommandHelm(cmd ...string) (string, error) {
|
||||
return d.runCommand(cmd...)
|
||||
}
|
||||
|
||||
func (d *DataLayer) runCommandKubectl(cmd ...string) (string, error) {
|
||||
// TODO: migrate into using kubectl "k8s.io/kubectl/pkg/cmd" and kube API
|
||||
if d.Kubectl == "" {
|
||||
d.Kubectl = "kubectl"
|
||||
func (d *DataLayer) forceNamespace(s *string) {
|
||||
if d.Namespace != "" {
|
||||
*s = d.Namespace
|
||||
}
|
||||
|
||||
cmd = append([]string{d.Kubectl}, cmd...)
|
||||
|
||||
if d.KubeContext != "" {
|
||||
cmd = append(cmd, "--context", d.KubeContext)
|
||||
}
|
||||
|
||||
return d.runCommand(cmd...)
|
||||
}
|
||||
|
||||
func (d *DataLayer) CheckConnectivity() error {
|
||||
@@ -79,51 +85,29 @@ func (d *DataLayer) CheckConnectivity() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type KubeContext struct {
|
||||
IsCurrent bool
|
||||
Name string
|
||||
Cluster string
|
||||
AuthInfo string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
func (d *DataLayer) ListContexts() (res []KubeContext, err error) {
|
||||
out, err := d.runCommandKubectl("config", "get-contexts")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (d *DataLayer) GetStatus() *StatusInfo {
|
||||
sum := float64(d.Cache.HitCount + d.Cache.MissCount)
|
||||
if sum > 0 {
|
||||
d.StatusInfo.CacheHitRatio = float64(d.Cache.HitCount) / sum
|
||||
} else {
|
||||
d.StatusInfo.CacheHitRatio = 0
|
||||
}
|
||||
|
||||
// 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
|
||||
return d.StatusInfo
|
||||
}
|
||||
|
||||
func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) {
|
||||
cmd := []string{"ls", "--all", "--output", "json", "--time-format", time.RFC3339}
|
||||
|
||||
// TODO: filter by namespace
|
||||
out, err := d.runCommandHelm("ls", "--all", "--all-namespaces", "--output", "json", "--time-format", time.RFC3339)
|
||||
if d.Namespace == "" {
|
||||
cmd = append(cmd, "--all-namespaces")
|
||||
} else {
|
||||
cmd = append(cmd, "--namespace", d.Namespace)
|
||||
}
|
||||
|
||||
out, err := d.Cache.String(CacheKeyRelList, nil, func() (string, error) {
|
||||
return d.runCommandHelm(cmd...)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -135,9 +119,13 @@ func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*HistoryElement, err error) {
|
||||
func (d *DataLayer) ReleaseHistory(namespace string, releaseName string) (res []*HistoryElement, err error) {
|
||||
// TODO: there is `max` but there is no `offset`
|
||||
out, err := d.runCommandHelm("history", chartName, "--namespace", namespace, "--output", "json", "--max", "18")
|
||||
ct := cacheTagRelease(namespace, releaseName)
|
||||
out, err := d.Cache.String(CacheKeyRelHistory+ct, []string{ct}, func() (string, error) {
|
||||
return d.runCommandHelm("history", releaseName, "--namespace", namespace, "--output", "json")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -152,7 +140,7 @@ func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*His
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elm.ChartName = chartRepoName
|
||||
elm.ChartName = chartRepoName // TODO: move it to frontend?
|
||||
elm.ChartVer = curVer
|
||||
elm.Updated.Time = elm.Updated.Time.Round(time.Second)
|
||||
}
|
||||
@@ -160,78 +148,15 @@ func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*His
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoVersions(chartName string) (res []*RepoChartElement, err error) {
|
||||
search := "/" + chartName + "\v"
|
||||
if strings.Contains(chartName, "/") {
|
||||
search = "\v" + chartName + "\v"
|
||||
}
|
||||
|
||||
cmd := []string{"search", "repo", "--regexp", search, "--versions", "--output", "json"}
|
||||
out, err := d.runCommandHelm(cmd...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoCharts(repoName string) (res []*RepoChartElement, err error) {
|
||||
cmd := []string{"search", "repo", "--regexp", "\v" + repoName + "/", "--output", "json"}
|
||||
out, err := d.runCommandHelm(cmd...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ins, err := d.ListInstalled()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enrichRepoChartsWithInstalled(res, ins)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func enrichRepoChartsWithInstalled(charts []*RepoChartElement, installed []ReleaseElement) {
|
||||
for _, chart := range charts {
|
||||
for _, rel := range installed {
|
||||
c, _, err := utils.ChartAndVersion(rel.Chart)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to parse chart: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
pieces := strings.Split(chart.Name, "/")
|
||||
if pieces[1] == c {
|
||||
chart.InstalledNamespace = rel.Namespace
|
||||
chart.InstalledName = rel.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SectionFn = func(string, string, int, bool) (string, error) // TODO: rework it into struct-based argument?
|
||||
|
||||
func (d *DataLayer) RevisionManifests(namespace string, chartName string, revision int, _ bool) (res string, err error) {
|
||||
cmd := []string{"get", "manifest", chartName, "--namespace", namespace}
|
||||
if revision > 0 {
|
||||
cmd = append(cmd, "--revision", strconv.Itoa(revision))
|
||||
}
|
||||
cmd := []string{"get", "manifest", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision)}
|
||||
|
||||
out, err := d.runCommandHelm(cmd...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
key := CacheKeyRevManifests + "\v" + namespace + "\v" + chartName + "\v" + strconv.Itoa(revision)
|
||||
return d.Cache.String(key, nil, func() (string, error) {
|
||||
return d.runCommandHelm(cmd...)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string, revision int) ([]*v1.Carp, error) {
|
||||
@@ -246,7 +171,7 @@ func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string,
|
||||
var tmp interface{}
|
||||
for dec.Decode(&tmp) == nil {
|
||||
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
|
||||
// bug we can juggle it
|
||||
// we can juggle it
|
||||
jsoned, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -270,115 +195,38 @@ func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string,
|
||||
}
|
||||
|
||||
func (d *DataLayer) RevisionNotes(namespace string, chartName string, revision int, _ bool) (res string, err error) {
|
||||
out, err := d.runCommandHelm("get", "notes", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
cmd := []string{"get", "notes", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision)}
|
||||
key := CacheKeyRevNotes + "\v" + namespace + "\v" + chartName + "\v" + strconv.Itoa(revision)
|
||||
return d.Cache.String(key, nil, func() (string, error) {
|
||||
return d.runCommandHelm(cmd...)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *DataLayer) RevisionValues(namespace string, chartName string, revision int, onlyUserDefined bool) (res string, err error) {
|
||||
cmd := []string{"get", "values", chartName, "--namespace", namespace, "--output", "yaml"}
|
||||
|
||||
if revision > 0 {
|
||||
cmd = append(cmd, "--revision", strconv.Itoa(revision))
|
||||
}
|
||||
cmd := []string{"get", "values", chartName, "--namespace", namespace, "--output", "yaml", "--revision", strconv.Itoa(revision)}
|
||||
|
||||
if !onlyUserDefined {
|
||||
cmd = append(cmd, "--all")
|
||||
}
|
||||
out, err := d.runCommandHelm(cmd...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) GetResource(namespace string, def *v1.Carp) (*v1.Carp, error) {
|
||||
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "json")
|
||||
if err != nil {
|
||||
if strings.HasSuffix(strings.TrimSpace(err.Error()), " not found") {
|
||||
return &v1.Carp{
|
||||
Status: v1.CarpStatus{
|
||||
Phase: "NotFound",
|
||||
Message: err.Error(),
|
||||
Reason: "not found",
|
||||
},
|
||||
}, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var res v1.Carp
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(res.Status.Conditions, func(i, j int) bool {
|
||||
// some condition types always bubble up
|
||||
if res.Status.Conditions[i].Type == "Available" {
|
||||
return false
|
||||
}
|
||||
|
||||
if res.Status.Conditions[j].Type == "Available" {
|
||||
return true
|
||||
}
|
||||
|
||||
t1 := res.Status.Conditions[i].LastTransitionTime
|
||||
t2 := res.Status.Conditions[j].LastTransitionTime
|
||||
return t1.Time.Before(t2.Time)
|
||||
key := CacheKeyRevValues + "\v" + namespace + "\v" + chartName + "\v" + strconv.Itoa(revision) + "\v" + fmt.Sprintf("%v", onlyUserDefined)
|
||||
return d.Cache.String(key, nil, func() (string, error) {
|
||||
return d.runCommandHelm(cmd...)
|
||||
})
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) GetResourceYAML(namespace string, def *v1.Carp) (string, error) {
|
||||
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "yaml")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
func (d *DataLayer) ReleaseUninstall(namespace string, name string) error {
|
||||
d.Cache.Invalidate(CacheKeyRelList, cacheTagRelease(namespace, name))
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) DescribeResource(namespace string, kind string, name string) (string, error) {
|
||||
out, err := d.runCommandKubectl("describe", strings.ToLower(kind), name, "--namespace", namespace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartUninstall(namespace string, name string) error {
|
||||
_, err := d.runCommandHelm("uninstall", name, "--namespace", namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DataLayer) Revert(namespace string, name string, rev int) error {
|
||||
func (d *DataLayer) Rollback(namespace string, name string, rev int) error {
|
||||
d.Cache.Invalidate(CacheKeyRelList, cacheTagRelease(namespace, name))
|
||||
|
||||
_, err := d.runCommandHelm("rollback", name, strconv.Itoa(rev), "--namespace", namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoUpdate(name string) error {
|
||||
cmd := []string{"repo", "update"}
|
||||
if name != "" {
|
||||
cmd = append(cmd, name)
|
||||
}
|
||||
|
||||
_, err := d.runCommandHelm(cmd...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string, version string, justTemplate bool, values string, reuseVals bool) (string, error) {
|
||||
@@ -405,6 +253,11 @@ func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !justTemplate {
|
||||
d.Cache.Invalidate(CacheKeyRelList, cacheTagRelease(namespace, name))
|
||||
}
|
||||
|
||||
res := release.Release{}
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
@@ -417,41 +270,6 @@ func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ShowValues(chart string, ver string) (string, error) {
|
||||
return d.runCommandHelm("show", "values", chart, "--version", ver)
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoList() (res []RepositoryElement, err error) {
|
||||
out, err := d.runCommandHelm("repo", "list", "--output", "json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoAdd(name string, url string) (string, error) {
|
||||
out, err := d.runCommandHelm("repo", "add", "--force-update", name, url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoDelete(name string) (string, error) {
|
||||
out, err := d.runCommandHelm("repo", "remove", name)
|
||||
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)
|
||||
|
||||
@@ -13,7 +13,9 @@ func TestFlow(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
var _ release.Status
|
||||
data := DataLayer{}
|
||||
data := DataLayer{
|
||||
Cache: NewCache(),
|
||||
}
|
||||
err := data.CheckConnectivity()
|
||||
if err != nil {
|
||||
if err.Error() == "did not find any kubectl contexts configured" {
|
||||
@@ -40,7 +42,7 @@ func TestFlow(t *testing.T) {
|
||||
}
|
||||
|
||||
chart := installed[1]
|
||||
history, err := data.ChartHistory(chart.Namespace, chart.Name)
|
||||
history, err := data.ReleaseHistory(chart.Namespace, chart.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,9 @@ type HistoryElement struct {
|
||||
Chart string `json:"chart"`
|
||||
AppVersion string `json:"app_version"`
|
||||
Description string `json:"description"`
|
||||
ChartName string `json:"chart_name"`
|
||||
ChartVer string `json:"chart_ver"`
|
||||
|
||||
ChartName string `json:"chart_name"` // custom addition on top of Helm
|
||||
ChartVer string `json:"chart_ver"` // custom addition on top of Helm
|
||||
}
|
||||
|
||||
type RepoChartElement struct {
|
||||
|
||||
144
pkg/dashboard/subproc/kubectl.go
Normal file
144
pkg/dashboard/subproc/kubectl.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package subproc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (d *DataLayer) runCommandKubectl(cmd ...string) (string, error) {
|
||||
if d.Kubectl == "" {
|
||||
d.Kubectl = "kubectl"
|
||||
}
|
||||
|
||||
cmd = append([]string{d.Kubectl}, cmd...)
|
||||
|
||||
if d.KubeContext != "" {
|
||||
cmd = append(cmd, "--context", d.KubeContext)
|
||||
}
|
||||
|
||||
return d.runCommand(cmd...)
|
||||
}
|
||||
|
||||
type KubeContext struct {
|
||||
IsCurrent bool
|
||||
Name string
|
||||
Cluster string
|
||||
AuthInfo string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
func (d *DataLayer) ListContexts() (res []KubeContext, err error) {
|
||||
out, err := d.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
|
||||
}
|
||||
|
||||
type NamespaceElement struct {
|
||||
Items []struct {
|
||||
Metadata struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"metadata"`
|
||||
} `json:"items"`
|
||||
}
|
||||
|
||||
func (d *DataLayer) GetNameSpaces() (res *NamespaceElement, err error) {
|
||||
out, err := d.runCommandKubectl("get", "namespaces", "-o", "json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) GetResource(namespace string, def *v1.Carp) (*v1.Carp, error) {
|
||||
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "json")
|
||||
if err != nil {
|
||||
if strings.HasSuffix(strings.TrimSpace(err.Error()), " not found") {
|
||||
return &v1.Carp{
|
||||
Status: v1.CarpStatus{
|
||||
Phase: "NotFound",
|
||||
Message: err.Error(),
|
||||
Reason: "not found",
|
||||
},
|
||||
}, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var res v1.Carp
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(res.Status.Conditions, func(i, j int) bool {
|
||||
// some condition types always bubble up
|
||||
if res.Status.Conditions[i].Type == "Available" {
|
||||
return false
|
||||
}
|
||||
|
||||
if res.Status.Conditions[j].Type == "Available" {
|
||||
return true
|
||||
}
|
||||
|
||||
t1 := res.Status.Conditions[i].LastTransitionTime
|
||||
t2 := res.Status.Conditions[j].LastTransitionTime
|
||||
return t1.Time.Before(t2.Time)
|
||||
})
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) GetResourceYAML(namespace string, def *v1.Carp) (string, error) {
|
||||
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "yaml")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) DescribeResource(namespace string, kind string, name string) (string, error) {
|
||||
out, err := d.runCommandKubectl("describe", strings.ToLower(kind), name, "--namespace", namespace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
159
pkg/dashboard/subproc/repos.go
Normal file
159
pkg/dashboard/subproc/repos.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package subproc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (d *DataLayer) ChartRepoList() (res []RepositoryElement, err error) {
|
||||
out, err := d.Cache.String(CacheKeyAllRepos, nil, func() (string, error) {
|
||||
return d.runCommandHelm("repo", "list", "--output", "json")
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoAdd(name string, url string) (string, error) {
|
||||
d.Cache.Invalidate(CacheKeyAllRepos)
|
||||
out, err := d.runCommandHelm("repo", "add", "--force-update", name, url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoDelete(name string) (string, error) {
|
||||
d.Cache.Invalidate(CacheKeyAllRepos)
|
||||
out, err := d.runCommandHelm("repo", "remove", name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoUpdate(name string) error {
|
||||
d.Cache.Invalidate(cacheTagRepoName(name), CacheKeyAllRepos)
|
||||
|
||||
cmd := []string{"repo", "update"}
|
||||
if name != "" {
|
||||
cmd = append(cmd, name)
|
||||
}
|
||||
|
||||
_, err := d.runCommandHelm(cmd...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoVersions(chartName string) (res []*RepoChartElement, err error) {
|
||||
search := "/" + chartName + "\v"
|
||||
if strings.Contains(chartName, "/") {
|
||||
search = "\v" + chartName + "\v"
|
||||
}
|
||||
|
||||
cmd := []string{"search", "repo", "--regexp", search, "--versions", "--output", "json"}
|
||||
out, err := d.Cache.String(cacheTagRepoVers(chartName), []string{CacheKeyAllRepos}, func() (string, error) {
|
||||
return d.runCommandHelm(cmd...)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoCharts(repoName string) (res []*RepoChartElement, err error) {
|
||||
cmd := []string{"search", "repo", "--regexp", "\v" + repoName + "/", "--output", "json"}
|
||||
out, err := d.Cache.String(cacheTagRepoCharts(repoName), []string{CacheKeyAllRepos}, func() (string, error) {
|
||||
return d.runCommandHelm(cmd...)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ins, err := d.ListInstalled()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enrichRepoChartsWithInstalled(res, ins)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func enrichRepoChartsWithInstalled(charts []*RepoChartElement, installed []ReleaseElement) {
|
||||
for _, rchart := range charts {
|
||||
for _, rel := range installed {
|
||||
c, _, err := utils.ChartAndVersion(rel.Chart)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to parse chart: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
pieces := strings.Split(rchart.Name, "/")
|
||||
if pieces[1] == c {
|
||||
// TODO: there can be more than one
|
||||
rchart.InstalledNamespace = rel.Namespace
|
||||
rchart.InstalledName = rel.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ShowValues get values from repo chart, not from installed release
|
||||
func (d *DataLayer) ShowValues(chart string, ver string) (string, error) {
|
||||
return d.Cache.String(CacheKeyRepoChartValues+"\v"+chart+"\v"+ver, nil, func() (string, error) {
|
||||
return d.runCommandHelm("show", "values", chart, "--version", ver)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *DataLayer) ShowChart(chartName string) ([]*chart.Metadata, error) { // TODO: add version parameter to method
|
||||
out, err := d.Cache.String(CacheKeyShowChart+"\v"+chartName, []string{"chart\v" + chartName}, func() (string, error) {
|
||||
return d.runCommandHelm("show", "chart", chartName)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
deccoder := yaml.NewDecoder(bytes.NewReader([]byte(out)))
|
||||
res := make([]*chart.Metadata, 0)
|
||||
var tmp interface{}
|
||||
|
||||
for deccoder.Decode(&tmp) == nil {
|
||||
jsoned, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resjson chart.Metadata
|
||||
err = json.Unmarshal(jsoned, &resjson)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, &resjson)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -5,6 +5,8 @@ type Scanner interface {
|
||||
Test() bool // test if the scanner is available
|
||||
ScanManifests(mnf string) (*ScanResults, error) // run the scanner on manifests
|
||||
ScanResource(ns string, kind string, name string) (*ScanResults, error) // run the scanner on k8s resource
|
||||
SupportedResourceKinds() []string
|
||||
ManifestIsScannable() bool
|
||||
}
|
||||
|
||||
type ScanResults struct {
|
||||
|
||||
@@ -3,24 +3,35 @@ package utils
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var FailLogLevel = log.WarnLevel // allows to suppress error logging in some situations
|
||||
|
||||
type ControlChan = chan struct{}
|
||||
|
||||
func ChartAndVersion(x string) (string, string, error) {
|
||||
lastInd := strings.LastIndex(x, "-")
|
||||
if lastInd < 0 {
|
||||
strs := strings.Split(x, "-")
|
||||
lens := len(strs)
|
||||
if lens < 2 {
|
||||
return "", "", errors.New("can't parse chart version string")
|
||||
} else if lens == 2 {
|
||||
return strs[0], strs[1], nil
|
||||
} else {
|
||||
// semver2 regex , add optional v prefix
|
||||
re := regexp.MustCompile(`v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?`)
|
||||
match := re.FindString(x)
|
||||
lastInd := strings.LastIndex(x, match)
|
||||
return x[:lastInd-1], match, nil
|
||||
}
|
||||
|
||||
return x[:lastInd], x[lastInd+1:], nil
|
||||
}
|
||||
|
||||
func TempFile(txt string) (string, func(), error) {
|
||||
@@ -34,7 +45,7 @@ func TempFile(txt string) (string, func(), error) {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return file.Name(), func() { os.Remove(file.Name()) }, nil
|
||||
return file.Name(), func() { _ = os.Remove(file.Name()) }, nil
|
||||
}
|
||||
|
||||
type CmdError struct {
|
||||
@@ -49,6 +60,7 @@ func (e CmdError) Error() string {
|
||||
}
|
||||
|
||||
func RunCommand(cmd []string, env map[string]string) (string, error) {
|
||||
log.Debugf("Starting command: %s", cmd)
|
||||
prog := exec.Command(cmd[0], cmd[1:]...)
|
||||
prog.Env = os.Environ()
|
||||
|
||||
@@ -63,10 +75,10 @@ func RunCommand(cmd []string, env map[string]string) (string, error) {
|
||||
prog.Stderr = &stderr
|
||||
|
||||
if err := prog.Run(); err != nil {
|
||||
log.Warnf("Failed command: %s", cmd)
|
||||
log.StandardLogger().Logf(FailLogLevel, "Failed command: %s", cmd)
|
||||
serr := stderr.Bytes()
|
||||
if serr != nil {
|
||||
log.Warnf("STDERR:\n%s", serr)
|
||||
log.StandardLogger().Logf(FailLogLevel, "STDERR:\n%s", serr)
|
||||
}
|
||||
if eerr, ok := err.(*exec.ExitError); ok {
|
||||
return "", CmdError{
|
||||
|
||||
118
pkg/dashboard/utils/utils_test.go
Normal file
118
pkg/dashboard/utils/utils_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestGetQueryProps(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
revRequired bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Get query props - all set with revRequired true",
|
||||
wantErr: false,
|
||||
revRequired: true,
|
||||
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing&revision=1",
|
||||
},
|
||||
{
|
||||
name: "Get query props - no revision with revRequired true",
|
||||
wantErr: true,
|
||||
revRequired: true,
|
||||
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing",
|
||||
},
|
||||
{
|
||||
name: "Get query props - no namespace with revRequired true",
|
||||
wantErr: false,
|
||||
revRequired: true,
|
||||
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&revision=1",
|
||||
},
|
||||
{
|
||||
name: "Get query props - no name with revRequired true",
|
||||
wantErr: true,
|
||||
revRequired: true,
|
||||
endpoint: "/api/v1/namespaces/komodorio/charts?namespace=testing&revision=1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", tt.endpoint, nil)
|
||||
_, err := GetQueryProps(c, tt.revRequired)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetQueryProps() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChartAndVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params string
|
||||
wantChart string
|
||||
wantVer string
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "Chart and version - successfully parsing chart and version",
|
||||
params: "chart-1.0.0",
|
||||
wantChart: "chart",
|
||||
wantVer: "1.0.0",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Chart and version - successfully parsing chart and version",
|
||||
params: "chart-v1.0.0",
|
||||
wantChart: "chart",
|
||||
wantVer: "v1.0.0",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Chart and version - successfully parsing chart and version",
|
||||
params: "chart-v1.0.0-alpha",
|
||||
wantChart: "chart",
|
||||
wantVer: "v1.0.0-alpha",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Chart and version - successfully parsing chart and version",
|
||||
params: "chart-1.0.0-alpha",
|
||||
wantChart: "chart",
|
||||
wantVer: "1.0.0-alpha",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Chart and version - parsing chart without version",
|
||||
params: "chart",
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a, b, err := ChartAndVersion(tt.params)
|
||||
if (err != nil) != tt.wantError {
|
||||
t.Errorf("ChartAndVersion() error = %v, wantErr %v", err, tt.wantError)
|
||||
return
|
||||
}
|
||||
|
||||
if a != tt.wantChart {
|
||||
t.Errorf("ChartAndVersion() got = %v, want %v", a, tt.wantChart)
|
||||
}
|
||||
|
||||
if b != tt.wantVer {
|
||||
t.Errorf("ChartAndVersion() got1 = %v, want %v", b, tt.wantVer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
name: "dashboard"
|
||||
version: "0.2.0"
|
||||
version: "0.2.8"
|
||||
usage: "A simplified way of working with Helm"
|
||||
description: "View HELM situation in nice web UI"
|
||||
command: "$HELM_PLUGIN_DIR/bin/helm-dashboard"
|
||||
ignoreFlags: false
|
||||
hooks:
|
||||
install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
|
||||
update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
|
||||
|
||||
@@ -62,3 +62,8 @@ fi
|
||||
tar xzf "releases/v${version}.tar.gz" -C "releases/v${version}"
|
||||
mv "releases/v${version}/${name}" "bin/${name}" || \
|
||||
mv "releases/v${version}/${name}.exe" "bin/${name}"
|
||||
|
||||
echo
|
||||
echo "Helm Dashboard is installed, to start it, run in your terminal:"
|
||||
echo " helm dashboard"
|
||||
echo
|
||||
Reference in New Issue
Block a user