mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-26 14:28:04 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2221fb22a0 | ||
|
|
9dc3e6a12d | ||
|
|
de0024cd03 | ||
|
|
ed4e970194 | ||
|
|
b0067e31ba | ||
|
|
7e8ba4709e | ||
|
|
be7b2642fc | ||
|
|
896d9e3f72 | ||
|
|
be6666373b | ||
|
|
91df9392c0 | ||
|
|
bd058ee912 | ||
|
|
997f951d0c | ||
|
|
09886ad933 | ||
|
|
0de0b5d0cb | ||
|
|
0141eecef1 | ||
|
|
65ecc20c90 | ||
|
|
f86a4a93a7 |
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal 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
|
||||||
|
https://komodorkommunity.slack.com/archives/C044U1B0265.
|
||||||
|
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.
|
||||||
90
README.md
90
README.md
@@ -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,18 +42,23 @@ To uninstall, run:
|
|||||||
helm plugin uninstall dashboard
|
helm plugin uninstall dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: 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.
|
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 your port 8080 is busy, you can specify a different port to use via `HD_PORT` environment variable.
|
If your port 8080 is busy, you can specify a different port to use via `HD_PORT` environment variable.
|
||||||
|
|
||||||
@@ -48,53 +66,26 @@ If you don't want browser tab to automatically open, set `HD_NOBROWSER=1` in you
|
|||||||
|
|
||||||
If you want to increase the logging verbosity and see all the debug info, set `DEBUG=1` environment variable.
|
If you want to increase the logging verbosity and see all the debug info, set `DEBUG=1` environment variable.
|
||||||
|
|
||||||
|
## 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:
|
||||||
|

|
||||||
|
|
||||||
|
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:
|
||||||
|

