43 Commits

Author SHA1 Message Date
Andrei Pohilko
9a144c1c6f Release 0.2.5 2022-11-06 16:22:30 +00:00
Andrei Pohilko
f897c0f197 Display forced namespace in UI 2022-11-06 16:16:36 +00:00
Andrey Pokhilko
671fa949df Improve resource scanning flow (#68)
* List supported resources for scanners

* Don't warn on scanner discovery commands

* Use scanner-to-resource map

* Save changes

* Scan result tabs

* Own table render for Checkov

* Scannable manifest flag for scanners
2022-11-06 15:56:34 +00:00
Duy Nguyen
612352d69f Add namespace dropdown (#67)
* add get namespace endpoint

* add namespace dropdown

* misc fix
2022-11-06 13:19:32 +00:00
Dimas Yudha P
ef31263797 refactoring makefile, also adding test (#71) 2022-11-03 16:47:28 +00:00
Andrei Pohilko
f64fbd4a2e Fix analytics version 2022-11-01 21:59:16 +00:00
Harshit Mehta
dffb8a726b Allow filtering on repository charts list (#61)
Fixes #46

Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2022-11-01 21:58:34 +00:00
Andrey Pokhilko
14d4886e61 Pass the NS param from Helm correctly (#64) 2022-10-31 18:07:28 +00:00
Andrei Pohilko
0012b0a797 Explicit flag for parameters 2022-10-31 17:04:09 +00:00
Andrei Pohilko
f6b2a8c66d Release 0.2.4 2022-10-31 15:45:14 +00:00
Andrey Pokhilko
c0a1d31c8d Make user analytics optional (#59)
* Make user analytics optional

* Mention analytics in README

* Use analytics flag
2022-10-31 15:33:18 +00:00
Harshit Mehta
329ae055ee Add current cluster info to Install and Upgrade popup (#58)
Fixes #44

Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2022-10-31 08:34:51 +00:00
Andrei Pohilko
7ab0f33201 Transfer TODOs into GH issues 2022-10-28 19:10:53 +01:00
Andrei Pohilko
2e8ba39b8f Attempt to reuse the application if it's already running 2022-10-28 18:28:08 +01:00
Harshit Mehta
c5f9f71e45 Remove version check for charts without repo (#54)
Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2022-10-28 15:22:20 +01:00
Andrei Pohilko
9bb597f366 Update project logo 2022-10-28 14:28:07 +01:00
Andrey Pokhilko
786bddc478 Force namespace via cmdline parameter (#53)
* Force name via cmdline parameter

* Use a library to parse CLI flags

* Use less env vars

* Document it
2022-10-28 12:39:19 +01:00
Andrei Pohilko
d9edcf2f48 Print command hint upon install 2022-10-27 15:32:16 +01:00
Andrei Pohilko
2262445b75 Don't offer to describe historical revisions 2022-10-27 15:31:57 +01:00
Andrei Pohilko
0c486e76c0 Change new version alert display 2022-10-27 15:30:52 +01:00
Andrei Pohilko
7d50f4e620 Recognize "Bound" status as normal 2022-10-26 16:14:10 +01:00
Andrei Pohilko
b2ec371709 Correct link to Slack community 2022-10-26 15:32:46 +01:00
Andrei Pohilko
549cdd9bfb Release 0.2.3 2022-10-26 15:26:40 +01:00
Andrei Pohilko
44787b31cf Release 0.2.2 2022-10-26 13:12:58 +01:00
Andrei Pohilko
e75e653c58 Fix version information field 2022-10-26 12:55:36 +01:00
Andrei Pohilko
3eae013286 Hide upgrade menu item by default 2022-10-26 12:44:06 +01:00
Andrei Pohilko
2221fb22a0 Release 0.2.1 2022-10-26 12:37:47 +01:00
Andrei Pohilko
9dc3e6a12d Merge branch 'main' of github.com:komodorio/helm-dashboard 2022-10-26 12:35:24 +01:00
Andrey Pokhilko
de0024cd03 Check for newer version available (#47)
* Add helm version requirement notes

* Check for newer version and offer upgrade

* fix lint
2022-10-26 12:35:07 +01:00
Andrei Pohilko
ed4e970194 Merge branch 'main' of github.com:komodorio/helm-dashboard 2022-10-26 11:09:43 +01:00
Andrey Pokhilko
b0067e31ba Improvements after release (#45)
* Add helm version requirement notes

* Create Help section, no charts placeholder

* Revive missed "user-defined" values

* Fix namespace undefined upon install

* cosmetics
2022-10-26 11:05:50 +01:00
Andrei Pohilko
7e8ba4709e Merge branch 'main' of github.com:komodorio/helm-dashboard 2022-10-26 08:59:09 +01:00
Andrei Pohilko
be7b2642fc Add helm version requirement notes 2022-10-25 16:58:01 +01:00
Andrey Pokhilko
896d9e3f72 Use different library for opening the browser (#22) 2022-10-25 16:53:33 +01:00
Lior Noy
be6666373b Add version information into UI (#35)
This commit adds the tool version into the UI in the top bar.
2022-10-25 14:08:28 +01:00
Dimas Yudha P
91df9392c0 adding unit test for GetQueryProps (utils). (#39)
* adding unit test for GetQueryProps (utils).

* add unit test chart and version
2022-10-25 14:02:48 +01:00
Dimas Yudha P
bd058ee912 removing else flow (#38) 2022-10-25 12:57:25 +01:00
Andrei Pohilko
997f951d0c Fix error when no scanners are present 2022-10-24 15:42:19 +01:00
Andrei Pohilko
09886ad933 Release 0.2.0 2022-10-23 13:46:18 +01:00
Andrey Pokhilko
0de0b5d0cb Repository-related functions (#19)
* Roadmap item

* Start building repo view

* Section switcher

* Show repo list

* Adding chart repo works

* Showing the pane

* Couple of buttons

* Listing items

* Styling

* Enriching repo view

* Navigate from repo to installed

* Tuning install popup

* Working on install

* Cosmetics
2022-10-23 13:41:45 +01:00
Andrey Pokhilko
0141eecef1 Update README.md 2022-10-20 12:34:42 +01:00
Andrey Pokhilko
65ecc20c90 Create CODE_OF_CONDUCT.md (#20) 2022-10-19 12:23:38 +01:00
Andrey Pokhilko
f86a4a93a7 Scanners Integration (#18)
* Research scanning

* Move files around

* Reports the list

* Scanner happens

* Commit

* Work on alternative

* refactorings

* Progress

* Save the state

* Commit

* Display trivy Results

* Checkov also reports

* Better display

* Correct trivy numbers

* Scan pre-install manifest

* Readme items

* Static checks
2022-10-17 13:41:08 +01:00
38 changed files with 2171 additions and 1511 deletions

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
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
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,9 +1,24 @@
pull: DATE ?= $(shell date +%FT%T%z)
git pull VERSION ?= $(git describe --tags --always --dirty --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo "v0")
build: .PHONY: test
go build -o bin/dashboard . 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: .PHONY: build
DEBUG=1 ./bin/dashboard build: $(BIN) ; $(info $(M) Building executable...) @ ## Build program binary
$Q $(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

104
README.md
View File

@@ -1,4 +1,4 @@
# <img src="pkg/dashboard/static/logo.png" height=30 style="height: 2rem"> Helm Dashboard # ![Helm Dashboard](pkg/dashboard/static/logo-header.svg)
A simplified way of working with Helm. A simplified way of working with Helm.
@@ -6,9 +6,21 @@ A simplified way of working with Helm.
## What it Does? ## What it Does?
The _Helm Dashboard_ plugin offers a UI-driven way to view the installed Helm charts, see their revision history and corresponding k8s resources. Also, you can perform simple actions like roll back to a revision or upgrade to newer version. The _Helm Dashboard_ plugin offers a UI-driven way to view the installed Helm charts, see their revision history and
corresponding k8s resources. Also, you can perform simple actions like roll back to a revision or upgrade to newer
version.
This project is part of [Komodor's](https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash-gh) vision of helping Kubernetes users to navigate and troubleshoot their clusters. This project is part of [Komodor's](https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash-gh) vision of
helping Kubernetes users to navigate and troubleshoot their clusters.
Some of the key capabilities of the tool:
- See all installed charts and their revision history
- See manifest diff of the past revisions
- Browse k8s resources resulting from the chart
- Easy rollback or upgrade version with a clear and easy manifest diff
- Integration with popular problem scanners
- Easy switch between multiple clusters
## Installing ## Installing
@@ -19,6 +31,7 @@ helm plugin install https://github.com/komodorio/helm-dashboard.git
``` ```
To update the plugin to the latest version, run: To update the plugin to the latest version, run:
```shell ```shell
helm plugin update dashboard helm plugin update dashboard
``` ```
@@ -29,72 +42,56 @@ To uninstall, run:
helm plugin uninstall dashboard 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 ## 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: After installing, start the UI by running:
```shell ```shell
helm dashboard 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. 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.
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. You can see the list of available command-line flags by running `helm dashboard --help`.
If your port 8080 is busy, you can specify a different port to use via `HD_PORT` environment variable. 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.
If you don't want browser tab to automatically open, set `HD_NOBROWSER=1` in your environment variables. If your port 8080 is busy, you can specify a different port to use via `--port <number>` command-line flag.
If you want to increase the logging verbosity and see all the debug info, set `DEBUG=1` environment variable. If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line.
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
Upon startup, Helm Dashboard detects the presence of [Trivy](https://github.com/aquasecurity/trivy)
and [Checkov](https://github.com/bridgecrewio/checkov) scanners. When available, these scanners are offered on k8s
resources page, as well as install/upgrade preview page.
You can request scanning of the specific k8s resource in your cluster:
![](screenshot_scan_resource.png)
If you want to validate the k8s manifest prior to installing/reconfiguring a Helm chart, look for "Scan for Problems"
button at the bottom of the dialog:
![](screenshot_scan_manifest.png)
## Support Channels ## Support Channels
We have two main channels for supporting the Helm Dashboard users: [Slack community](https://komodorkommunity.slack.com/archives/C044U1B0265) for general conversations We have two main channels for supporting the Helm Dashboard
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. and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
## Roadmap & Ideas
### First Public Version
- CLI launcher
- Web Server with REST API
- Listing the installed applications
- View k8s resources created by the application (describe, status)
- Viewing revision history for application
- View manifest diffs between revisions, also changelogs etc
- Analytics reporting (telemetry)
- Rollback to a revision
- Check for repo updates & upgrade flow
- Uninstalling the app completely
- Switch clusters
- Show manifest/describe upon clicking on resource
- Helm Plugin Packaging
- Styled properly
### Further Ideas
- solve umbrella-chart case
- Have cleaner idea on the web API structure
- Recognise & show ArgoCD-originating charts/objects, those `helm ls` does not show
#### Topic "Validating Manifests"
- Validate manifests before deploy and get better errors
- See if we can build in Chechov or Validkube validation
#### Iteration "Value Setting"
- Setting parameter values and installing
- Reconfiguring the application
#### Iteration "Repo View"
- Browsing repositories
- Adding new repository
- Installing new app from repo
## Local Dev Testing ## Local Dev Testing
Prerequisites: `helm` and `kubectl` binaries installed and operational. Prerequisites: `helm` and `kubectl` binaries installed and operational.
@@ -105,13 +102,16 @@ There is a need to build binary for plugin to function, run:
go build -o bin/dashboard . go build -o bin/dashboard .
``` ```
You can just run the `bin/dashboard` binary directly, it will just work.
To install, checkout the source code and run from source dir: To install, checkout the source code and run from source dir:
```shell ```shell
helm plugin install . helm plugin install .
``` ```
Local installation of plugin just creates a symlink, so making the changes and rebuilding the binary would not require to Local installation of plugin just creates a symlink, so making the changes and rebuilding the binary would not require
to
reinstall a plugin. reinstall a plugin.
To use the plugin, run in your terminal: To use the plugin, run in your terminal:

84
go.mod
View File

@@ -3,104 +3,46 @@ module github.com/komodorio/helm-dashboard
go 1.18 go 1.18
require ( require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/gin-gonic/gin v1.8.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/hexops/gotextdiff v1.0.3
github.com/sirupsen/logrus v1.8.1 github.com/jessevdk/go-flags v1.5.0
github.com/toqueteos/webbrowser v1.2.0 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/sirupsen/logrus v1.9.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.9.4 helm.sh/helm/v3 v3.9.4
k8s.io/kubectl v0.24.2 k8s.io/apimachinery v0.25.0-alpha.2
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/camelcase v1.0.0 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/gin-contrib/sse v0.1.0 // 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-logr/logr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.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-playground/validator/v10 v10.11.0 // indirect
github.com/goccy/go-json v0.9.11 // indirect github.com/goccy/go-json v0.9.11 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.8 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/lithammer/dedent v1.1.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.3 // 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/russross/blackfriday v1.5.2 // indirect
github.com/spf13/cobra v1.4.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.8.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.70.0 // indirect
k8s.io/api v0.24.2 // indirect
k8s.io/apimachinery v0.24.2 // indirect
k8s.io/cli-runtime v0.24.2 // indirect
k8s.io/client-go v0.24.2 // indirect
k8s.io/component-base v0.24.2 // indirect
k8s.io/component-helpers v0.24.2 // indirect
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect
k8s.io/metrics v0.24.2 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/kustomize/api v0.11.4 // indirect
sigs.k8s.io/kustomize/kustomize/v4 v4.5.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/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
) )

868
go.sum

File diff suppressed because it is too large Load Diff

72
main.go
View File

@@ -1,10 +1,12 @@
package main package main
import ( import (
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jessevdk/go-flags"
"github.com/komodorio/helm-dashboard/pkg/dashboard" "github.com/komodorio/helm-dashboard/pkg/dashboard"
"github.com/pkg/browser"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/toqueteos/webbrowser"
"os" "os"
) )
@@ -14,37 +16,75 @@ var (
date = "unknown" date = "unknown"
) )
func main() { type options struct {
setupLogging() 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.)"`
// TODO: proper command-line parsing Port uint `short:"p" long:"port" description:"Port to start server on" default:"8080"` // TODO: better default port to clash less?
if len(os.Args) > 1 { // dirty thing to allow --help to work
os.Exit(0) Namespace string `short:"n" long:"namespace" description:"Limit operations to a specific namespace"`
} }
address, webServerDone := dashboard.StartServer(version) func main() {
opts := parseFlags()
if os.Getenv("HD_NOBROWSER") == "" { setupLogging(opts.Verbose)
address, webServerDone := dashboard.StartServer(version, int(opts.Port), opts.Namespace, opts.Verbose, opts.NoTracking)
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) log.Infof("Opening web UI: %s", address)
err := webbrowser.Open(address) err := browser.OpenURL(address)
if err != nil { if err != nil {
log.Warnf("Failed to open Web browser for URL: %s", err) log.Warnf("Failed to open Web browser for URL: %s", err)
} }
} else {
log.Infof("Access web UI at: %s", address)
} }
<-webServerDone <-webServerDone
log.Infof("Done.") log.Infof("Done.")
} }
func setupLogging() { func parseFlags() options {
if os.Getenv("DEBUG") == "" { opts := options{Namespace: os.Getenv("HELM_NAMESPACE")}
log.SetLevel(log.InfoLevel) args, err := flags.Parse(&opts)
gin.SetMode(gin.ReleaseMode) if err != nil {
} else { 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.Print(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) log.SetLevel(log.DebugLevel)
gin.SetMode(gin.DebugMode) 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) log.Infof("Helm Dashboard by Komodor, version %s (%s @ %s)", version, commit, date)
} }

View File

@@ -2,13 +2,15 @@ package dashboard
import ( import (
"embed" "embed"
"errors"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"net/http" "net/http"
"os" "os"
"path" "path"
"strconv"
"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"
) )
//go:embed static/* //go:embed static/*
@@ -33,9 +35,19 @@ func errorHandler(c *gin.Context) {
} }
} }
func NewRouter(abortWeb ControlChan, data *DataLayer, version string) *gin.Engine { 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]
}
c.Next()
}
}
func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, debug bool) *gin.Engine {
var api *gin.Engine var api *gin.Engine
if os.Getenv("DEBUG") == "" { if debug {
api = gin.New() api = gin.New()
api.Use(gin.Recovery()) api.Use(gin.Recovery())
} else { } else {
@@ -47,12 +59,12 @@ func NewRouter(abortWeb ControlChan, data *DataLayer, version string) *gin.Engin
api.Use(errorHandler) api.Use(errorHandler)
configureStatic(api) configureStatic(api)
configureRoutes(abortWeb, data, api, version) configureRoutes(abortWeb, data, api)
return api return api
} }
func configureRoutes(abortWeb ControlChan, data *DataLayer, api *gin.Engine, version string) { func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *gin.Engine) {
// server shutdown handler // server shutdown handler
api.DELETE("/", func(c *gin.Context) { api.DELETE("/", func(c *gin.Context) {
abortWeb <- struct{}{} abortWeb <- struct{}{}
@@ -60,32 +72,42 @@ func configureRoutes(abortWeb ControlChan, data *DataLayer, api *gin.Engine, ver
}) })
api.GET("/status", func(c *gin.Context) { 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.StatusInfo)
}) })
configureHelms(api.Group("/api/helm"), data) configureHelms(api.Group("/api/helm"), data)
configureKubectls(api.Group("/api/kube"), data) configureKubectls(api.Group("/api/kube"), data)
configureScanners(api.Group("/api/scanners"), data)
} }
func configureHelms(api *gin.RouterGroup, data *DataLayer) { func configureHelms(api *gin.RouterGroup, data *subproc.DataLayer) {
h := HelmHandler{Data: data} h := handlers.HelmHandler{Data: data}
api.GET("/charts", h.GetCharts) api.GET("/charts", h.GetCharts)
api.DELETE("/charts", h.Uninstall) api.DELETE("/charts", h.Uninstall)
api.POST("/charts/rollback", h.Rollback)
api.GET("/charts/history", h.History) api.GET("/charts/history", h.History)
api.GET("/charts/resources", h.Resources) api.GET("/charts/resources", h.Resources)
api.GET("/charts/:section", h.GetInfoSection)
api.POST("/charts/install", h.Install)
api.POST("/charts/rollback", h.Rollback)
api.GET("/repo", h.RepoList)
api.POST("/repo", h.RepoAdd)
api.DELETE("/repo", h.RepoDelete)
api.GET("/repo/charts", h.RepoCharts)
api.GET("/repo/search", h.RepoSearch) api.GET("/repo/search", h.RepoSearch)
api.POST("/repo/update", h.RepoUpdate) api.POST("/repo/update", h.RepoUpdate)
api.GET("/repo/values", h.RepoValues) api.GET("/repo/values", h.RepoValues)
api.POST("/charts/install", h.Install)
api.GET("/charts/:section", h.GetInfoSection)
} }
func configureKubectls(api *gin.RouterGroup, data *DataLayer) { func configureKubectls(api *gin.RouterGroup, data *subproc.DataLayer) {
h := KubeHandler{Data: data} h := handlers.KubeHandler{Data: data}
api.GET("/contexts", h.GetContexts) api.GET("/contexts", h.GetContexts)
api.GET("/resources/:kind", h.GetResourceInfo) api.GET("/resources/:kind", h.GetResourceInfo)
api.GET("/describe/:kind", h.Describe) api.GET("/describe/:kind", h.Describe)
api.GET("/namespaces", h.GetNameSpaces)
} }
func configureStatic(api *gin.Engine) { func configureStatic(api *gin.Engine) {
@@ -118,36 +140,9 @@ func configureStatic(api *gin.Engine) {
} }
} }
func contextSetter(data *DataLayer) gin.HandlerFunc { func configureScanners(api *gin.RouterGroup, data *subproc.DataLayer) {
return func(c *gin.Context) { h := handlers.ScannersHandler{Data: data}
if context, ok := c.Request.Header["X-Kubecontext"]; ok { api.GET("", h.List)
log.Debugf("Setting current context to: %s", context) api.POST("/manifests", h.ScanDraftManifest)
data.KubeContext = context[0] api.GET("/resource/:kind", h.ScanResource)
}
c.Next()
}
}
type QueryProps struct {
Namespace string
Name string
Revision int
}
func getQueryProps(c *gin.Context, revRequired bool) (*QueryProps, error) {
qp := QueryProps{}
qp.Namespace = c.Query("namespace")
qp.Name = c.Query("name")
if qp.Name == "" {
return nil, errors.New("missing required query string parameter: name")
}
cRev, err := strconv.Atoi(c.Query("revision"))
if err != nil && revRequired {
return nil, err
}
qp.Revision = cRev
return &qp, nil
} }

View File

@@ -1,14 +1,17 @@
package dashboard package handlers
import ( import (
"errors" "errors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
"net/http" "net/http"
"strconv" "strconv"
"strings"
) )
type HelmHandler struct { type HelmHandler struct {
Data *DataLayer Data *subproc.DataLayer
} }
func (h *HelmHandler) GetCharts(c *gin.Context) { func (h *HelmHandler) GetCharts(c *gin.Context) {
@@ -20,15 +23,13 @@ func (h *HelmHandler) GetCharts(c *gin.Context) {
c.IndentedJSON(http.StatusOK, res) c.IndentedJSON(http.StatusOK, res)
} }
// TODO: helm show chart komodorio/k8s-watcher to get the icon URL
func (h *HelmHandler) Uninstall(c *gin.Context) { func (h *HelmHandler) Uninstall(c *gin.Context) {
qp, err := getQueryProps(c, false) qp, err := utils.GetQueryProps(c, false)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return
} }
err = h.Data.UninstallChart(qp.Namespace, qp.Name) err = h.Data.ChartUninstall(qp.Namespace, qp.Name)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return
@@ -37,7 +38,7 @@ func (h *HelmHandler) Uninstall(c *gin.Context) {
} }
func (h *HelmHandler) Rollback(c *gin.Context) { func (h *HelmHandler) Rollback(c *gin.Context) {
qp, err := getQueryProps(c, true) qp, err := utils.GetQueryProps(c, true)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return
@@ -52,7 +53,7 @@ func (h *HelmHandler) Rollback(c *gin.Context) {
} }
func (h *HelmHandler) History(c *gin.Context) { func (h *HelmHandler) History(c *gin.Context) {
qp, err := getQueryProps(c, false) qp, err := utils.GetQueryProps(c, false)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return
@@ -67,7 +68,7 @@ func (h *HelmHandler) History(c *gin.Context) {
} }
func (h *HelmHandler) Resources(c *gin.Context) { func (h *HelmHandler) Resources(c *gin.Context) {
qp, err := getQueryProps(c, true) qp, err := utils.GetQueryProps(c, true)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return
@@ -82,7 +83,7 @@ func (h *HelmHandler) Resources(c *gin.Context) {
} }
func (h *HelmHandler) RepoSearch(c *gin.Context) { func (h *HelmHandler) RepoSearch(c *gin.Context) {
qp, err := getQueryProps(c, false) qp, err := utils.GetQueryProps(c, false)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return
@@ -96,8 +97,23 @@ func (h *HelmHandler) RepoSearch(c *gin.Context) {
c.IndentedJSON(http.StatusOK, res) c.IndentedJSON(http.StatusOK, res)
} }
func (h *HelmHandler) RepoCharts(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
res, err := h.Data.ChartRepoCharts(qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, res)
}
func (h *HelmHandler) RepoUpdate(c *gin.Context) { func (h *HelmHandler) RepoUpdate(c *gin.Context) {
qp, err := getQueryProps(c, false) qp, err := utils.GetQueryProps(c, false)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return
@@ -112,20 +128,31 @@ func (h *HelmHandler) RepoUpdate(c *gin.Context) {
} }
func (h *HelmHandler) Install(c *gin.Context) { func (h *HelmHandler) Install(c *gin.Context) {
qp, err := getQueryProps(c, false) qp, err := utils.GetQueryProps(c, false)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return
} }
justTemplate := c.Query("flag") != "true" justTemplate := c.Query("flag") != "true"
out, err := h.Data.ChartUpgrade(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), justTemplate, c.PostForm("values")) isInitial := c.Query("initial") != "true"
out, err := h.Data.ChartInstall(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), justTemplate, c.PostForm("values"), isInitial)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return
} }
if !justTemplate { if justTemplate {
manifests := ""
if isInitial {
manifests, err = h.Data.RevisionManifests(qp.Namespace, qp.Name, 0, false)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
out = subproc.GetDiff(strings.TrimSpace(manifests), out, "current.yaml", "upgraded.yaml")
} else {
c.Header("Content-Type", "application/json") c.Header("Content-Type", "application/json")
} }
@@ -133,7 +160,7 @@ func (h *HelmHandler) Install(c *gin.Context) {
} }
func (h *HelmHandler) GetInfoSection(c *gin.Context) { func (h *HelmHandler) GetInfoSection(c *gin.Context) {
qp, err := getQueryProps(c, true) qp, err := utils.GetQueryProps(c, true)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return
@@ -158,8 +185,41 @@ func (h *HelmHandler) RepoValues(c *gin.Context) {
c.String(http.StatusOK, out) c.String(http.StatusOK, out)
} }
func handleGetSection(data *DataLayer, section string, rDiff string, qp *QueryProps, flag bool) (string, error) { func (h *HelmHandler) RepoList(c *gin.Context) {
sections := map[string]SectionFn{ out, err := h.Data.ChartRepoList()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, out)
}
func (h *HelmHandler) RepoAdd(c *gin.Context) {
_, err := h.Data.ChartRepoAdd(c.PostForm("name"), c.PostForm("url"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Status(http.StatusNoContent)
}
func (h *HelmHandler) RepoDelete(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
_, err = h.Data.ChartRepoDelete(qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Status(http.StatusNoContent)
}
func handleGetSection(data *subproc.DataLayer, section string, rDiff string, qp *utils.QueryProps, flag bool) (string, error) {
sections := map[string]subproc.SectionFn{
"manifests": data.RevisionManifests, "manifests": data.RevisionManifests,
"values": data.RevisionValues, "values": data.RevisionValues,
"notes": data.RevisionNotes, "notes": data.RevisionNotes,
@@ -181,16 +241,16 @@ func handleGetSection(data *DataLayer, section string, rDiff string, qp *QueryPr
ext = ".txt" ext = ".txt"
} }
res, err := RevisionDiff(functor, ext, qp.Namespace, qp.Name, cRevDiff, qp.Revision, flag) res, err := subproc.RevisionDiff(functor, ext, qp.Namespace, qp.Name, cRevDiff, qp.Revision, flag)
if err != nil { if err != nil {
return "", err return "", err
} }
return res, nil return res, nil
} else { }
res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag) res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag)
if err != nil { if err != nil {
return "", err return "", err
} }
return res, nil return res, nil
} }
}

View File

@@ -1,14 +1,17 @@
package dashboard package handlers
import ( import (
"github.com/gin-gonic/gin"
"k8s.io/apimachinery/pkg/apis/meta/v1"
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"net/http" "net/http"
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
) )
type KubeHandler struct { type KubeHandler struct {
Data *DataLayer Data *subproc.DataLayer
} }
func (h *KubeHandler) GetContexts(c *gin.Context) { func (h *KubeHandler) GetContexts(c *gin.Context) {
@@ -21,7 +24,7 @@ func (h *KubeHandler) GetContexts(c *gin.Context) {
} }
func (h *KubeHandler) GetResourceInfo(c *gin.Context) { func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
qp, err := getQueryProps(c, false) qp, err := utils.GetQueryProps(c, false)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return
@@ -53,7 +56,7 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
} }
func (h *KubeHandler) Describe(c *gin.Context) { func (h *KubeHandler) Describe(c *gin.Context) {
qp, err := getQueryProps(c, false) qp, err := utils.GetQueryProps(c, false)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return
@@ -67,3 +70,13 @@ func (h *KubeHandler) Describe(c *gin.Context) {
c.String(http.StatusOK, res) 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)
}

View File

@@ -0,0 +1,76 @@
package handlers
import (
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
"net/http"
)
type ScannersHandler struct {
Data *subproc.DataLayer
}
func (h *ScannersHandler) List(c *gin.Context) {
type ScannerInfo struct {
SupportedResourceKinds []string
ManifestScannable bool
}
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) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
reuseVals := c.Query("initial") != "true"
mnf, err := h.Data.ChartInstall(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), true, c.PostForm("values"), reuseVals)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
reps := map[string]*subproc.ScanResults{}
for _, scanner := range h.Data.Scanners {
sr, err := scanner.ScanManifests(mnf)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
reps[scanner.Name()] = sr
}
c.IndentedJSON(http.StatusOK, reps)
}
func (h *ScannersHandler) ScanResource(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
reps := map[string]*subproc.ScanResults{}
for _, scanner := range h.Data.Scanners {
sr, err := scanner.ScanResource(qp.Namespace, c.Param("kind"), qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
reps[scanner.Name()] = sr
}
c.IndentedJSON(http.StatusOK, reps)
}

View File

@@ -0,0 +1,164 @@
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"
"strings"
)
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
}
log.Infof("Discovered Checkov version: %s", strings.TrimSpace(res))
return true
}
func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) {
fname, fclose, err := utils.TempFile(mnf)
if err != nil {
return nil, err
}
defer fclose()
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{}
err = json.Unmarshal([]byte(out), res.OrigReport)
if err != nil {
return nil, err
}
return res, nil
}
func (c *Checkov) ScanResource(ns string, kind string, name string) (*subproc.ScanResults, error) {
carp := v1.Carp{}
carp.Kind = kind
carp.Name = name
mnf, err := c.Data.GetResourceYAML(ns, &carp)
if err != nil {
return nil, err
}
fname, fclose, err := utils.TempFile(mnf)
if err != nil {
return nil, err
}
defer fclose()
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "json", "--file", fname}
out, err := utils.RunCommand(cmd, nil)
if err != nil {
return nil, err
}
cr := CheckovReport{}
err = json.Unmarshal([]byte(out), &cr)
if err != nil {
return nil, err
}
res := &subproc.ScanResults{
PassedCount: cr.Summary.Passed,
FailedCount: cr.Summary.Failed,
OrigReport: checkovReportTable(&cr),
}
return res, nil
}
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 {
Failed int `json:"failed"`
Passed int `json:"passed"`
ResourceCount int `json:"resource_count"`
// 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"`
}

View File

@@ -0,0 +1,107 @@
package scanners
import (
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
"strconv"
"strings"
)
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
}
parts := strings.Split(res, "\n")
log.Infof("Discovered Trivy: %s", strings.TrimSpace(parts[0]))
return true
}
func (c *Trivy) ScanManifests(_ string) (*subproc.ScanResults, error) {
return nil, nil // Trivy is unable to scan manifests
}
func (c *Trivy) scanResource(ns string, kind string, name string) (string, error) {
cmd := []string{"trivy", "kubernetes", "--quiet", "--format", "table", "--report", "all", "--no-progress",
"--context", c.Data.KubeContext, "--namespace", ns, kind + "/" + name}
out, err := utils.RunCommand(cmd, nil)
if err != nil {
return "", err
}
return out, nil
}
func (c *Trivy) ScanResource(ns string, kind string, name string) (*subproc.ScanResults, error) {
res := subproc.ScanResults{}
resource, err := c.scanResource(ns, kind, name)
if err != nil {
return nil, err
}
for _, line := range strings.Split(resource, "\n") {
if strings.HasPrefix(line, "Tests:") {
parts := strings.FieldsFunc(line, func(r rune) bool {
return r == ':' || r == ',' || r == ')'
})
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[2])); err == nil {
res.PassedCount += cnt
} else {
log.Warnf("Failed to parse Trivy output: %s", err)
}
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[4])); err == nil {
res.FailedCount += cnt
} else {
log.Warnf("Failed to parse Trivy output: %s", err)
}
}
if strings.HasPrefix(line, "Total:") {
parts := strings.FieldsFunc(line, func(r rune) bool {
return r == ':' || r == ',' || r == '('
})
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
res.FailedCount += cnt
} else {
log.Warnf("Failed to parse Trivy output: %s", err)
}
}
}
res.OrigReport = resource
return &res, nil
}

View File

@@ -2,46 +2,66 @@ package dashboard
import ( import (
"context" "context"
"encoding/json"
"github.com/gin-gonic/gin" "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" log "github.com/sirupsen/logrus"
"net/http" "net/http"
"os" "os"
"strconv"
"strings"
"time"
) )
func StartServer(version string) (string, ControlChan) { func StartServer(version string, port int, ns string, debug bool, noTracking bool) (string, utils.ControlChan) {
data := DataLayer{} data := subproc.DataLayer{
Namespace: ns,
}
err := data.CheckConnectivity() err := data.CheckConnectivity()
if err != nil { if err != nil {
log.Errorf("Failed to check that Helm is operational, cannot continue. The error was: %s", err) log.Errorf("Failed to check that Helm is operational, cannot continue. The error was: %s", err)
os.Exit(1) // TODO: propagate error instead? os.Exit(1) // TODO: propagate error instead?
} }
data.StatusInfo = &subproc.StatusInfo{
CurVer: version,
Analytics: !noTracking,
LimitedToNamespace: ns,
}
go checkUpgrade(data.StatusInfo)
discoverScanners(&data)
address := os.Getenv("HD_BIND") address := os.Getenv("HD_BIND")
if address == "" { if address == "" {
address = "localhost" address = "localhost"
} }
if os.Getenv("HD_PORT") == "" { address += ":" + strconv.Itoa(port)
address += ":8080" // TODO: better default port to clash less?
} else {
address += ":" + os.Getenv("HD_PORT")
}
abort := make(ControlChan) abort := make(utils.ControlChan)
api := NewRouter(abort, &data, version) api := NewRouter(abort, &data, debug)
done := startBackgroundServer(address, api, abort) done := startBackgroundServer(address, api, abort)
return "http://" + address, done return "http://" + address, done
} }
func startBackgroundServer(addr string, routes *gin.Engine, abort ControlChan) ControlChan { func startBackgroundServer(addr string, routes *gin.Engine, abort utils.ControlChan) utils.ControlChan {
done := make(ControlChan) done := make(utils.ControlChan)
server := &http.Server{Addr: addr, Handler: routes} server := &http.Server{Addr: addr, Handler: routes}
go func() { go func() {
err := server.ListenAndServe() err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed { 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...", addr)
if itIsUs(addr) {
log.Infof("Yes, it's another instance of us. Just reuse it.")
} else {
panic(err)
}
} }
done <- struct{}{} done <- struct{}{}
}() }()
@@ -56,3 +76,70 @@ func startBackgroundServer(addr string, routes *gin.Engine, abort ControlChan) C
return done return done
} }
func itIsUs(addr string) bool {
var myClient = &http.Client{Timeout: 5 * time.Second}
r, err := myClient.Get("http://" + addr + "/status")
if err != nil {
log.Debugf("It's not us on %s: %s", addr, 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},
&scanners.Trivy{Data: data},
}
data.Scanners = []subproc.Scanner{}
for _, scanner := range potential {
if scanner.Test() {
data.Scanners = append(data.Scanners, scanner)
}
}
}
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 version: %s", err)
v1 = &version.Version{}
}
v2, err := version.NewVersion(d.LatestVer)
if err != nil {
log.Warnf("Failed to parse version: %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)
}
}
}