|
||||||
|
|
||||||
## 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://komodorkommunity.slack.com/archives/C044U1B0265) 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 +96,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:
|
||||||
|
|||||||
81
go.mod
81
go.mod
@@ -3,104 +3,43 @@ 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/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||||
github.com/toqueteos/webbrowser v1.2.0
|
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/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/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
|
|
||||||
)
|
)
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -3,8 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ func main() {
|
|||||||
|
|
||||||
if os.Getenv("HD_NOBROWSER") == "" {
|
if os.Getenv("HD_NOBROWSER") == "" {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ package dashboard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
|
||||||
"github.com/gin-gonic/gin"
|
"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"
|
log "github.com/sirupsen/logrus"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed static/*
|
//go:embed static/*
|
||||||
@@ -33,7 +34,17 @@ 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) *gin.Engine {
|
||||||
var api *gin.Engine
|
var api *gin.Engine
|
||||||
if os.Getenv("DEBUG") == "" {
|
if os.Getenv("DEBUG") == "" {
|
||||||
api = gin.New()
|
api = gin.New()
|
||||||
@@ -47,12 +58,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,29 +71,37 @@ 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.IndentedJSON(http.StatusOK, data.VersionInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
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)
|
||||||
@@ -118,36 +137,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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -23,12 +26,12 @@ func (h *HelmHandler) GetCharts(c *gin.Context) {
|
|||||||
// TODO: helm show chart komodorio/k8s-watcher to get the icon URL
|
// 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 +40,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 +55,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 +70,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 +85,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 +99,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 +130,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 +162,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 +187,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 +243,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
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
package dashboard
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"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"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
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 +23,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 +55,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
|
||||||
69
pkg/dashboard/handlers/scannerHandlers.go
Normal file
69
pkg/dashboard/handlers/scannerHandlers.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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) {
|
||||||
|
res := make([]string, 0)
|
||||||
|
for _, scanner := range h.Data.Scanners {
|
||||||
|
res = append(res, scanner.Name())
|
||||||
|
}
|
||||||
|
c.JSON(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)
|
||||||
|
}
|
||||||
107
pkg/dashboard/scanners/checkov.go
Normal file
107
pkg/dashboard/scanners/checkov.go
Normal 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"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Checkov struct {
|
||||||
|
Data *subproc.DataLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Checkov) Name() string {
|
||||||
|
return "Checkov"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Checkov) Test() bool {
|
||||||
|
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", "cli", "--file", fname}
|
||||||
|
out, err := utils.RunCommand(cmd, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &subproc.ScanResults{}
|
||||||
|
|
||||||
|
res.OrigReport = out
|
||||||
|
|
||||||
|
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", "cli", "--file", fname}
|
||||||
|
out, err := utils.RunCommand(cmd, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res := subproc.ScanResults{}
|
||||||
|
_, out, _ = strings.Cut(out, "\n") // kubernetes scan results:
|
||||||
|
_, out, _ = strings.Cut(out, "\n") // empty line
|
||||||
|
line, out, found := strings.Cut(out, "\n") // status line
|
||||||
|
if found {
|
||||||
|
parts := strings.FieldsFunc(line, func(r rune) bool {
|
||||||
|
return r == ':' || r == ','
|
||||||
|
})
|
||||||
|
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
|
||||||
|
res.PassedCount = cnt
|
||||||
|
} else {
|
||||||
|
log.Warnf("Failed to parse Checkov output: %s", err)
|
||||||
|
}
|
||||||
|
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[3])); err == nil {
|
||||||
|
res.FailedCount = cnt
|
||||||
|
} else {
|
||||||
|
log.Warnf("Failed to parse Checkov output: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Warnf("Failed to parse Checkov output")
|
||||||
|
}
|
||||||
|
|
||||||
|
res.OrigReport = strings.TrimSpace(out)
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckovResults struct {
|
||||||
|
Summary CheckovSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckovSummary struct {
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
Passed int `json:"passed"`
|
||||||
|
ResourceCount int `json:"resource_count"`
|
||||||
|
// parsing errors?
|
||||||
|
// skipped ?
|
||||||
|
}
|
||||||
87
pkg/dashboard/scanners/trivy.go
Normal file
87
pkg/dashboard/scanners/trivy.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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) Name() string {
|
||||||
|
return "Trivy"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Trivy) Test() bool {
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -2,20 +2,31 @@ 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"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartServer(version string) (string, ControlChan) {
|
func StartServer(version string) (string, utils.ControlChan) {
|
||||||
data := DataLayer{}
|
data := subproc.DataLayer{}
|
||||||
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.VersionInfo = &subproc.VersionInfo{CurVer: version}
|
||||||
|
go checkUpgrade(data.VersionInfo)
|
||||||
|
|
||||||
|
discoverScanners(&data)
|
||||||
|
|
||||||
address := os.Getenv("HD_BIND")
|
address := os.Getenv("HD_BIND")
|
||||||
if address == "" {
|
if address == "" {
|
||||||
address = "localhost"
|
address = "localhost"
|
||||||
@@ -27,15 +38,15 @@ func StartServer(version string) (string, ControlChan) {
|
|||||||
address += ":" + os.Getenv("HD_PORT")
|
address += ":" + os.Getenv("HD_PORT")
|
||||||
}
|
}
|
||||||
|
|
||||||
abort := make(ControlChan)
|
abort := make(utils.ControlChan)
|
||||||
api := NewRouter(abort, &data, version)
|
api := NewRouter(abort, &data)
|
||||||
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() {
|
||||||
@@ -56,3 +67,58 @@ func startBackgroundServer(addr string, routes *gin.Engine, abort ControlChan) C
|
|||||||
|
|
||||||
return done
|
return done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.VersionInfo) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 () {
|
||||||
@@ -30,17 +30,6 @@ function checkUpgradeable(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 + " ·")
|
|
||||||
} 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 +45,76 @@ 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)
|
||||||
|
|
||||||
|
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/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 + " ·")
|
||||||
|
} 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 +123,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 +134,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 +147,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 +155,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 +210,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 +231,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');
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
})(window,document,'script','https://www.datadoghq-browser-agent.com/datadog-rum-v4.js','DD_RUM')
|
})(window,document,'script','https://www.datadoghq-browser-agent.com/datadog-rum-v4.js','DD_RUM')
|
||||||
DD_RUM.onReady(function() {
|
DD_RUM.onReady(function() {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onload = function() {
|
||||||
const version = xhr.responseText;
|
const version = JSON.parse(xhr.responseText).VerCur;
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE && version!=="dev") {
|
if (xhr.readyState === XMLHttpRequest.DONE && version!=="dev") {
|
||||||
DD_RUM.init({
|
DD_RUM.init({
|
||||||
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
|
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
|
||||||
|
|||||||
@@ -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>');
|
||||||
@@ -156,9 +157,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"></div>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
@@ -182,16 +183,23 @@ 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") {
|
||||||
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())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 +220,42 @@ 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.")
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let name in data) {
|
||||||
|
const res = data[name]
|
||||||
|
|
||||||
|
if (!res.OrigReport) continue
|
||||||
|
const hdr = $("<h3>" + name + " Scan Results</h3>");
|
||||||
|
|
||||||
|
if (res.FailedCount) {
|
||||||
|
hdr.append("<span class='badge bg-danger ms-3'>" + res.FailedCount + " failed</span>")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.PassedCount) {
|
||||||
|
hdr.append("<span class='badge bg-info ms-3'>" + res.PassedCount + " passed</span>")
|
||||||
|
}
|
||||||
|
|
||||||
|
body.append(hdr)
|
||||||
|
|
||||||
|
const hl = hljs.highlight(res.OrigReport, {language: 'yaml'}).value
|
||||||
|
const pre = $("<pre class='bg-white rounded p-3' style='font-size: inherit; overflow: unset'></pre>").html(hl)
|
||||||
|
body.append(pre)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,26 +48,43 @@
|
|||||||
|
|
||||||
<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 dropdown">
|
||||||
<!-- TODO
|
<a class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown"
|
||||||
<li class="nav-item">
|
aria-expanded="false">
|
||||||
<a class="nav-link disabled">Provisional Charts</a>
|
<span class="position-absolute top-50 start-0 translate-middle p-1 bg-danger border border-light rounded-circle new-version-pill display-none">
|
||||||
|
<span class="visually-hidden">New version</span>
|
||||||
|
</span>
|
||||||
|
Help
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu fs-80">
|
||||||
|
<li><a class="dropdown-item" href="https://komodorkommunity.slack.com/archives/C044U1B0265"
|
||||||
|
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>
|
||||||
|
<li class="">
|
||||||
|
<a class="dropdown-item position-relative" href="https://github.com/komodorio/helm-dashboard#installing" target="_blank">
|
||||||
|
<span class="position-absolute top-50 start-0 translate-middle p-1 bg-danger border border-light rounded-circle new-version-pill display-none">
|
||||||
|
<span class="visually-hidden">New version</span>
|
||||||
|
</span>
|
||||||
|
Upgrade to <span id="toolVersionUpgrade"></span>
|
||||||
|
</a></li>
|
||||||
|
</ul>
|
||||||
|
</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 +92,48 @@
|
|||||||
</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>
|
||||||
|
</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="float-end">
|
||||||
|
<!-- TODO <input class="form-control form-control-sm" type="text" placeholder="Filter..."> -->
|
||||||
|
</div>
|
||||||
|
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
|
||||||
|
style="text-transform: uppercase">
|
||||||
|
<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"></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">
|
||||||
@@ -113,11 +171,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">
|
||||||
@@ -174,15 +235,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 +261,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 +286,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 +293,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 +303,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 +334,48 @@
|
|||||||
</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 class="form-control rel-ns">
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 pe-3">
|
<div class="col-6 pe-3">
|
||||||
@@ -302,7 +387,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 +400,12 @@
|
|||||||
<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">Scan for Problems</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-confirm">Confirm</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -336,6 +423,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
120
pkg/dashboard/static/repo.js
Normal file
120
pkg/dashboard/static/repo.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
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 () {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,33 @@ $(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) {
|
||||||
|
if (!data.length) {
|
||||||
|
$("#upgradeModal .btn-scan").hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$.getJSON("/status").fail(function (xhr) {
|
||||||
|
reportError("Failed to get tool version", xhr)
|
||||||
|
}).done(function (data) {
|
||||||
|
fillToolVersion(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 +45,28 @@ $(function () {
|
|||||||
} else {
|
} else {
|
||||||
loadChartHistory(namespace, chart)
|
loadChartHistory(namespace, chart)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
$("#topNav ul a").click(function () {
|
||||||
|
const self = $(this)
|
||||||
|
|
||||||
|
if (self.hasClass("section-repo")) {
|
||||||
|
setHashParam("section", "repository")
|
||||||
|
} else if (self.hasClass("section-installed")) {
|
||||||
|
setHashParam("section", null)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#topNav ul a").removeClass("active")
|
||||||
|
|
||||||
|
const ctx = getHashParam("context")
|
||||||
|
setHashParam(null, null)
|
||||||
|
setHashParam("context", ctx)
|
||||||
|
|
||||||
|
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 +89,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 +121,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 +137,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 +194,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 +212,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)
|
||||||
|
$(".new-version-pill").show()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.link, .nav-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.strike {
|
.strike {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
@@ -72,6 +76,10 @@
|
|||||||
min-width: 60%;
|
min-width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fs-80 {
|
||||||
|
font-size: 0.8rem!important;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
@@ -350,3 +358,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;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package dashboard
|
package subproc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -8,12 +8,11 @@ import (
|
|||||||
"github.com/hexops/gotextdiff"
|
"github.com/hexops/gotextdiff"
|
||||||
"github.com/hexops/gotextdiff/myers"
|
"github.com/hexops/gotextdiff/myers"
|
||||||
"github.com/hexops/gotextdiff/span"
|
"github.com/hexops/gotextdiff/span"
|
||||||
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"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"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -21,61 +20,23 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
|
VersionInfo *VersionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type VersionInfo struct {
|
||||||
|
CurVer string
|
||||||
|
LatestVer string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) runCommand(cmd ...string) (string, error) {
|
func (d *DataLayer) runCommand(cmd ...string) (string, error) {
|
||||||
log.Debugf("Starting command: %s", cmd)
|
log.Debugf("Starting command: %s", cmd)
|
||||||
prog := exec.Command(cmd[0], cmd[1:]...)
|
|
||||||
prog.Env = os.Environ()
|
|
||||||
prog.Env = append(prog.Env, "HELM_KUBECONTEXT="+d.KubeContext)
|
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
return utils.RunCommand(cmd, map[string]string{"HELM_KUBECONTEXT": d.KubeContext})
|
||||||
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{
|
|
||||||
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) {
|
||||||
@@ -166,7 +127,8 @@ 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) {
|
||||||
|
// TODO: filter by namespace
|
||||||
out, err := d.runCommandHelm("ls", "--all", "--all-namespaces", "--output", "json", "--time-format", time.RFC3339)
|
out, err := d.runCommandHelm("ls", "--all", "--all-namespaces", "--output", "json", "--time-format", time.RFC3339)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -179,7 +141,7 @@ func (d *DataLayer) ListInstalled() (res []releaseElement, err error) {
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*historyElement, err error) {
|
func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*HistoryElement, err error) {
|
||||||
// TODO: there is `max` but there is no `offset`
|
// TODO: there is `max` but there is no `offset`
|
||||||
out, err := d.runCommandHelm("history", chartName, "--namespace", namespace, "--output", "json", "--max", "18")
|
out, err := d.runCommandHelm("history", chartName, "--namespace", namespace, "--output", "json", "--max", "18")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -192,7 +154,7 @@ func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*his
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, elm := range res {
|
for _, elm := range res {
|
||||||
chartRepoName, curVer, err := chartAndVersion(elm.Chart)
|
chartRepoName, curVer, err := utils.ChartAndVersion(elm.Chart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -204,8 +166,13 @@ func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*his
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) ChartRepoVersions(chartName string) (res []repoChartElement, err error) {
|
func (d *DataLayer) ChartRepoVersions(chartName string) (res []*RepoChartElement, err error) {
|
||||||
cmd := []string{"search", "repo", "--regexp", "/" + chartName + "\v", "--versions", "--output", "json"}
|
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...)
|
out, err := d.runCommandHelm(cmd...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -218,6 +185,46 @@ func (d *DataLayer) ChartRepoVersions(chartName string) (res []repoChartElement,
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ChartRepoCharts(repoName string) (res []*RepoChartElement, err error) {
|
||||||
|
cmd := []string{"search", "repo", "--regexp", "\v" + repoName + "/", "--output", "json"}
|
||||||
|
out, err := d.runCommandHelm(cmd...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(out), &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ins, err := d.ListInstalled()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
enrichRepoChartsWithInstalled(res, ins)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func enrichRepoChartsWithInstalled(charts []*RepoChartElement, installed []ReleaseElement) {
|
||||||
|
for _, chart := range charts {
|
||||||
|
for _, rel := range installed {
|
||||||
|
c, _, err := utils.ChartAndVersion(rel.Chart)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to parse chart: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pieces := strings.Split(chart.Name, "/")
|
||||||
|
if pieces[1] == c {
|
||||||
|
chart.InstalledNamespace = rel.Namespace
|
||||||
|
chart.InstalledName = rel.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type SectionFn = func(string, string, int, bool) (string, error) // TODO: rework it into struct-based argument?
|
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 +264,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 +340,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 +357,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 +387,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 +396,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 +411,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 +427,37 @@ 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 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 +474,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)
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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,7 +17,7 @@ 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"`
|
||||||
@@ -27,9 +28,17 @@ type historyElement struct {
|
|||||||
ChartVer string `json:"chart_ver"`
|
ChartVer string `json:"chart_ver"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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"`
|
||||||
}
|
}
|
||||||
15
pkg/dashboard/subproc/scan.go
Normal file
15
pkg/dashboard/subproc/scan.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScanResults struct {
|
||||||
|
PassedCount int
|
||||||
|
FailedCount int
|
||||||
|
OrigReport interface{}
|
||||||
|
Error error
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
115
pkg/dashboard/utils/utils.go
Normal file
115
pkg/dashboard/utils/utils.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"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_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) {
|
||||||
|
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.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: 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
|
||||||
|
}
|
||||||
96
pkg/dashboard/utils/utils_test.go
Normal file
96
pkg/dashboard/utils/utils_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
name: "dashboard"
|
name: "dashboard"
|
||||||
version: "0.1.1"
|
version: "0.2.1"
|
||||||
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"
|
||||||
|
|||||||
BIN
screenshot_scan_manifest.png
Normal file
BIN
screenshot_scan_manifest.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
BIN
screenshot_scan_resource.png
Normal file
BIN
screenshot_scan_resource.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Reference in New Issue
Block a user