View File

@@ -4,7 +4,7 @@ $("#btnUpgradeCheck").click(function () {
self.find(".spinner-border").show() self.find(".spinner-border").show()
const repoName = self.data("repo") const repoName = self.data("repo")
$("#btnUpgrade span").text("Checking...") $("#btnUpgrade span").text("Checking...")
$("#btnUpgrade .icon").removeClass("bi-arrow-up bi-pencil").addClass("bi-hourglass") $("#btnUpgrade .icon").removeClass("bi-arrow-up bi-pencil").addClass("bi-hourglass-split")
$.post("/api/helm/repo/update?name=" + repoName).fail(function (xhr) { $.post("/api/helm/repo/update?name=" + repoName).fail(function (xhr) {
reportError("Failed to update chart repo", xhr) reportError("Failed to update chart repo", xhr)
}).done(function () { }).done(function () {
@@ -26,21 +26,12 @@ function checkUpgradeable(name) {
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-x-octagon") $("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-x-octagon")
$("#btnUpgrade").prop("disabled", true) $("#btnUpgrade").prop("disabled", true)
$("#btnUpgradeCheck").prop("disabled", true) $("#btnUpgradeCheck").prop("disabled", true)
$("#btnAddRepository").text("Add repository for it")
return return
} }
$("#btnUpgradeCheck").text("Check for new version")
const verCur = $("#specRev").data("last-chart-ver"); const verCur = $("#specRev").data("last-chart-ver");
$('#upgradeModal select').empty()
for (let i = 0; i < data.length; i++) {
const opt = $("<option value='" + data[i].version + "'></option>");
if (data[i].version === verCur) {
opt.html(data[i].version + " &middot;")
} else {
opt.html(data[i].version)
}
$('#upgradeModal select').append(opt)
}
const elm = data[0] const elm = data[0]
$("#btnUpgradeCheck").data("repo", elm.name.split('/').shift()) $("#btnUpgradeCheck").data("repo", elm.name.split('/').shift())
$("#btnUpgradeCheck").data("chart", elm.name.split('/').pop()) $("#btnUpgradeCheck").data("chart", elm.name.split('/').pop())
@@ -56,36 +47,90 @@ function checkUpgradeable(name) {
} }
$("#btnUpgrade").off("click").click(function () { $("#btnUpgrade").off("click").click(function () {
popUpUpgrade($(this), verCur, elm) popUpUpgrade(elm, getHashParam("namespace"), getHashParam("chart"), verCur, $("#specRev").data("last-rev"))
}) })
}) })
} }
function popUpUpgrade(self, verCur, elm) { function popUpUpgrade(elm, ns, name, verCur, lastRev) {
const name = getHashParam("chart"); $("#upgradeModal .btn-confirm").prop("disabled", true)
let url = "/api/helm/charts/install?namespace=" + getHashParam("namespace") + "&name=" + name + "&chart=" + elm.name;
$('#upgradeModal select').data("url", url).data("chart", elm.name)
$("#upgradeModalLabel .name").text(name) $('#upgradeModal').data("chart", elm.name).data("initial", !verCur)
$("#upgradeModal .ver-old").text(verCur)
$("#upgradeModalLabel .name").text(elm.name)
$("#upgradeModal .rel-cluster").text(getHashParam("context"))
if (verCur) {
$("#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 {
$("#upgradeModal .ver-old").hide()
$("#upgradeModal .rel-name").prop("disabled", false).val(elm.name.split("/").pop())
$("#upgradeModal .rel-ns").prop("disabled", false).val("")
}
$.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
}))
})
})
$.getJSON("/api/helm/repo/search?name=" + elm.name).fail(function (xhr) {
reportError("Failed to find chart in repo", xhr)
}).done(function (vers) {
// fill versions
$('#upgradeModal select').empty()
for (let i = 0; i < vers.length; i++) {
const opt = $("<option value='" + vers[i].version + "'></option>");
if (vers[i].version === verCur) {
opt.html(vers[i].version + " &middot;")
} else {
opt.html(vers[i].version)
}
$('#upgradeModal select').append(opt)
}
$('#upgradeModal select').val(elm.version).trigger("change") $('#upgradeModal select').val(elm.version).trigger("change")
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {}); const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
myModal.show() myModal.show()
const btnConfirm = $("#upgradeModal .btn-confirm"); if (verCur) {
btnConfirm.prop("disabled", true).off('click').click(function () { // fill current values
$.get("/api/helm/charts/values?namespace=" + ns + "&revision=" + lastRev + "&name=" + name + "&flag=true").fail(function (xhr) {
reportError("Failed to get charts values info", xhr)
}).done(function (data) {
$("#upgradeModal textarea").val(data).data("dirty", false)
})
} else {
$("#upgradeModal textarea").val("").data("dirty", true)
}
})
}
$("#upgradeModal .btn-confirm").click(function () {
const btnConfirm = $("#upgradeModal .btn-confirm")
btnConfirm.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>') btnConfirm.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: url + "&version=" + $('#upgradeModal select').val() + "&flag=true", url: "/api/helm/charts/install" + upgradeModalQstr() + "&flag=true",
data: $("#upgradeModal textarea").data("dirty") ? $("#upgradeModal form").serialize() : null, data: $("#upgradeModal textarea").data("dirty") ? $("#upgradeModal form").serialize() : null,
}).fail(function (xhr) { }).fail(function (xhr) {
reportError("Failed to upgrade the chart", xhr) reportError("Failed to upgrade the chart", xhr)
}).done(function (data) { }).done(function (data) {
console.log(data)
if (data.version) { if (data.version) {
setHashParam("section", null)
const ns = $("#upgradeModal .rel-ns").val();
setHashParam("namespace", ns ? ns : "default")
setHashParam("chart", $("#upgradeModal .rel-name").val())
setHashParam("revision", data.version) setHashParam("revision", data.version)
window.location.reload() window.location.reload()
} else { } else {
@@ -94,17 +139,9 @@ function popUpUpgrade(self, verCur, elm) {
}) })
}) })
// fill current values
const lastRev = $("#specRev").data("last-rev")
$.get("/api/helm/charts/values?namespace=" + getHashParam("namespace") + "&revision=" + lastRev + "&name=" + getHashParam("chart") + "&flag=true").fail(function (xhr) {
reportError("Failed to get charts values info", xhr)
}).done(function (data) {
$("#upgradeModal textarea").val(data).data("dirty", false)
})
}
let reconfigTimeout = null; let reconfigTimeout = null;
$("#upgradeModal textarea").keyup(function () {
function changeTimer() {
const self = $(this); const self = $(this);
self.data("dirty", true) self.data("dirty", true)
if (reconfigTimeout) { if (reconfigTimeout) {
@@ -113,7 +150,11 @@ $("#upgradeModal textarea").keyup(function () {
reconfigTimeout = window.setTimeout(function () { reconfigTimeout = window.setTimeout(function () {
requestChangeDiff() requestChangeDiff()
}, 500) }, 500)
}) }
$("#upgradeModal textarea").keyup(changeTimer)
$("#upgradeModal .rel-name").keyup(changeTimer)
$("#upgradeModal .rel-ns").keyup(changeTimer)
$('#upgradeModal select').change(function () { $('#upgradeModal select').change(function () {
const self = $(this) const self = $(this)
@@ -122,7 +163,7 @@ $('#upgradeModal select').change(function () {
// fill reference values // fill reference values
$("#upgradeModal .ref-vals").html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>') $("#upgradeModal .ref-vals").html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
$.get("/api/helm/repo/values?chart=" + self.data("chart") + "&version=" + self.val()).fail(function (xhr) { $.get("/api/helm/repo/values?chart=" + $("#upgradeModal").data("chart") + "&version=" + self.val()).fail(function (xhr) {
reportError("Failed to get upgrade info", xhr) reportError("Failed to get upgrade info", xhr)
}).done(function (data) { }).done(function (data) {
data = hljs.highlight(data, {language: 'yaml'}).value data = hljs.highlight(data, {language: 'yaml'}).value
@@ -130,6 +171,39 @@ $('#upgradeModal select').change(function () {
}) })
}) })
$('#upgradeModal .btn-scan').click(function () {
const self = $(this)
self.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
$.ajax({
type: "POST",
url: "/api/scanners/manifests" + upgradeModalQstr(),
data: $("#upgradeModal form").serialize(),
}).fail(function (xhr) {
reportError("Failed to scan the manifest", xhr)
}).done(function (data) {
self.prop("disabled", false).find(".spinner-border").hide()
const container = $("<div></div>")
for (let name in data) {
const res = data[name]
if (!res) {
continue
}
const pre = $("<pre></pre>").text(res.OrigReport)
container.append("<h2>" + name + " Scan Results</h2>")
container.append(pre)
}
const tab = window.open('about:blank', '_blank');
tab.document.write(container.prop('outerHTML')); // where 'html' is a variable containing your HTML
tab.document.close(); // to finish loading the page
})
})
function requestChangeDiff() { function requestChangeDiff() {
const self = $('#upgradeModal select'); const self = $('#upgradeModal select');
const diffBody = $("#upgradeModalBody"); const diffBody = $("#upgradeModalBody");
@@ -152,7 +226,7 @@ function requestChangeDiff() {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: self.data("url") + "&version=" + self.val(), url: "/api/helm/charts/install" + upgradeModalQstr(),
data: values, data: values,
}).fail(function (xhr) { }).fail(function (xhr) {
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>") $("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>")
@@ -173,6 +247,20 @@ function requestChangeDiff() {
}) })
} }
function upgradeModalQstr() {
let qstr = "?" +
"namespace=" + $("#upgradeModal .rel-ns").val() +
"&name=" + $("#upgradeModal .rel-name").val() +
"&chart=" + $("#upgradeModal").data("chart") +
"&version=" + $('#upgradeModal select').val()
if ($("#upgradeModal").data("initial")) {
qstr += "&initial=true"
}
return qstr
}
const btnConfirm = $("#confirmModal .btn-confirm"); const btnConfirm = $("#confirmModal .btn-confirm");
$("#btnUninstall").click(function () { $("#btnUninstall").click(function () {
const chart = getHashParam('chart'); const chart = getHashParam('chart');
@@ -258,3 +346,7 @@ $("#btnRollback").click(function () {
}) })
}) })
$("#btnAddRepository").click(function () {
setHashParam("section", "repository")
window.location.reload()
})

View File

@@ -0,0 +1,59 @@
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 && version !== "dev") {
enableDD(version)
enableHeap()
}
}
}
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() {
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("3615793373");
}

View File

@@ -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);
})

View File

@@ -140,6 +140,7 @@ $("#nav-tab [data-tab]").click(function () {
} }
}) })
function showResources(namespace, chart, revision) { function showResources(namespace, chart, revision) {
const resBody = $("#nav-resources .body"); const resBody = $("#nav-resources .body");
resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>'); resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
@@ -149,6 +150,12 @@ function showResources(namespace, chart, revision) {
$.getJSON(url).fail(function (xhr) { $.getJSON(url).fail(function (xhr) {
reportError("Failed to get list of resources", xhr) reportError("Failed to get list of resources", xhr)
}).done(function (data) { }).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(); resBody.empty();
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const res = data[i] const res = data[i]
@@ -156,9 +163,9 @@ function showResources(namespace, chart, revision) {
<div class="row px-3 py-2 mb-3 bg-white rounded"> <div class="row px-3 py-2 mb-3 bg-white rounded">
<div class="col-2 res-kind text-break"></div> <div class="col-2 res-kind text-break"></div>
<div class="col-3 res-name text-break fw-bold"></div> <div class="col-3 res-name text-break fw-bold"></div>
<div class="col-1 res-status"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span></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-5 res-statusmsg"><span class="text-muted small">Getting status...</span></div> <div class="col-4 res-statusmsg"><span class="text-muted small">Getting status...</span></div>
<div class="col-1 res-actions"></div> <div class="col-2 res-actions"><button class='btn btn-sm ms-2 visually-hidden'>Vertical-sizer</button></div>
</div> </div>
`) `)
@@ -171,7 +178,7 @@ function showResources(namespace, chart, revision) {
//reportError("Failed to get list of resources") //reportError("Failed to get list of resources")
}).done(function (data) { }).done(function (data) {
const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase); 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") badge.addClass("bg-success text-dark")
} else if (["Exists"].includes(data.status.phase)) { } else if (["Exists"].includes(data.status.phase)) {
badge.addClass("bg-success text-dark bg-opacity-50") badge.addClass("bg-success text-dark bg-opacity-50")
@@ -182,16 +189,25 @@ function showResources(namespace, chart, revision) {
} }
const statusBlock = resBlock.find(".res-status"); const statusBlock = resBlock.find(".res-status");
statusBlock.empty().append(badge) 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>") 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") resBlock.find(".res-actions")
const btn = $("<button class=\"btn btn-sm btn-white border-secondary\">Describe</button>"); const btn = $("<button class=\"btn btn-sm btn-white border-secondary\">Describe</button>");
resBlock.find(".res-actions").append(btn) resBlock.find(".res-actions").append(btn)
btn.click(function () { btn.click(function () {
showDescribe(ns, res.kind, res.metadata.name, badge.clone()) showDescribe(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())
})
}
} }
}) })
} }
@@ -212,3 +228,53 @@ function showDescribe(ns, kind, name, badge) {
$("#describeModalBody").empty().append("<pre class='bg-white rounded p-3'></pre>").find("pre").html(data) $("#describeModalBody").empty().append("<pre class='bg-white rounded p-3'></pre>").find("pre").html(data)
}) })
} }
function scanResource(ns, kind, name, badge) {
$("#describeModal .offcanvas-header p").text(kind)
$("#describeModalLabel").text(name).append(badge.addClass("ms-3 small fw-normal"))
const body = $("#describeModalBody");
body.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Scanning...')
const myModal = new bootstrap.Offcanvas(document.getElementById('describeModal'));
myModal.show()
$.get("/api/scanners/resource/" + kind.toLowerCase() + "?name=" + name + "&namespace=" + ns).fail(function (xhr) {
reportError("Failed to scan resource", xhr)
}).done(function (data) {
body.empty()
if ($.isEmptyObject(data)) {
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 && !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.find('button').append("<span class='badge bg-danger ms-2'>" + res.FailedCount + " failed</span>")
}
if (res.PassedCount) {
hdr.find('button').append("<span class='badge bg-info ms-2'>" + res.PassedCount + " passed</span>")
}
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)
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()
})
}

View File

@@ -5,31 +5,16 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Helm Dashboard</title> <title>Helm Dashboard</title>
<script src="static/datadog.js"></script> <script src="static/analytics.js"></script>
<link rel="stylesheet" <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"/> 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" <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"> 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://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" 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 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"> <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> </head>
<body> <body>
@@ -37,37 +22,47 @@
<!-- TOP BAR --> <!-- TOP BAR -->
<nav class="navbar navbar-expand bg-white mb-0 p-0 b-shadow" id="topNav"> <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="container-fluid m-0 p-0">
<div class="navbar-brand"> <div class="navbar-brand me-0">
<a href="/"><img src="static/logo.png" alt="Logo"></a> <a href="/"><img src="static/logo-header.svg" alt="Helm Dashboard"></a>
<div>
<h1><a href="/">Helm Dashboard</a></h1>
</div>
</div> </div>
<div class="separator-vertical mx-3"><span></span></div> <div class="separator-vertical mx-3"><span></span></div>
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item mx-2"> <li class="nav-item mx-2">
<a class="nav-link px-3 active" aria-current="page" href="/">Installed</a> <a class="nav-link px-3 section-installed">Installed</a>
</li> </li>
<!-- TODO
<li class="nav-item mx-2"> <li class="nav-item mx-2">
<a href="#" class="nav-link px-3">Repository</a> <a class="nav-link px-3 section-repo">Repository</a>
</li> </li>
--> <li class="nav-item mx-2 dropdown">
<!-- TODO <a class="nav-link dropdown-toggle section-help" role="button" data-bs-toggle="dropdown"
<li class="nav-item"> aria-expanded="false">
<a class="nav-link disabled">Provisional Charts</a> 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>
--> <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> </ul>
<div> <div>
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img <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> 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>
<div class="separator-vertical"><span></span></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> <i class="btn bi-power text-muted p-2 m-1 mx-2" title="Shut down the Helm Dashboard application"></i>
@@ -75,7 +70,49 @@
</nav> </nav>
<!-- /TOP BAR --> <!-- /TOP BAR -->
<div class="row mt-3 pt-3 me-5" id="sectionList" style="display: none"> <!--REPO SECTION-->
<div class="row mt-3 pt-3 me-5 section" id="sectionRepo" style="display: none">
<div class="col-3 ps-4 repo-list">
<div class="p-2 bg-white rounded-1 b-shadow">
<h4 class="fs-6">Repositories</h4>
<ul class="list-unstyled p-2">
</ul>
<button class="btn btn-sm border-secondary text-muted">
<i class="bi-plus-lg"></i> Add Repository
</button>
</div>
</div>
<div class="col-9 repo-details bg-white b-shadow pt-4 px-5 overflow-auto rounded">
<div class="float-end">
<button class="me-2 btn btn-sm btn-light bg-white border border-secondary btn-update">
<i class="bi-arrow-repeat"></i> Update
</button>
<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>
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
style="text-transform: uppercase">
<div class="col-3">Chart Name</div>
<div class="col">Description</div>
<div class="col-1">Version</div>
<div class="col-1"></div>
</div>
<ul class="list-unstyled mt-4 charts"></ul>
</div>
</div>
<div class="row mt-3 pt-3 me-5 section" id="sectionList" style="display: none">
<div class="col-2 ms-3"> <div class="col-2 ms-3">
<!-- FILTER BLOCK --> <!-- FILTER BLOCK -->
<div class="p-2 ps-2 bg-white rounded-1 b-shadow" id="filters"> <div class="p-2 ps-2 bg-white rounded-1 b-shadow" id="filters">
@@ -84,6 +121,7 @@
<ul class="list-unstyled" id="cluster"> <ul class="list-unstyled" id="cluster">
</ul> </ul>
<h4 id="limitNamespace" class="display-none">Forced Namespace: <span></span></h4>
<!-- TODO <!-- TODO
<h4 class="mt-4">Namespaces</h4> <h4 class="mt-4">Namespaces</h4>
<ul class="list-unstyled" id="namespaces"> <ul class="list-unstyled" id="namespaces">
@@ -113,11 +151,14 @@
</div> </div>
<div class="body"></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> </div>
<!-- /INSTALLED LIST --> <!-- /INSTALLED LIST -->
</div> </div>
<div class="row flex-nowrap pt-0 mx-0" id="sectionDetails" style="display: none"> <div class="row flex-nowrap pt-0 mx-0 section" id="sectionDetails" style="display: none">
<div class="col-2 px-4 py-4 pe-3 rev-list"> <div class="col-2 px-4 py-4 pe-3 rev-list">
<h3 class="fw-bold small">Revisions</h3> <h3 class="fw-bold small">Revisions</h3>
<ul class="list-unstyled"> <ul class="list-unstyled">
@@ -141,7 +182,11 @@
title="Uninstall the chart"><i class="bi-trash3"></i> Uninstall title="Uninstall the chart"><i class="bi-trash3"></i> Uninstall
</button> </button>
<br/> <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" <span class="spinner-border spinner-border-sm" style="display: none" role="status"
aria-hidden="true"></span> aria-hidden="true"></span>
</a> </a>
@@ -174,15 +219,15 @@
Resources Resources
</button> </button>
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="manifests" <button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="manifests"
type="button" role="tab" aria-controls="nav-manifest-diff" aria-selected="false" type="button" role="tab" aria-controls="nav-manifest" aria-selected="false"
tabindex="-1">Manifests tabindex="-1">Manifests
</button> </button>
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="values" <button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="values"
type="button" role="tab" aria-controls="nav-disabled" aria-selected="false" tabindex="-1"> type="button" role="tab" aria-controls="nav-manifest" aria-selected="false" tabindex="-1">
Values Values
</button> </button>
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="notes" <button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="notes"
type="button" role="tab" aria-controls="nav-disabled" aria-selected="false" tabindex="-1"> type="button" role="tab" aria-controls="nav-manifest" aria-selected="false" tabindex="-1">
Notes Notes
</button> </button>
</div> </div>
@@ -200,7 +245,7 @@
<div class="body"></div> <div class="body"></div>
</div> </div>
<div class="tab-pane" id="nav-manifest" role="tabpanel"> <div class="tab-pane" id="nav-manifest" role="tabpanel">
<nav class="navbar bg-light"> <nav class="navbar bg-white rounded border border-secondary">
<form class="container-fluid" id="modePanel"> <form class="container-fluid" id="modePanel">
<label class="form-check-label" for="diffModeNone"> <label class="form-check-label" for="diffModeNone">
<input class="form-check-input" type="radio" name="diffMode" id="diffModeNone" <input class="form-check-input" type="radio" name="diffMode" id="diffModeNone"
@@ -225,9 +270,6 @@
<div id="manifestText" class="mt-2 bg-white"></div> <div id="manifestText" class="mt-2 bg-white"></div>
</div> </div>
<div class="tab-pane" id="nav-disabled" role="tabpanel" aria-labelledby="nav-disabled-tab"
tabindex="0">...
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -235,7 +277,7 @@
<!-- Modals --> <!-- Modals -->
<div id="errorAlert" style="z-index: 2000" <div id="errorAlert" style="z-index: 2000; max-width: 95%; overflow: auto"
class="display-none alert alert-sm alert-danger alert-dismissible position-absolute position-absolute top-0 start-50 translate-middle-x mt-3 border-danger" class="display-none alert alert-sm alert-danger alert-dismissible position-absolute position-absolute top-0 start-50 translate-middle-x mt-3 border-danger"
role="alert"> role="alert">
<h4 class="alert-heading"><i class="bi-exclamation-triangle-fill"></i> <span></span></h4> <h4 class="alert-heading"><i class="bi-exclamation-triangle-fill"></i> <span></span></h4>
@@ -245,7 +287,7 @@
</div> </div>
<div class="offcanvas offcanvas-end rounded-start" tabindex="-1" id="describeModal" <div class="offcanvas offcanvas-end rounded-start" tabindex="-1" id="describeModal"
aria-labelledby="describeModalLabel"> aria-labelledby="describeModalLabel" style="overflow-x: auto">
<div class="offcanvas-header border-bottom p-4"> <div class="offcanvas-header border-bottom p-4">
<div> <div>
<h5 id="describeModalLabel"></h5> <h5 id="describeModalLabel"></h5>
@@ -276,21 +318,53 @@
</div> </div>
</div> </div>
<div class="modal" id="repoAddModal" tabindex="-1">
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Chart Repository</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form enctype="application/x-www-form-urlencoded">
<label class="form-label">Name: <input class="form-control" name="name"></label>
<label class="form-label">URL: <input class="form-control" name="url"></label>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-confirm">Add Repository</button>
</div>
</div>
</div>
</div>
<div class="modal" id="upgradeModal" tabindex="-1" aria-labelledby="describeModalLabel" aria-hidden="true"> <div class="modal" id="upgradeModal" tabindex="-1">
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl"> <div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="upgradeModalLabel"> <h4 class="modal-title" id="upgradeModalLabel">
Upgrade <b class='text-success name'></b> Install <b class='text-success name'></b>
</h4> </h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form class="modal-body border-bottom fs-5" enctype="multipart/form-data"> <form class="modal-body border-bottom fs-5" enctype="multipart/form-data">
<div class="input-group mb-3 text-muted"> <div class="input-group mb-3 text-muted">
<label class="form-label me-4 text-dark">Version to install: <select <label class="form-label me-4 text-dark">Version to install: <select
class='fw-bold text-success ver-new'></select></label> (current version is <span class='fw-bold text-success ver-new'></select></label> <span class="ver-old">(current version is <span
class='text-success ver-old ms-1'>0.0.0</span>) class='text-success ms-1'>0.0.0</span>)</span>
</div>
<div class="input-group mb-3 text-muted">
<label class="form-label me-4 text-dark">
Release Name: <input class="form-control rel-name">
</label>
<label class="form-label me-4 text-dark">
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>
<div class="row"> <div class="row">
<div class="col-6 pe-3"> <div class="col-6 pe-3">
@@ -302,7 +376,8 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-6 pe-3"> <div class="col-6 pe-3">
<textarea name="values" class="form-control w-100 h-100" rows="5"></textarea> <textarea name="values" class="form-control w-100 h-100" rows="5"
style="font-family: monospace"></textarea>
</div> </div>
<div class="col-6 ps-3"> <div class="col-6 ps-3">
<pre class="ref-vals fs-6 w-100 bg-secondary p-2 rounded" style="max-height: 20rem"></pre> <pre class="ref-vals fs-6 w-100 bg-secondary p-2 rounded" style="max-height: 20rem"></pre>
@@ -314,11 +389,13 @@
<span class="invalid-feedback small mb-3"> (wrong YAML)</span> <span class="invalid-feedback small mb-3"> (wrong YAML)</span>
</div> </div>
</div> </div>
<label class="form-label mt-5">Manifest changes:</label> <label class="form-label mt-4">Manifest changes:</label>
<div id="upgradeModalBody" class="small"></div> <div id="upgradeModalBody" class="small"></div>
</form> </form>
<div class="modal-footer"> <div class="modal-footer d-flex">
<button type="button" class="btn btn-primary btn-confirm">Confirm Upgrade</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>
</div> </div>
</div> </div>
@@ -336,6 +413,8 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"
integrity="sha512-CSBhVREyzHAjAFfBlIBakjoRUKp5h7VSweP0InR/pAJyptH7peuhCsqAI/snV+TwZmXZqoUklpXp6R6wMnYf5Q==" integrity="sha512-CSBhVREyzHAjAFfBlIBakjoRUKp5h7VSweP0InR/pAJyptH7peuhCsqAI/snV+TwZmXZqoUklpXp6R6wMnYf5Q=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script> crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="static/repo.js"></script>
<script src="static/list-view.js"></script> <script src="static/list-view.js"></script>
<script src="static/revisions-view.js"></script> <script src="static/revisions-view.js"></script>
<script src="static/details-view.js"></script> <script src="static/details-view.js"></script>

View File

@@ -12,10 +12,13 @@ function loadChartsList() {
let card = buildChartCard(elm); let card = buildChartCard(elm);
chartsCards.append(card) chartsCards.append(card)
}) })
if (!data.length) {
$("#installedList .no-charts").show()
}
}) })
} }
function buildChartCard(elm) { function buildChartCard(elm) {
const card = $(`<div class="row m-0 py-3 bg-white rounded-1 b-shadow border-4 border-start"> 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> <div class="col-4 rel-name"><span class="link">release-name</span><div></div></div>

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

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg <svg
width="32" width="179"
height="37" height="164"
viewBox="0 0 32 37" viewBox="0 0 179 164"
fill="none" fill="none"
version="1.1" version="1.1"
id="svg4" id="svg20"
sodipodi:docname="logo.svg" sodipodi:docname="logo.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
@@ -13,9 +13,9 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"> xmlns:svg="http://www.w3.org/2000/svg">
<defs <defs
id="defs8" /> id="defs24" />
<sodipodi:namedview <sodipodi:namedview
id="namedview6" id="namedview22"
pagecolor="#ffffff" pagecolor="#ffffff"
bordercolor="#666666" bordercolor="#666666"
borderopacity="1.0" borderopacity="1.0"
@@ -23,27 +23,70 @@
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0" inkscape:pagecheckerboard="0"
showgrid="false" showgrid="false"
inkscape:zoom="34.810811" inkscape:zoom="3.9268293"
inkscape:cx="13.228649" inkscape:cx="39.090062"
inkscape:cy="17.552019" inkscape:cy="109.24845"
inkscape:window-width="3840" inkscape:window-width="3840"
inkscape:window-height="2059" inkscape:window-height="2059"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="svg4" /> inkscape:current-layer="svg20" />
<rect <rect
style="opacity:0.455635;fill:#ffffff;stroke-width:18.0094;fill-opacity:1" style="fill:#ffffff;fill-opacity:1;stroke-width:22.5327"
id="rect1031" id="rect967"
width="17.224331" width="113.34328"
height="10.929513" height="68.482964"
x="7.3572245" x="32.284447"
y="12.500892" y="47.989918" />
ry="0" /> <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 <path
fill-rule="evenodd" fill-rule="evenodd"
clip-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" 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> </svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,134 @@
function loadRepoView() {
$("#sectionRepo .repo-details").hide()
$("#sectionRepo").show()
$.getJSON("/api/helm/repo").fail(function (xhr) {
reportError("Failed to get list of repositories", xhr)
}).done(function (data) {
const items = $("#sectionRepo .repo-list ul").empty()
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)
opt.find("input").val(elm.name).text(elm.name).data("item", elm)
opt.find("span").text(elm.name)
items.append(opt)
})
if (!data.length) {
items.text("No repositories found, try adding one")
}
items.find("input").click(function () {
$("#inputSearch").val('')
const self = $(this)
const elm = self.data("item");
setHashParam("repo", elm.name)
$("#sectionRepo .repo-details").show()
$("#sectionRepo .repo-details h2").text(elm.name)
$("#sectionRepo .repo-details .url").text(elm.url)
$("#sectionRepo .repo-details ul").html('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>')
$.getJSON("/api/helm/repo/charts?name=" + elm.name).fail(function (xhr) {
reportError("Failed to get list of charts in repo", xhr)
}).done(function (data) {
$("#sectionRepo .repo-details ul").empty()
data.forEach(function (elm) {
const li = $(`<li class="row p-2 rounded">
<h6 class="col-3 py-2">` + elm.name.split('/').pop() + `</h6>
<div class="col py-2">` + elm.description + `</div>
<div class="col-1 py-2">` + elm.version + `</div>
<div class="col-1 action text-nowrap"><button class="btn btn-sm border-secondary bg-white">Install</button></div>
</li>`)
li.data("item", elm)
if (elm.installed_namespace) {
li.find("button").text("View").addClass("btn-success").removeClass("bg-white")
li.find(".action").prepend("<i class='bi-check-circle-fill me-1 text-success' title='Already installed'></i>")
}
li.click(repoChartClicked)
$("#sectionRepo .repo-details ul").append(li)
})
})
})
if (getHashParam("repo")) {
items.find("input[value='" + getHashParam("repo") + "']").click()
} else {
items.find("input").first().click()
}
})
}
$("#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()
})
$("#repoAddModal .btn-confirm").click(function () {
$("#repoAddModal .btn-confirm").prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>')
$.ajax({
type: 'POST',
url: "/api/helm/repo",
data: $("#repoAddModal form").serialize(),
}).fail(function (xhr) {
reportError("Failed to add repo", xhr)
}).done(function () {
setHashParam("repo", $("#repoAddModal form input[name=name]").val())
window.location.reload()
})
})
$("#sectionRepo .btn-remove").click(function () {
if (confirm("Confirm removing repository?")) {
$.ajax({
type: 'DELETE',
url: "/api/helm/repo?name=" + $("#sectionRepo .repo-details h2").text(),
}).fail(function (xhr) {
reportError("Failed to add repo", xhr)
}).done(function () {
setHashParam("repo", null)
window.location.reload()
})
}
})
$("#sectionRepo .btn-update").click(function () {
$("#sectionRepo .btn-update i").removeClass("bi-arrow-repeat").append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
$.ajax({
type: 'POST',
url: "/api/helm/repo/update?name=" + $("#sectionRepo .repo-details h2").text(),
}).fail(function (xhr) {
reportError("Failed to add repo", xhr)
}).done(function () {
window.location.reload()
})
})
function repoChartClicked() {
const self = $(this)
const elm = self.data("item")
if (elm.installed_namespace) {
setHashParam("section", null)
setHashParam("namespace", elm.installed_namespace)
setHashParam("chart", elm.installed_name)
window.location.reload()
} else {
popUpUpgrade(elm)
}
}

View File

@@ -53,9 +53,9 @@ function fillChartHistory(data, namespace, name) {
const nxt = data[x + 1]; const nxt = data[x + 1];
if (nxt && isNewerVersion(elm.chart_ver, nxt.chart_ver)) { 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)) { } 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.data("elm", elm)

View File

@@ -11,6 +11,37 @@ $(function () {
const context = getHashParam("context") const context = getHashParam("context")
fillClusterList(data, context); fillClusterList(data, context);
initView(); // can only do it after loading cluster list
})
$.getJSON("/api/scanners").fail(function (xhr) {
reportError("Failed to get list of scanners", xhr)
}).done(function (data) {
$("body").data("scanners", data)
for (let k in data) {
if (data[k].ManifestScannable) {
$("#upgradeModal .btn-scan").show() // TODO: move this to install flow
}
}
})
$.getJSON("/status").fail(function (xhr) {
reportError("Failed to get tool version", xhr)
}).done(function (data) {
fillToolVersion(data)
$("#limitNamespace").show().find("span").text(data.LimitedToNamespace)
})
})
function initView() {
$(".section").hide()
const section = getHashParam("section")
if (section === "repository") {
$("#topNav ul a.section-repo").addClass("active")
loadRepoView()
} else {
$("#topNav ul a.section-installed").addClass("active")
const namespace = getHashParam("namespace") const namespace = getHashParam("namespace")
const chart = getHashParam("chart") const chart = getHashParam("chart")
if (!chart) { if (!chart) {
@@ -18,9 +49,31 @@ $(function () {
} else { } else {
loadChartHistory(namespace, chart) loadChartHistory(namespace, chart)
} }
}) }
}) }
$("#topNav ul a").click(function () {
const self = $(this)
if (self.hasClass("section-help")) {
return;
}
$("#topNav ul a").removeClass("active")
const ctx = getHashParam("context")
setHashParam(null, null)
setHashParam("context", ctx)
if (self.hasClass("section-repo")) {
setHashParam("section", "repository")
} else if (self.hasClass("section-installed")) {
setHashParam("section", null)
} else {
return
}
initView()
})
const myAlert = document.getElementById('errorAlert') const myAlert = document.getElementById('errorAlert')
myAlert.addEventListener('close.bs.alert', event => { myAlert.addEventListener('close.bs.alert', event => {
@@ -43,8 +96,14 @@ function getHashParam(name) {
} }
function setHashParam(name, val) { function setHashParam(name, val) {
const params = new URLSearchParams(window.location.hash.substring(1)) let params = new URLSearchParams(window.location.hash.substring(1))
if (!name) {
params = new URLSearchParams()
} else if (!val) {
params.delete(name)
} else {
params.set(name, val) params.set(name, val)
}
window.location.hash = new URLSearchParams(params).toString() window.location.hash = new URLSearchParams(params).toString()
} }
@@ -69,14 +128,14 @@ function statusStyle(status, card, txt) {
} }
function getCleanClusterName(rawClusterName) { function getCleanClusterName(rawClusterName) {
if (rawClusterName.indexOf('arn')==0) { if (rawClusterName.indexOf('arn') === 0) {
// AWS cluster // AWS cluster
clusterSplit = rawClusterName.split(':') const clusterSplit = rawClusterName.split(':')
clusterName = clusterSplit.at(-1).split("/").at(-1) const clusterName = clusterSplit.at(-1).split("/").at(-1)
region = clusterSplit.at(-3) const region = clusterSplit.at(-3)
return region + "/" + clusterName + ' [AWS]' return region + "/" + clusterName + ' [AWS]'
} }
if (rawClusterName.indexOf('gke')==0) { if (rawClusterName.indexOf('gke') === 0) {
// GKE cluster // GKE cluster
return rawClusterName.split('_').at(-2) + '/' + rawClusterName.split('_').at(-1) + ' [GKE]' return rawClusterName.split('_').at(-2) + '/' + rawClusterName.split('_').at(-1) + ' [GKE]'
} }
@@ -85,13 +144,11 @@ function getCleanClusterName(rawClusterName) {
function fillClusterList(data, context) { function fillClusterList(data, context) {
data.forEach(function (elm) { data.forEach(function (elm) {
// aws CLI uses complicated context names, the suffix does not work well let label = getCleanClusterName(elm.Name)
// maybe we should have an `if` statement here
let label = elm.Name //+ " (" + elm.Cluster + "/" + elm.AuthInfo + "/" + elm.Namespace + ")"
let opt = $('<li><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>'); let opt = $('<li><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
opt.attr('title', label) opt.attr('title', elm.Name)
opt.find("input").val(elm.Name).text(label) opt.find("input").val(elm.Name).text(label)
opt.find("span").text(getCleanClusterName(label)) opt.find("span").text(label)
if (elm.IsCurrent && !context) { if (elm.IsCurrent && !context) {
opt.find("input").prop("checked", true) opt.find("input").prop("checked", true)
setCurrentContext(elm.Name) setCurrentContext(elm.Name)
@@ -144,6 +201,14 @@ $(".bi-power").click(function () {
}) })
function isNewerVersion(oldVer, newVer) { 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 oldParts = oldVer.split('.')
const newParts = newVer.split('.') const newParts = newVer.split('.')
for (let i = 0; i < newParts.length; i++) { for (let i = 0; i < newParts.length; i++) {
@@ -154,3 +219,11 @@ function isNewerVersion(oldVer, newVer) {
} }
return false return false
} }
function fillToolVersion(data) {
$("#toolVersion").text(data.CurVer)
if (isNewerVersion(data.CurVer, data.LatestVer)) {
$("#toolVersionUpgrade").text(data.LatestVer)
$(".upgrade-possible").show()
}
}

View 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;
}

View File

@@ -1,77 +1,3 @@
.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 { html {
min-height: 100%; min-height: 100%;
} }
@@ -100,22 +26,11 @@ body > .container-fluid {
min-height: 100% !important; 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 { .navbar-brand > a > img {
vertical-align: middle; vertical-align: middle;
height: 2.5rem; height: 3rem;
display: inline-block; display: inline-block;
margin: 0.5rem 0.8rem margin: 0.25rem 0.75rem
} }
.navbar-brand > div { .navbar-brand > div {
@@ -152,6 +67,10 @@ body > .container-fluid {
color: #3B3D45 !important; color: #3B3D45 !important;
} }
#topNav .nav-link.text-danger {
color: #FC1683 !important;
}
#topNav .nav-link.active { #topNav .nav-link.active {
background: #EBEFFF; background: #EBEFFF;
border-radius: 2px; border-radius: 2px;
@@ -230,6 +149,10 @@ span.link {
text-decoration: underline; text-decoration: underline;
} }
#installedList .body .row div {
overflow: hidden;
}
#installedList .rel-name { #installedList .rel-name {
padding-left: 5.5rem; padding-left: 5.5rem;
background-image: url("helm-gray-50.svg"); background-image: url("helm-gray-50.svg");
@@ -284,17 +207,17 @@ span.link {
position: static; position: static;
} }
.nav-tabs { nav .nav-tabs {
border: none; border: none;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.nav-tabs .nav-link { nav .nav-tabs .nav-link {
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
color: #3B3D45; color: #3B3D45;
} }
.nav-tabs .nav-link.active { nav .nav-tabs .nav-link.active {
border: none; border: none;
border-bottom: 3px solid #3B3D45; border-bottom: 3px solid #3B3D45;
background-color: transparent; background-color: transparent;
@@ -350,3 +273,15 @@ span.link {
#describeModalBody pre { #describeModalBody pre {
font-size: 1rem; font-size: 1rem;
} }
#sectionRepo .repo-details ul .row .btn {
visibility: hidden;
}
#sectionRepo .repo-details ul .row:hover {
background-color: #F4F7FA !important;
}
#sectionRepo .repo-details ul .row:hover .btn {
visibility: visible;
}

View File

@@ -1,81 +1,50 @@
package dashboard package subproc
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/release"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"os"
"os/exec"
"regexp" "regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/release"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
) )
type CmdError struct {
Command []string
OrigError error
StdErr []byte
}
func (e CmdError) Error() string {
//return fmt.Sprintf("failed to run command %s:\nError: %s\nSTDERR:%s", e.Command, e.OrigError, e.StdErr)
return string(e.StdErr)
}
type DataLayer struct { type DataLayer struct {
KubeContext string KubeContext string
Helm string Helm string
Kubectl string Kubectl string
Scanners []Scanner
StatusInfo *StatusInfo
Namespace string
}
type StatusInfo struct {
CurVer string
LatestVer string
Analytics bool
LimitedToNamespace string
} }
func (d *DataLayer) runCommand(cmd ...string) (string, error) { func (d *DataLayer) runCommand(cmd ...string) (string, error) {
log.Debugf("Starting command: %s", cmd) for i, c := range cmd {
prog := exec.Command(cmd[0], cmd[1:]...) if c == "--namespace" && i < len(cmd) { // TODO: in case it's not found - add it?
prog.Env = os.Environ() d.forceNamespace(&cmd[i+1])
prog.Env = append(prog.Env, "HELM_KUBECONTEXT="+d.KubeContext)
var stdout bytes.Buffer
prog.Stdout = &stdout
var stderr bytes.Buffer
prog.Stderr = &stderr
if err := prog.Run(); err != nil {
log.Warnf("Failed command: %s", cmd)
serr := stderr.Bytes()
if serr != nil {
log.Warnf("STDERR:\n%s", serr)
}
if eerr, ok := err.(*exec.ExitError); ok {
return "", CmdError{
Command: cmd,
StdErr: serr,
OrigError: eerr,
} }
} }
return "", CmdError{ return utils.RunCommand(cmd, map[string]string{"HELM_KUBECONTEXT": d.KubeContext})
Command: cmd,
StdErr: serr,
OrigError: err,
}
}
sout := stdout.Bytes()
serr := stderr.Bytes()
log.Debugf("Command STDOUT:\n%s", sout)
log.Debugf("Command STDERR:\n%s", serr)
return string(sout), nil
} }
func (d *DataLayer) runCommandHelm(cmd ...string) (string, error) { func (d *DataLayer) runCommandHelm(cmd ...string) (string, error) {
@@ -92,7 +61,6 @@ func (d *DataLayer) runCommandHelm(cmd ...string) (string, error) {
} }
func (d *DataLayer) runCommandKubectl(cmd ...string) (string, error) { func (d *DataLayer) runCommandKubectl(cmd ...string) (string, error) {
// TODO: migrate into using kubectl "k8s.io/kubectl/pkg/cmd" and kube API
if d.Kubectl == "" { if d.Kubectl == "" {
d.Kubectl = "kubectl" d.Kubectl = "kubectl"
} }
@@ -106,6 +74,12 @@ func (d *DataLayer) runCommandKubectl(cmd ...string) (string, error) {
return d.runCommand(cmd...) return d.runCommand(cmd...)
} }
func (d *DataLayer) forceNamespace(s *string) {
if d.Namespace != "" {
*s = d.Namespace
}
}
func (d *DataLayer) CheckConnectivity() error { func (d *DataLayer) CheckConnectivity() error {
contexts, err := d.ListContexts() contexts, err := d.ListContexts()
if err != nil { if err != nil {
@@ -166,46 +140,16 @@ func (d *DataLayer) ListContexts() (res []KubeContext, err error) {
return res, nil return res, nil
} }
func (d *DataLayer) ListInstalled() (res []releaseElement, err error) { func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) {
out, err := d.runCommandHelm("ls", "--all", "--all-namespaces", "--output", "json", "--time-format", time.RFC3339) cmd := []string{"ls", "--all", "--output", "json", "--time-format", time.RFC3339}
if err != nil {
return nil, err // TODO: filter by namespace
if d.Namespace == "" {
cmd = append(cmd, "--all-namespaces")
} else {
cmd = append(cmd, "--namespace", d.Namespace)
} }
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
return res, nil
}
func (d *DataLayer) ChartHistory(namespace string, chartName 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")
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
for _, elm := range res {
chartRepoName, curVer, err := chartAndVersion(elm.Chart)
if err != nil {
return nil, err
}
elm.ChartName = chartRepoName
elm.ChartVer = curVer
elm.Updated.Time = elm.Updated.Time.Round(time.Second)
}
return res, nil
}
func (d *DataLayer) ChartRepoVersions(chartName string) (res []repoChartElement, err error) {
cmd := []string{"search", "repo", "--regexp", "/" + chartName + "\v", "--versions", "--output", "json"}
out, err := d.runCommandHelm(cmd...) out, err := d.runCommandHelm(cmd...)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -218,6 +162,91 @@ func (d *DataLayer) ChartRepoVersions(chartName string) (res []repoChartElement,
return res, nil return res, nil
} }
func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*HistoryElement, err error) {
// TODO: there is `max` but there is no `offset`
out, err := d.runCommandHelm("history", chartName, "--namespace", namespace, "--output", "json")
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
for _, elm := range res {
chartRepoName, curVer, err := utils.ChartAndVersion(elm.Chart)
if err != nil {
return nil, err
}
elm.ChartName = chartRepoName // TODO: move it to frontend?
elm.ChartVer = curVer
elm.Updated.Time = elm.Updated.Time.Round(time.Second)
}
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 {
// TODO: there can be more than one
chart.InstalledNamespace = rel.Namespace
chart.InstalledName = rel.Name
}
}
}
}
type SectionFn = func(string, string, int, bool) (string, error) // TODO: rework it into struct-based argument? 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) { func (d *DataLayer) RevisionManifests(namespace string, chartName string, revision int, _ bool) (res string, err error) {
@@ -257,6 +286,11 @@ func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string,
return nil, err return nil, err
} }
if doc.Kind == "" {
log.Warnf("Manifest piece is not k8s resource: %s", jsoned)
continue
}
res = append(res, &doc) res = append(res, &doc)
} }
@@ -328,6 +362,15 @@ func (d *DataLayer) GetResource(namespace string, def *v1.Carp) (*v1.Carp, error
return &res, nil 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) { func (d *DataLayer) DescribeResource(namespace string, kind string, name string) (string, error) {
out, err := d.runCommandKubectl("describe", strings.ToLower(kind), name, "--namespace", namespace) out, err := d.runCommandKubectl("describe", strings.ToLower(kind), name, "--namespace", namespace)
if err != nil { if err != nil {
@@ -336,7 +379,7 @@ func (d *DataLayer) DescribeResource(namespace string, kind string, name string)
return out, nil return out, nil
} }
func (d *DataLayer) UninstallChart(namespace string, name string) error { func (d *DataLayer) ChartUninstall(namespace string, name string) error {
_, err := d.runCommandHelm("uninstall", name, "--namespace", namespace) _, err := d.runCommandHelm("uninstall", name, "--namespace", namespace)
if err != nil { if err != nil {
return err return err
@@ -366,8 +409,8 @@ func (d *DataLayer) ChartRepoUpdate(name string) error {
return nil return nil
} }
func (d *DataLayer) ChartUpgrade(namespace string, name string, repoChart string, version string, justTemplate bool, values string) (string, error) { func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string, version string, justTemplate bool, values string, reuseVals bool) (string, error) {
if values == "" { if values == "" && reuseVals {
oldVals, err := d.RevisionValues(namespace, name, 0, true) oldVals, err := d.RevisionValues(namespace, name, 0, true)
if err != nil { if err != nil {
return "", err return "", err
@@ -375,13 +418,13 @@ func (d *DataLayer) ChartUpgrade(namespace string, name string, repoChart string
values = oldVals values = oldVals
} }
oldValsFile, close1, err := tempFile(values) valsFile, close1, err := utils.TempFile(values)
defer close1() defer close1()
if err != nil { if err != nil {
return "", err return "", err
} }
cmd := []string{"upgrade", name, repoChart, "--version", version, "--namespace", namespace, "--values", oldValsFile, "--output", "json"} cmd := []string{"upgrade", "--install", "--create-namespace", name, repoChart, "--version", version, "--namespace", namespace, "--values", valsFile, "--output", "json"}
if justTemplate { if justTemplate {
cmd = append(cmd, "--dry-run") cmd = append(cmd, "--dry-run")
} }
@@ -390,26 +433,13 @@ func (d *DataLayer) ChartUpgrade(namespace string, name string, repoChart string
if err != nil { if err != nil {
return "", err return "", err
} }
res := release.Release{}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return "", err
}
if justTemplate { if justTemplate {
res := release.Release{} out = strings.TrimSpace(res.Manifest)
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return "", err
}
manifests, err := d.RevisionManifests(namespace, name, 0, false)
if err != nil {
return "", err
}
out = getDiff(strings.TrimSpace(manifests), strings.TrimSpace(res.Manifest), "current.yaml", "upgraded.yaml")
} else {
res := release.Release{}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return "", err
}
_ = res
} }
return out, nil return out, nil
@@ -419,6 +449,51 @@ func (d *DataLayer) ShowValues(chart string, ver string) (string, error) {
return d.runCommandHelm("show", "values", chart, "--version", ver) 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 (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 RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) { func RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) {
if revision1 == 0 || revision2 == 0 { if revision1 == 0 || revision2 == 0 {
log.Debugf("One of revisions is zero: %d %d", revision1, revision2) log.Debugf("One of revisions is zero: %d %d", revision1, revision2)
@@ -435,11 +510,11 @@ func RevisionDiff(functor SectionFn, ext string, namespace string, name string,
return "", err return "", err
} }
diff := getDiff(manifest1, manifest2, strconv.Itoa(revision1)+ext, strconv.Itoa(revision2)+ext) diff := GetDiff(manifest1, manifest2, strconv.Itoa(revision1)+ext, strconv.Itoa(revision2)+ext)
return diff, nil return diff, nil
} }
func getDiff(text1 string, text2 string, name1 string, name2 string) string { func GetDiff(text1 string, text2 string, name1 string, name2 string) string {
edits := myers.ComputeEdits(span.URIFromPath(""), text1, text2) edits := myers.ComputeEdits(span.URIFromPath(""), text1, text2)
unified := gotextdiff.ToUnified(name1, name2, text1, edits) unified := gotextdiff.ToUnified(name1, name2, text1, edits)
diff := fmt.Sprint(unified) diff := fmt.Sprint(unified)

View File

@@ -1,6 +1,7 @@
package dashboard package subproc
import ( import (
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1" v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
@@ -45,7 +46,7 @@ func TestFlow(t *testing.T) {
} }
_ = history _ = history
chartRepoName, curVer, err := chartAndVersion(chart.Chart) chartRepoName, curVer, err := utils.ChartAndVersion(chart.Chart)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -1,4 +1,4 @@
package dashboard package subproc
import ( import (
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
@@ -6,7 +6,8 @@ import (
) )
// unpleasant copy from Helm sources, where they have it non-public // unpleasant copy from Helm sources, where they have it non-public
type releaseElement struct {
type ReleaseElement struct {
Name string `json:"name"` Name string `json:"name"`
Namespace string `json:"namespace"` Namespace string `json:"namespace"`
Revision string `json:"revision"` Revision string `json:"revision"`
@@ -16,20 +17,37 @@ type releaseElement struct {
AppVersion string `json:"app_version"` AppVersion string `json:"app_version"`
} }
type historyElement struct { type HistoryElement struct {
Revision int `json:"revision"` Revision int `json:"revision"`
Updated helmtime.Time `json:"updated"` Updated helmtime.Time `json:"updated"`
Status release.Status `json:"status"` Status release.Status `json:"status"`
Chart string `json:"chart"` Chart string `json:"chart"`
AppVersion string `json:"app_version"` AppVersion string `json:"app_version"`
Description string `json:"description"` 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 { type RepoChartElement struct {
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
AppVersion string `json:"app_version"` AppVersion string `json:"app_version"`
Description string `json:"description"` Description string `json:"description"`
InstalledNamespace string `json:"installed_namespace"` // custom addition on top of Helm
InstalledName string `json:"installed_name"` // custom addition on top of Helm
}
type RepositoryElement struct {
Name string `json:"name"`
URL string `json:"url"`
}
type NamespaceElement struct {
Items []struct {
Metadata struct {
Name string `json:"name"`
} `json:"metadata"`
} `json:"items"`
} }

View File

@@ -0,0 +1,17 @@
package subproc
type Scanner interface {
Name() string // returns string label for the scanner
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 {
PassedCount int
FailedCount int
OrigReport interface{}
Error error
}

View File

@@ -1,33 +0,0 @@
package dashboard
import (
"errors"
"io/ioutil"
"os"
"strings"
)
type ControlChan = chan struct{}
func chartAndVersion(x string) (string, string, error) {
lastInd := strings.LastIndex(x, "-")
if lastInd < 0 {
return "", "", errors.New("can't parse chart version string")
}
return x[:lastInd], x[lastInd+1:], nil
}
func tempFile(txt string) (string, func(), error) {
file, err := ioutil.TempFile("", "helm_vals_")
if err != nil {
return "", nil, err
}
err = ioutil.WriteFile(file.Name(), []byte(txt), 0600)
if err != nil {
return "", nil, err
}
return file.Name(), func() { os.Remove(file.Name()) }, nil
}

View File

@@ -0,0 +1,118 @@
package utils
import (
"bytes"
"errors"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"io/ioutil"
"os"
"os/exec"
"strconv"
"strings"
)
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 {
return "", "", errors.New("can't parse chart version string")
}
return x[:lastInd], x[lastInd+1:], nil
}
func TempFile(txt string) (string, func(), error) {
file, err := ioutil.TempFile("", "helm_dahsboard_*.yaml")
if err != nil {
return "", nil, err
}
err = ioutil.WriteFile(file.Name(), []byte(txt), 0600)
if err != nil {
return "", nil, err
}
return file.Name(), func() { _ = os.Remove(file.Name()) }, nil
}
type CmdError struct {
Command []string
OrigError error
StdErr string
}
func (e CmdError) Error() string {
//return fmt.Sprintf("failed to run command %s:\nError: %s\nSTDERR:%s", e.Command, e.OrigError, e.StdErr)
return string(e.StdErr)
}
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()
for k, v := range env {
prog.Env = append(prog.Env, k+"="+v)
}
var stdout bytes.Buffer
prog.Stdout = &stdout
var stderr bytes.Buffer
prog.Stderr = &stderr
if err := prog.Run(); err != nil {
log.StandardLogger().Logf(FailLogLevel, "Failed command: %s", cmd)
serr := stderr.Bytes()
if serr != nil {
log.StandardLogger().Logf(FailLogLevel, "STDERR:\n%s", serr)
}
if eerr, ok := err.(*exec.ExitError); ok {
return "", CmdError{
Command: cmd,
StdErr: string(serr),
OrigError: eerr,
}
}
return "", CmdError{
Command: cmd,
StdErr: string(serr),
OrigError: err,
}
}
sout := stdout.Bytes()
serr := stderr.Bytes()
log.Debugf("Command STDOUT:\n%s", sout)
log.Debugf("Command STDERR:\n%s", serr)
return string(sout), nil
}
type QueryProps struct {
Namespace string
Name string
Revision int
}
func GetQueryProps(c *gin.Context, revRequired bool) (*QueryProps, error) {
qp := QueryProps{}
qp.Namespace = c.Query("namespace")
qp.Name = c.Query("name")
if qp.Name == "" {
return nil, errors.New("missing required query string parameter: name")
}
cRev, err := strconv.Atoi(c.Query("revision"))
if err != nil && revRequired {
return nil, err
}
qp.Revision = cRev
return &qp, nil
}

View File

@@ -0,0 +1,96 @@
package utils
import (
"github.com/gin-gonic/gin"
"net/http/httptest"
"testing"
)
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 - 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)
}
})
}
}

View File

@@ -1,8 +1,9 @@
name: "dashboard" name: "dashboard"
version: "0.1.1" version: "0.2.5"
usage: "A simplified way of working with Helm" usage: "A simplified way of working with Helm"
description: "View HELM situation in nice web UI" description: "View HELM situation in nice web UI"
command: "$HELM_PLUGIN_DIR/bin/helm-dashboard" command: "$HELM_PLUGIN_DIR/bin/helm-dashboard"
ignoreFlags: false
hooks: hooks:
install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh" install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh" update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -62,3 +62,8 @@ fi
tar xzf "releases/v${version}.tar.gz" -C "releases/v${version}" tar xzf "releases/v${version}.tar.gz" -C "releases/v${version}"
mv "releases/v${version}/${name}" "bin/${name}" || \ mv "releases/v${version}/${name}" "bin/${name}" || \
mv "releases/v${version}/${name}.exe" "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