Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2221fb22a0 | ||
|
|
9dc3e6a12d | ||
|
|
de0024cd03 | ||
|
|
ed4e970194 | ||
|
|
b0067e31ba | ||
|
|
7e8ba4709e | ||
|
|
be7b2642fc | ||
|
|
896d9e3f72 | ||
|
|
be6666373b | ||
|
|
91df9392c0 | ||
|
|
bd058ee912 | ||
|
|
997f951d0c | ||
|
|
09886ad933 | ||
|
|
0de0b5d0cb | ||
|
|
0141eecef1 | ||
|
|
65ecc20c90 | ||
|
|
f86a4a93a7 | ||
|
|
5cae4b5adf | ||
|
|
d8afa3861d | ||
|
|
3c4d73665e | ||
|
|
8b5f8e1031 | ||
|
|
44461bf5ab | ||
|
|
061bd12f2f | ||
|
|
86c9f89acc | ||
|
|
890994d70d | ||
|
|
35097fed45 | ||
|
|
11912e7b51 | ||
|
|
8e90c9f8d0 | ||
|
|
1e3a706698 | ||
|
|
1b6dc4159a | ||
|
|
69609b1ee2 | ||
|
|
bdd5b9b32e | ||
|
|
870a1196f0 | ||
|
|
c1732c86a5 | ||
|
|
388c330390 | ||
|
|
cb7c29de90 | ||
|
|
fa6a38c50f |
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.
|
||||||
99
README.md
@@ -6,11 +6,23 @@ 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.io) 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.
|
||||||
|
|
||||||
## Installing
|
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
|
||||||
|
|
||||||
To install it, simply run Helm command:
|
To install it, simply run Helm command:
|
||||||
|
|
||||||
@@ -18,24 +30,35 @@ To install it, simply run Helm command:
|
|||||||
helm plugin install https://github.com/komodorio/helm-dashboard.git
|
helm plugin install https://github.com/komodorio/helm-dashboard.git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To update the plugin to the latest version, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
helm plugin update dashboard
|
||||||
|
```
|
||||||
|
|
||||||
To uninstall, run:
|
To uninstall, run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
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.
|
||||||
|
|
||||||
@@ -43,55 +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/x-p3820586794880-3937175868755-4092688791734/archives/C042U85BD45/p1663573506220839) 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
|
|
||||||
- use `--dry-run` instead of `template`
|
|
||||||
- Have cleaner idea on the web API structure
|
|
||||||
- Recognise & show ArgoCD-originating charts/objects, those `helm ls` does not show
|
|
||||||
- Recognise the revisions that are rollbacks by their description and mark in timeline
|
|
||||||
|
|
||||||
#### 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.
|
||||||
@@ -102,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 install 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
@@ -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
|
|
||||||
)
|
)
|
||||||
|
|||||||
6
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,11 +22,11 @@ func main() {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
address, webServerDone := dashboard.StartServer()
|
address, webServerDone := dashboard.StartServer(version)
|
||||||
|
|
||||||
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) *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()
|
||||||
@@ -52,32 +63,45 @@ func NewRouter(abortWeb ControlChan, data *DataLayer) *gin.Engine {
|
|||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureRoutes(abortWeb ControlChan, data *DataLayer, api *gin.Engine) {
|
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{}{}
|
||||||
|
c.Status(http.StatusAccepted)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.GET("/status", func(c *gin.Context) {
|
||||||
|
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("/charts/install", h.InstallPreview)
|
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)
|
||||||
@@ -113,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,16 +1,17 @@
|
|||||||
package dashboard
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"helm.sh/helm/v3/pkg/release"
|
"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) {
|
||||||
@@ -25,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
|
||||||
@@ -39,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
|
||||||
@@ -54,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
|
||||||
@@ -69,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
|
||||||
@@ -84,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
|
||||||
@@ -98,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
|
||||||
@@ -113,34 +129,40 @@ func (h *HelmHandler) RepoUpdate(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) InstallPreview(c *gin.Context) {
|
|
||||||
out, err := chartInstall(c, h.Data, true)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.String(http.StatusOK, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HelmHandler) Install(c *gin.Context) {
|
func (h *HelmHandler) Install(c *gin.Context) {
|
||||||
out, err := chartInstall(c, h.Data, false)
|
qp, err := utils.GetQueryProps(c, false)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
justTemplate := c.Query("flag") != "true"
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
res := release.Release{}
|
if justTemplate {
|
||||||
err = json.Unmarshal([]byte(out), &res)
|
manifests := ""
|
||||||
if err != nil {
|
if isInitial {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
manifests, err = h.Data.RevisionManifests(qp.Namespace, qp.Name, 0, false)
|
||||||
return
|
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.IndentedJSON(http.StatusAccepted, res)
|
c.String(http.StatusAccepted, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -156,21 +178,50 @@ func (h *HelmHandler) GetInfoSection(c *gin.Context) {
|
|||||||
c.String(http.StatusOK, res)
|
c.String(http.StatusOK, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func chartInstall(c *gin.Context, data *DataLayer, justTemplate bool) (string, error) {
|
func (h *HelmHandler) RepoValues(c *gin.Context) {
|
||||||
qp, err := getQueryProps(c, false)
|
out, err := h.Data.ShowValues(c.Query("chart"), c.Query("version"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
c.String(http.StatusOK, out)
|
||||||
out, err := data.ChartUpgrade(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), justTemplate)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -192,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 {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
} else {
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
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,13 +23,13 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := h.Data.GetResource(qp.Namespace, &GenericResource{
|
res, err := h.Data.GetResource(qp.Namespace, &v12.Carp{
|
||||||
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
|
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
|
||||||
ObjectMeta: v1.ObjectMeta{Name: qp.Name},
|
ObjectMeta: v1.ObjectMeta{Name: qp.Name},
|
||||||
})
|
})
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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() (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() (string, ControlChan) {
|
|||||||
address += ":" + os.Getenv("HD_PORT")
|
address += ":" + os.Getenv("HD_PORT")
|
||||||
}
|
}
|
||||||
|
|
||||||
abort := make(ControlChan)
|
abort := make(utils.ControlChan)
|
||||||
api := NewRouter(abort, &data)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 20.5C16.2467 20.5 20.5 16.2467 20.5 11C20.5 5.75329 16.2467 1.5 11 1.5C5.75329 1.5 1.5 5.75329 1.5 11C1.5 16.2467 5.75329 20.5 11 20.5ZM11 22C17.0751 22 22 17.0751 22 11C22 4.92487 17.0751 0 11 0C4.92487 0 0 4.92487 0 11C0 17.0751 4.92487 22 11 22Z" fill="#3B3D45"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4718 16.3004C10.6247 16.8115 8.56283 16.3411 7.11084 14.8891C5.65885 13.4371 5.18842 11.3753 5.69955 9.52809L4.51969 8.34823C3.48499 10.8798 3.99515 13.8947 6.05018 15.9498C8.10522 18.0048 11.1201 18.515 13.6517 17.4803L12.4718 16.3004ZM10.213 5.55627C11.8698 5.31807 13.6144 5.83629 14.889 7.11092C16.1636 8.38555 16.6819 10.1302 16.4437 11.7869L17.6973 13.0406C18.4242 10.6477 17.8416 7.94219 15.9497 6.05026C14.0577 4.15833 11.3523 3.57578 8.95934 4.3026L10.213 5.55627Z" fill="#3B3D45"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 968 B |
@@ -4,6 +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-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 () {
|
||||||
@@ -20,72 +21,201 @@ function checkUpgradeable(name) {
|
|||||||
$.getJSON("/api/helm/repo/search?name=" + name).fail(function (xhr) {
|
$.getJSON("/api/helm/repo/search?name=" + name).fail(function (xhr) {
|
||||||
reportError("Failed to find chart in repo", xhr)
|
reportError("Failed to find chart in repo", xhr)
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
if (!data) {
|
if (!data || !data.length) {
|
||||||
|
$("#btnUpgrade span").text("No upgrades")
|
||||||
|
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-x-octagon")
|
||||||
|
$("#btnUpgrade").prop("disabled", true)
|
||||||
|
$("#btnUpgradeCheck").prop("disabled", true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#upgradeModalLabel select').empty()
|
const verCur = $("#specRev").data("last-chart-ver");
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
$('#upgradeModalLabel select').append("<option value='" + data[i].version + "'>" + data[i].version + "</option>")
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
||||||
|
|
||||||
const verCur = $("#specRev").data("last-chart-ver");
|
|
||||||
const canUpgrade = isNewerVersion(verCur, elm.version);
|
const canUpgrade = isNewerVersion(verCur, elm.version);
|
||||||
$("#btnUpgradeCheck").prop("disabled", false)
|
$("#btnUpgradeCheck").prop("disabled", false)
|
||||||
if (canUpgrade) {
|
if (canUpgrade) {
|
||||||
$("#btnUpgrade span").text("Upgrade to " + elm.version)
|
$("#btnUpgrade span").text("Upgrade to " + elm.version)
|
||||||
|
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-arrow-up")
|
||||||
} else {
|
} else {
|
||||||
$("#btnUpgrade span").text("No upgrades")
|
$("#btnUpgrade span").text("Reconfigure")
|
||||||
|
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-pencil")
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#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;
|
|
||||||
$('#upgradeModalLabel select').data("url", url)
|
|
||||||
|
|
||||||
$("#upgradeModalLabel .name").text(name)
|
$('#upgradeModal').data("chart", elm.name).data("initial", !verCur)
|
||||||
$("#upgradeModalLabel .ver-old").text(verCur)
|
|
||||||
|
|
||||||
$('#upgradeModalLabel select').val(elm.version).trigger("change")
|
$("#upgradeModalLabel .name").text(elm.name)
|
||||||
|
|
||||||
const myModal = new bootstrap.Offcanvas(document.getElementById('upgradeModal'), {});
|
if (verCur) {
|
||||||
myModal.show()
|
$("#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("")
|
||||||
|
}
|
||||||
|
|
||||||
const btnConfirm = $("#upgradeModal .btn-confirm");
|
$.getJSON("/api/helm/repo/search?name=" + elm.name).fail(function (xhr) {
|
||||||
btnConfirm.prop("disabled", true).off('click').click(function () {
|
reportError("Failed to find chart in repo", xhr)
|
||||||
console.log("working")
|
}).done(function (vers) {
|
||||||
btnConfirm.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
// fill versions
|
||||||
$.ajax({
|
$('#upgradeModal select').empty()
|
||||||
url: url + "&version=" + $('#upgradeModalLabel select').val(),
|
for (let i = 0; i < vers.length; i++) {
|
||||||
type: 'POST',
|
const opt = $("<option value='" + vers[i].version + "'></option>");
|
||||||
}).fail(function (xhr) {
|
if (vers[i].version === verCur) {
|
||||||
reportError("Failed to upgrade the chart", xhr)
|
opt.html(vers[i].version + " ·")
|
||||||
}).done(function (data) {
|
} else {
|
||||||
|
opt.html(vers[i].version)
|
||||||
|
}
|
||||||
|
$('#upgradeModal select').append(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#upgradeModal select').val(elm.version).trigger("change")
|
||||||
|
|
||||||
|
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
|
||||||
|
myModal.show()
|
||||||
|
|
||||||
|
if (verCur) {
|
||||||
|
// 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>')
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: "/api/helm/charts/install" + upgradeModalQstr() + "&flag=true",
|
||||||
|
data: $("#upgradeModal textarea").data("dirty") ? $("#upgradeModal form").serialize() : null,
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
reportError("Failed to upgrade the chart", xhr)
|
||||||
|
}).done(function (data) {
|
||||||
|
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 {
|
||||||
|
reportError("Failed to get new revision number")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let reconfigTimeout = null;
|
||||||
|
|
||||||
|
function changeTimer() {
|
||||||
|
const self = $(this);
|
||||||
|
self.data("dirty", true)
|
||||||
|
if (reconfigTimeout) {
|
||||||
|
window.clearTimeout(reconfigTimeout)
|
||||||
|
}
|
||||||
|
reconfigTimeout = window.setTimeout(function () {
|
||||||
|
requestChangeDiff()
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#upgradeModalLabel select').change(function () {
|
$("#upgradeModal textarea").keyup(changeTimer)
|
||||||
|
$("#upgradeModal .rel-name").keyup(changeTimer)
|
||||||
|
$("#upgradeModal .rel-ns").keyup(changeTimer)
|
||||||
|
|
||||||
|
$('#upgradeModal select').change(function () {
|
||||||
const self = $(this)
|
const self = $(this)
|
||||||
|
|
||||||
$("#upgradeModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
requestChangeDiff()
|
||||||
$("#upgradeModal .btn-confirm").prop("disabled", true)
|
|
||||||
$.get(self.data("url") + "&version=" + self.val()).fail(function (xhr) {
|
// fill reference values
|
||||||
|
$("#upgradeModal .ref-vals").html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
$.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) {
|
||||||
$("#upgradeModalBody").empty();
|
data = hljs.highlight(data, {language: 'yaml'}).value
|
||||||
|
$("#upgradeModal .ref-vals").html(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#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() {
|
||||||
|
const self = $('#upgradeModal select');
|
||||||
|
const diffBody = $("#upgradeModalBody");
|
||||||
|
diffBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Calculating diff...')
|
||||||
|
$("#upgradeModal .btn-confirm").prop("disabled", true)
|
||||||
|
|
||||||
|
let values = null;
|
||||||
|
if ($("#upgradeModal textarea").data("dirty")) {
|
||||||
|
$("#upgradeModal .invalid-feedback").hide()
|
||||||
|
values = $("#upgradeModal form").serialize()
|
||||||
|
|
||||||
|
try {
|
||||||
|
jsyaml.load($("#upgradeModal textarea").val())
|
||||||
|
} catch (e) {
|
||||||
|
$("#upgradeModal .invalid-feedback").text("YAML parse error: " + e.message).show()
|
||||||
|
$("#upgradeModalBody").html("Invalid values YAML")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: "/api/helm/charts/install" + upgradeModalQstr(),
|
||||||
|
data: values,
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>")
|
||||||
|
}).done(function (data) {
|
||||||
|
diffBody.empty();
|
||||||
$("#upgradeModal .btn-confirm").prop("disabled", false)
|
$("#upgradeModal .btn-confirm").prop("disabled", false)
|
||||||
|
|
||||||
const targetElement = document.getElementById('upgradeModalBody');
|
const targetElement = document.getElementById('upgradeModalBody');
|
||||||
@@ -95,12 +225,25 @@ $('#upgradeModalLabel select').change(function () {
|
|||||||
};
|
};
|
||||||
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
||||||
diff2htmlUi.draw()
|
diff2htmlUi.draw()
|
||||||
$("#upgradeModalBody").prepend("<p>Following changes will happen to cluster:</p>")
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
$("#upgradeModalBody").html("No changes will happen to cluster")
|
diffBody.html("No changes will happen to the cluster")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
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 () {
|
||||||
@@ -122,7 +265,7 @@ $("#btnUninstall").click(function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const myModal = new bootstrap.Offcanvas(document.getElementById('confirmModal'));
|
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'));
|
||||||
myModal.show()
|
myModal.show()
|
||||||
|
|
||||||
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
|
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
|
||||||
@@ -160,7 +303,7 @@ $("#btnRollback").click(function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const myModal = new bootstrap.Offcanvas(document.getElementById('confirmModal'), {});
|
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'), {});
|
||||||
myModal.show()
|
myModal.show()
|
||||||
|
|
||||||
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revisionNew + "&revisionDiff=" + revisionCur
|
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revisionNew + "&revisionDiff=" + revisionCur
|
||||||
|
|||||||
26
pkg/dashboard/static/datadog.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
(function(h,o,u,n,d) {
|
||||||
|
h=h[d]=h[d]||{q:[],onReady:function(c){h.q.push(c)}}
|
||||||
|
d=o.createElement(u);d.async=1;d.src=n
|
||||||
|
n=o.getElementsByTagName(u)[0];n.parentNode.insertBefore(d,n)
|
||||||
|
})(window,document,'script','https://www.datadoghq-browser-agent.com/datadog-rum-v4.js','DD_RUM')
|
||||||
|
DD_RUM.onReady(function() {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.onload = function() {
|
||||||
|
const version = JSON.parse(xhr.responseText).VerCur;
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE && version!=="dev") {
|
||||||
|
DD_RUM.init({
|
||||||
|
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
|
||||||
|
applicationId: 'e75439e5-e1b3-46ba-a9e9-a2e58579a2e2',
|
||||||
|
site: 'datadoghq.com',
|
||||||
|
service: 'helm-dashboard',
|
||||||
|
version: version,
|
||||||
|
trackInteractions: true,
|
||||||
|
trackResources: true,
|
||||||
|
trackLongTasks: true,
|
||||||
|
defaultPrivacyLevel: 'mask'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.open('GET', '/status', true);
|
||||||
|
xhr.send(null);
|
||||||
|
})
|
||||||
@@ -4,15 +4,16 @@ function revisionClicked(namespace, name, self) {
|
|||||||
revRow.find(".active").removeClass(active).addClass(inactive)
|
revRow.find(".active").removeClass(active).addClass(inactive)
|
||||||
self.removeClass(inactive).addClass(active)
|
self.removeClass(inactive).addClass(active)
|
||||||
const elm = self.data("elm")
|
const elm = self.data("elm")
|
||||||
console.log(elm)
|
|
||||||
setHashParam("revision", elm.revision)
|
setHashParam("revision", elm.revision)
|
||||||
$("#sectionDetails span.rev").text("#"+elm.revision)
|
$("#sectionDetails span.rev").text("#" + elm.revision)
|
||||||
statusStyle(elm.status, $("#none"), $("#sectionDetails .rev-details .rev-status"))
|
statusStyle(elm.status, $("#none"), $("#sectionDetails .rev-details .rev-status"))
|
||||||
|
|
||||||
$("#sectionDetails .rev-date").text(elm.updated.replace("T", " ").replace("+", " +"))
|
const rdate = luxon.DateTime.fromISO(elm.updated);
|
||||||
|
$("#sectionDetails .rev-date").text(rdate.toJSDate().toLocaleString())
|
||||||
$("#sectionDetails .rev-tags .rev-chart").text(elm.chart)
|
$("#sectionDetails .rev-tags .rev-chart").text(elm.chart)
|
||||||
$("#sectionDetails .rev-tags .rev-app").text(elm.app_version)
|
$("#sectionDetails .rev-tags .rev-app").text(elm.app_version)
|
||||||
$("#sectionDetails .rev-tags .rev-ns").text(getHashParam("namespace"))
|
$("#sectionDetails .rev-tags .rev-ns").text(getHashParam("namespace"))
|
||||||
|
$("#sectionDetails .rev-tags .rev-cluster").text(getHashParam("context"))
|
||||||
|
|
||||||
$("#revDescr").text(elm.description).removeClass("text-danger")
|
$("#revDescr").text(elm.description).removeClass("text-danger")
|
||||||
if (elm.status === "failed") {
|
if (elm.status === "failed") {
|
||||||
@@ -94,9 +95,11 @@ $('#specRev').keyup(function (event) {
|
|||||||
if (keycode == '13') {
|
if (keycode == '13') {
|
||||||
$("#diffModeRev").click()
|
$("#diffModeRev").click()
|
||||||
}
|
}
|
||||||
event.preventDefault()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("form").submit(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
$("#userDefinedVals").change(function () {
|
$("#userDefinedVals").change(function () {
|
||||||
const self = $(this)
|
const self = $(this)
|
||||||
@@ -137,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>');
|
||||||
@@ -150,11 +154,12 @@ function showResources(namespace, chart, revision) {
|
|||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
const res = data[i]
|
const res = data[i]
|
||||||
const resBlock = $(`
|
const resBlock = $(`
|
||||||
<div class="row px-3 py-2 mb-2">
|
<div class="row px-3 py-2 mb-3 bg-white rounded">
|
||||||
<div class="col-2 res-kind"></div>
|
<div class="col-2 res-kind text-break"></div>
|
||||||
<div class="col-4 res-name"></div>
|
<div class="col-3 res-name text-break fw-bold"></div>
|
||||||
<div class="col-5 res-status"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> <span class="text-muted small">Getting status...</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-1 res-actions"></div>
|
<div class="col-4 res-statusmsg"><span class="text-muted small">Getting status...</span></div>
|
||||||
|
<div class="col-2 res-actions"></div>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
@@ -166,11 +171,11 @@ function showResources(namespace, chart, revision) {
|
|||||||
$.getJSON("/api/kube/resources/" + res.kind.toLowerCase() + "?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
|
$.getJSON("/api/kube/resources/" + res.kind.toLowerCase() + "?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
|
||||||
//reportError("Failed to get list of resources")
|
//reportError("Failed to get list of resources")
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
const badge = $("<span class='badge me-2'></span>").text(data.status.phase);
|
const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase);
|
||||||
if (["Available", "Active", "Established"].includes(data.status.phase)) {
|
if (["Available", "Active", "Established"].includes(data.status.phase)) {
|
||||||
badge.addClass("bg-success")
|
badge.addClass("bg-success text-dark")
|
||||||
} else if (["Exists"].includes(data.status.phase)) {
|
} else if (["Exists"].includes(data.status.phase)) {
|
||||||
badge.addClass("bg-success bg-opacity-50")
|
badge.addClass("bg-success text-dark bg-opacity-50")
|
||||||
} else if (["Progressing"].includes(data.status.phase)) {
|
} else if (["Progressing"].includes(data.status.phase)) {
|
||||||
badge.addClass("bg-warning")
|
badge.addClass("bg-warning")
|
||||||
} else {
|
} else {
|
||||||
@@ -178,13 +183,22 @@ function showResources(namespace, chart, revision) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statusBlock = resBlock.find(".res-status");
|
const statusBlock = resBlock.find(".res-status");
|
||||||
statusBlock.empty().append(badge).append("<span class='text-muted small'>" + (data.status.message ? data.status.message : '') + "</span>")
|
statusBlock.empty().append(badge).attr("title", data.status.phase)
|
||||||
|
resBlock.find(".res-statusmsg").html("<span class='text-muted small'>" + (data.status.message ? data.status.message : '') + "</span>")
|
||||||
|
|
||||||
if (badge.text() !== "NotFound") {
|
if (badge.text() !== "NotFound") {
|
||||||
resBlock.find(".res-actions")
|
resBlock.find(".res-actions")
|
||||||
resBlock.find(".res-actions").append("<i class=\"btn bi-zoom-in float-end text-muted\"></i>")
|
|
||||||
statusBlock.find(".bi-zoom-in").click(function () {
|
const btn = $("<button class=\"btn btn-sm btn-white border-secondary\">Describe</button>");
|
||||||
showDescribe(ns, res.kind, res.metadata.name)
|
resBlock.find(".res-actions").append(btn)
|
||||||
|
btn.click(function () {
|
||||||
|
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())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -192,8 +206,9 @@ function showResources(namespace, chart, revision) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDescribe(ns, kind, name) {
|
function showDescribe(ns, kind, name, badge) {
|
||||||
$("#describeModalLabel").text("Describe " + kind + ": " + ns + " / " + name)
|
$("#describeModal .offcanvas-header p").text(kind)
|
||||||
|
$("#describeModalLabel").text(name).append(badge.addClass("ms-3 small fw-normal"))
|
||||||
$("#describeModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
$("#describeModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
|
|
||||||
const myModal = new bootstrap.Offcanvas(document.getElementById('describeModal'));
|
const myModal = new bootstrap.Offcanvas(document.getElementById('describeModal'));
|
||||||
@@ -205,3 +220,42 @@ function showDescribe(ns, kind, name) {
|
|||||||
$("#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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Helm Dashboard</title>
|
<title>Helm Dashboard</title>
|
||||||
|
<script src="static/datadog.js"></script>
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="https://fonts.googleapis.com/css2?family=Roboto&family=Inter&family=Poppins:wght@600&family=Inter:wght@500&family=Roboto+Slab:wght@400&family=Roboto+Slab:wght@700&family=Roboto:wght@700"/>
|
href="https://fonts.googleapis.com/css2?family=Roboto&family=Inter&family=Poppins:wght@600&family=Poppins:wght@500&family=Inter:wght@500&family=Roboto+Slab:wght@400&family=Roboto+Slab:wght@700&family=Roboto:wght@700&family=Roboto:wght@500"/>
|
||||||
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
|
||||||
@@ -14,19 +15,32 @@
|
|||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/lightfair.min.css"/>
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/lightfair.min.css"/>
|
||||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"/>
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"/>
|
||||||
<link href="static/styles.css" rel="stylesheet">
|
<link href="static/styles.css" rel="stylesheet">
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.heap = window.heap || [], heap.load = function (e, t) {
|
||||||
|
window.heap.appid = e, window.heap.config = t = t || {};
|
||||||
|
var r = document.createElement("script");
|
||||||
|
r.type = "text/javascript", r.async = !0, r.src = "https://cdn.heapanalytics.com/js/heap-" + e + ".js";
|
||||||
|
var a = document.getElementsByTagName("script")[0];
|
||||||
|
a.parentNode.insertBefore(r, a);
|
||||||
|
for (var n = function (e) {
|
||||||
|
return function () {
|
||||||
|
heap.push([e].concat(Array.prototype.slice.call(arguments, 0)))
|
||||||
|
}
|
||||||
|
}, p = ["addEventProperties", "addUserProperties", "clearEventProperties", "identify", "resetIdentity", "removeEventProperty", "setEventProperties", "track", "unsetEventProperty"], o = 0; o < p.length; o++) heap[p[o]] = n(p[o])
|
||||||
|
};
|
||||||
|
heap.load("3615793373");
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="container-fluid gx-0">
|
<div class="container-fluid px-0">
|
||||||
<!-- TOP BAR -->
|
<!-- TOP BAR -->
|
||||||
<nav class="navbar navbar-expand bg-white mb-0 p-0" id="topNav">
|
<nav class="navbar navbar-expand bg-white mb-0 p-0 b-shadow" id="topNav">
|
||||||
<div class="container-fluid m-0 p-0">
|
<div class="container-fluid m-0 p-0">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
<a href="/"><img src="static/logo.png" alt="Logo"></a>
|
<a href="/"><img src="static/logo.png" alt="Logo"></a>
|
||||||
<div>
|
<div>
|
||||||
<h1><a href="/">Helm Dashboard</a></h1>
|
<h1><a href="/">Helm Dashboard</a></h1>
|
||||||
<p><span class="mr-1">by</span><a href="https://komodor.io"><img src="static/komodor-logo.svg"
|
|
||||||
alt="komodor.io"></a></p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,26 +48,92 @@
|
|||||||
|
|
||||||
<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><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>
|
</li>
|
||||||
-->
|
|
||||||
</ul>
|
</ul>
|
||||||
|
<div>
|
||||||
|
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img
|
||||||
|
src="static/komodor-logo.svg" alt="komodor.io"
|
||||||
|
style="height: 1.2rem; vertical-align: text-bottom; filter: grayscale(00%);"></a>
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<!-- /TOP BAR -->
|
<!-- /TOP BAR -->
|
||||||
|
|
||||||
<div class="row mt-3 pt-3 mx-0 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">
|
||||||
@@ -91,54 +171,59 @@
|
|||||||
</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 section" id="sectionDetails" style="display: none">
|
||||||
<div class="row pt-0 mx-0 me-0" id="sectionDetails" style="display: none">
|
<div class="col-2 px-4 py-4 pe-3 rev-list">
|
||||||
<div class="col-2 ms-3 ps-1 pt-4 rev-list">
|
|
||||||
<h3 class="fw-bold small">Revisions</h3>
|
<h3 class="fw-bold small">Revisions</h3>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col ms-2 rev-details bg-white b-shadow pt-4 px-5">
|
|
||||||
|
<div class="col-10 rev-details bg-white b-shadow pt-4 px-5 overflow-auto">
|
||||||
<div><span class="rev-status fw-bold me-3"></span></div>
|
<div><span class="rev-status fw-bold me-3"></span></div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="name float-start">Name</h1>
|
<h1 class="name float-start">Name</h1>
|
||||||
<div id="actionButtons" class="float-end">
|
<div id="actionButtons" class="float-end">
|
||||||
<span><button id="btnUpgrade"
|
<span><button id="btnUpgrade"
|
||||||
class="opacity-10 btn btn-sm btn-light bg-white rounded-0 me-0 rounded-start border-secondary">
|
class="opacity-10 btn btn-sm btn-light bg-white me-2 border-secondary">
|
||||||
<img src="static/action.svg"> <span>Checking...</span>
|
<i class="icon bi-hourglass-split"></i> <span></span>
|
||||||
</button></span><span><button
|
</button></span>
|
||||||
id="btnUpgradeCheck"
|
|
||||||
class="btn btn-sm text-muted btn-light bg-white border-secondary rounded-0 rounded-end ms-0 me-2"
|
|
||||||
title="Check for newer chart version from repo"><i class="bi-repeat"></i><span
|
|
||||||
class="spinner-border spinner-border-sm" style="display: none" role="status"
|
|
||||||
aria-hidden="true"></span></button></span>
|
|
||||||
|
|
||||||
<button id="btnRollback" class="btn btn-sm btn-light bg-white border border-secondary me-2"
|
<button id="btnRollback" class="btn btn-sm btn-light bg-white border border-secondary me-2"
|
||||||
title="Rollback to this revision"><img src="static/action.svg"> <span>Rollback</span>
|
title="Rollback to this revision"><i class="bi-arrow-repeat"></i> <span>Rollback</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="btnUninstall" class="btn btn-sm btn-light bg-white border border-secondary"
|
<button id="btnUninstall" class="btn btn-sm btn-light bg-white border border-secondary"
|
||||||
title="Uninstall the chart"><img src="static/action.svg"> Uninstall
|
title="Uninstall the chart"><i class="bi-trash3"></i> Uninstall
|
||||||
</button>
|
</button>
|
||||||
|
<br/>
|
||||||
|
<a class="link small" id="btnUpgradeCheck">Check for new version
|
||||||
|
<span class="spinner-border spinner-border-sm" style="display: none" role="status"
|
||||||
|
aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="fs-2"> </div>
|
<div class="fs-2"> </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Revision <span class="rev fw-bold me-4"></span>
|
Revision <span class="rev fw-bold me-4"></span>
|
||||||
Upgraded on <span class="rev-date"></span>
|
<span class="rev-date"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rev-tags mt-3">
|
<div class="rev-tags mt-3">
|
||||||
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">namespace: <span
|
|
||||||
class="rev-ns fw-bold"></span></span>
|
|
||||||
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">chart version: <span
|
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">chart version: <span
|
||||||
class="rev-chart fw-bold"></span></span>
|
class="rev-chart fw-bold"></span></span>
|
||||||
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">app version: <span
|
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">app version: <span
|
||||||
class="rev-app fw-bold"></span></span>
|
class="rev-app fw-bold"></span></span>
|
||||||
|
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">namespace: <span
|
||||||
|
class="rev-ns fw-bold"></span></span>
|
||||||
|
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">cluster: <span
|
||||||
|
class="rev-cluster fw-bold"></span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="revDescr" class="mt-3 mb-4"></div>
|
<div id="revDescr" class="mt-3 mb-4"></div>
|
||||||
@@ -150,37 +235,38 @@
|
|||||||
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">
|
||||||
Parameterized 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>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="tab-pane p-3 container-fluid" id="nav-resources" role="tabpanel">
|
<div class="tab-pane p-3 container-fluid" id="nav-resources" role="tabpanel">
|
||||||
<div class="row bg-secondary rounded px-3 py-2 mb-2 fw-bold small"
|
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
|
||||||
style="text-transform: uppercase">
|
style="text-transform: uppercase">
|
||||||
<div class="col-2">Kind</div>
|
<div class="col-2">Resource Type</div>
|
||||||
<div class="col-4">Name</div>
|
<div class="col-3">Name</div>
|
||||||
<div class="col-5">Status</div>
|
<div class="col-1">Status</div>
|
||||||
<div class="col-1">Describe</div>
|
<div class="col-5">Status Message</div>
|
||||||
|
<div class="col-1"></div>
|
||||||
</div>
|
</div>
|
||||||
<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"
|
||||||
data-mode="view">
|
data-mode="view">
|
||||||
View Current
|
View
|
||||||
</label>
|
</label>
|
||||||
<label class="form-check-label" for="diffModePrev">
|
<label class="form-check-label" for="diffModePrev">
|
||||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModePrev"
|
<input class="form-check-input" type="radio" name="diffMode" id="diffModePrev"
|
||||||
@@ -200,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>
|
||||||
@@ -210,7 +293,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Modals -->
|
<!-- Modals -->
|
||||||
<div id="errorAlert"
|
<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>
|
||||||
@@ -220,60 +303,113 @@
|
|||||||
</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">
|
<div class="offcanvas-header border-bottom p-4">
|
||||||
<h5 id="describeModalLabel"></h5>
|
<div>
|
||||||
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
<h5 id="describeModalLabel"></h5>
|
||||||
</div>
|
<p class="m-0 mt-4">ResourceType</p>
|
||||||
<div class="offcanvas-body" id="describeModalBody">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="offcanvas offcanvas-end rounded-start" tabindex="-1" id="confirmModal" aria-labelledby="confirmModalLabel">
|
|
||||||
<div class="offcanvas-header">
|
|
||||||
<h5 id="confirmModalLabel"></h5>
|
|
||||||
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="offcanvas-body" id="confirmModalBody">
|
|
||||||
</div>
|
|
||||||
<div class="offcanvas-footer p-3">
|
|
||||||
<button type="button" class="btn btn-primary float-end btn-confirm">Confirm</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="offcanvas offcanvas-end rounded-start" tabindex="-1" id="upgradeModal" aria-labelledby="upgradeModalLabel">
|
|
||||||
<div class="offcanvas-header">
|
|
||||||
<h5 id="upgradeModalLabel">
|
|
||||||
Upgrade <b class='text-success name'></b> from version <b class='text-success ver-old'></b> to
|
|
||||||
<select class='fw-bold text-success ver-new'></select>
|
|
||||||
</h5>
|
|
||||||
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="offcanvas-body" id="upgradeModalBody">
|
|
||||||
</div>
|
|
||||||
<div class="offcanvas-footer p-3">
|
|
||||||
<button type="button" class="btn btn-primary float-end btn-confirm">Confirm Upgrade</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="container-fluid small mt-3" style="z-index: -50">
|
|
||||||
<div class="row align-items-end justify-content-end">
|
|
||||||
<div class="col-3"></div>
|
|
||||||
<div class="col-4 text-center bg-white bg-opacity-50 p-2 px-3 rounded b-shadow">
|
|
||||||
Brought to you by <img src="https://komodor.com/wp-content/uploads/2021/05/favicon.png"
|
|
||||||
style="height: 1rem"> <a class="me-4"
|
|
||||||
href="https://komodor.io">Komodor.io</a>
|
|
||||||
<i class="bi-github"></i>
|
|
||||||
<a href="https://github.com/komodorio/helm-dashboard">Project page on GitHub</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3"></div>
|
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
<div class="offcanvas-body p-2 ps-4" id="describeModalBody">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<img src="static/topographic.svg" class="position-absolute bottom-0 left-0" style="z-index: -100; height: 100%;"/>
|
|
||||||
|
|
||||||
|
<div class="modal" id="confirmModal" tabindex="-1" aria-labelledby="describeModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="confirmModalLabel"></h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="confirmModalBody">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary btn-confirm">Confirm</button>
|
||||||
|
</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">
|
||||||
|
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" id="upgradeModalLabel">
|
||||||
|
Install <b class='text-success name'></b>
|
||||||
|
</h4>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<form class="modal-body border-bottom fs-5" enctype="multipart/form-data">
|
||||||
|
<div class="input-group mb-3 text-muted">
|
||||||
|
<label class="form-label me-4 text-dark">Version to install: <select
|
||||||
|
class='fw-bold text-success ver-new'></select></label> <span class="ver-old">(current version is <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 class="row">
|
||||||
|
<div class="col-6 pe-3">
|
||||||
|
<label class="form-label">User-Defined Values:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 ps-3">
|
||||||
|
<label class="form-label">Chart Values Reference:</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 pe-3">
|
||||||
|
<textarea name="values" class="form-control w-100 h-100" rows="5"
|
||||||
|
style="font-family: monospace"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 ps-3">
|
||||||
|
<pre class="ref-vals fs-6 w-100 bg-secondary p-2 rounded" style="max-height: 20rem"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 pe-3">
|
||||||
|
<span class="invalid-feedback small mb-3"> (wrong YAML)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="form-label mt-4">Manifest changes:</label>
|
||||||
|
<div id="upgradeModalBody" class="small"></div>
|
||||||
|
</form>
|
||||||
|
<div class="modal-footer d-flex">
|
||||||
|
<button type="button" class="btn btn-scan bg-white border-secondary">Scan for Problems</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-confirm">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
|
||||||
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
|
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
@@ -282,10 +418,13 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"
|
||||||
integrity="sha512-gU7kztaQEl7SHJyraPfZLQCNnrKdaQi5ndOyt4L4UPL/FHDd/uB9Je6KDARIqwnNNE27hnqoWLBq+Kpe4iHfeQ=="
|
integrity="sha512-gU7kztaQEl7SHJyraPfZLQCNnrKdaQi5ndOyt4L4UPL/FHDd/uB9Je6KDARIqwnNNE27hnqoWLBq+Kpe4iHfeQ=="
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.1/dist/js.cookie.min.js"
|
|
||||||
integrity="sha256-0H3Nuz3aug3afVbUlsu12Puxva3CP4EhJtPExqs54Vg=" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/luxon@3.0.3/build/global/luxon.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/luxon@3.0.3/build/global/luxon.min.js"
|
||||||
integrity="sha256-RH4TKnKcKyde0s2jc5BW3pXZl/5annY3fcZI9VrV5WQ=" crossorigin="anonymous"></script>
|
integrity="sha256-RH4TKnKcKyde0s2jc5BW3pXZl/5annY3fcZI9VrV5WQ=" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"
|
||||||
|
integrity="sha512-CSBhVREyzHAjAFfBlIBakjoRUKp5h7VSweP0InR/pAJyptH7peuhCsqAI/snV+TwZmXZqoUklpXp6R6wMnYf5Q=="
|
||||||
|
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>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<svg width="53" height="13" viewBox="0 0 53 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="77" height="19" viewBox="0 0 77 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_36_1097)">
|
<g clip-path="url(#clip0_231_1524)">
|
||||||
<path d="M5.86295 4.90639H4.0672L1.355 7.52039V2.1665H0V11.0071H1.355V8.93513L2.42853 7.98768L4.57123 11.0071H6.23823L3.39732 7.11706L5.86295 4.90639Z" fill="#1347FF"/>
|
<path d="M8.51786 7.17108H5.90895L1.96859 10.9915V3.16663H0V16.0875H1.96859V13.0592L3.52824 11.6745L6.64123 16.0875H9.06309L4.93573 10.4021L8.51786 7.17108Z" fill="#1347FF"/>
|
||||||
<path d="M19.4109 4.90652L18.7279 5.58935L18.0559 4.90652H15.6142L14.8004 5.74086V4.90652H13.5217V11.0073H14.8789V6.73737L15.6011 6.01827H17.1634L17.5387 6.38531V11.0073H18.8959V6.49839L19.3847 6.01827H21.1804L21.5557 6.38531V11.0073H22.9108V5.99267L21.8263 4.90652H19.4109Z" fill="#1347FF"/>
|
<path d="M28.2007 7.17127L27.2085 8.16924L26.2321 7.17127H22.6848L21.5024 8.39069V7.17127H19.6448V16.0877H21.6165V9.84712L22.6658 8.79613H24.9356L25.4808 9.33257V16.0877H27.4526V9.49785L28.1627 8.79613H30.7716L31.3168 9.33257V16.0877H33.2854V8.75871L31.7099 7.17127H28.2007Z" fill="#1347FF"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.46283 5.99267L7.54726 4.90652H11.2544L12.3389 5.99267V9.92112L11.2544 11.0073H7.54726L6.46283 9.92112V5.99267ZM10.6086 9.89549L10.9708 9.52848H10.973V6.38531L10.6107 6.01827H8.19531L7.8331 6.38531V9.52848L8.19315 9.89549H10.6086Z" fill="#1347FF"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.38939 8.75871L10.9649 7.17127H16.3508L17.9263 8.75871V14.5003L16.3508 16.0877H10.9649L9.38939 14.5003V8.75871ZM15.4124 14.4628L15.9387 13.9264H15.9418V9.33257L15.4156 8.79613H11.9064L11.3802 9.33257V13.9264L11.9033 14.4628H15.4124Z" fill="#1347FF"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.2791 4.90652L42.1947 5.99267V9.92112L43.2791 11.0073H46.9863L48.0707 9.92112V5.99267L46.9863 4.90652H43.2791ZM46.7028 9.52848L46.3405 9.89549H43.9252L43.5629 9.52848V6.38531L43.9252 6.01827H46.3405L46.7028 6.38531V9.52848Z" fill="#1347FF"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.8772 7.17127L61.3017 8.75871V14.5003L62.8772 16.0877H68.2631L69.8385 14.5003V8.75871L68.2631 7.17127H62.8772ZM67.8513 13.9264L67.3249 14.4628H63.8158L63.2895 13.9264V9.33257L63.8158 8.79613H67.3249L67.8513 9.33257V13.9264Z" fill="#1347FF"/>
|
||||||
<path d="M50.5323 5.81555L51.4355 4.90652H53.0002V6.16979H51.3332L50.6108 6.8782V11.0073H49.2536V4.90652H50.5323V5.81555Z" fill="#1347FF"/>
|
<path d="M73.4148 8.49984L74.7271 7.17127H77.0003V9.01758H74.5785L73.5289 10.053V16.0877H71.5571V7.17127H73.4148V8.49984Z" fill="#1347FF"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.0906 4.90652L24.0061 5.99267V9.92112L25.0906 11.0073H33.1027L34.1871 9.92112V5.99267L33.1027 4.90652H25.0906ZM32.817 9.52848L32.4547 9.89549H25.7343L25.3721 9.52848V6.38531L25.7343 6.01827H32.4547L32.817 6.38531V9.52848Z" fill="#1347FF"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.4523 7.17127L34.8768 8.75871V14.5003L36.4523 16.0877H48.0926L49.668 14.5003V8.75871L48.0926 7.17127H36.4523ZM47.6776 13.9264L47.1512 14.4628H37.3875L36.8613 13.9264V9.33257L37.3875 8.79613H47.1512L47.6776 9.33257V13.9264Z" fill="#1347FF"/>
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.895 4.90639L39.6437 5.66392V2.1665H40.9984V11.0071H39.6437V10.2624L38.895 11.0071H36.3116L35.2272 9.92098V5.99252L36.3116 4.90639H38.895ZM38.9213 9.89538L39.6437 9.18905V6.73725L38.9213 6.01813H36.9577L36.5823 6.38516V9.52834L36.9577 9.89538H38.9213Z" fill="#1347FF"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.5078 7.17108L57.5955 8.27823V3.16663H59.5637V16.0875H57.5955V14.9991L56.5078 16.0875H52.7546L51.1792 14.5001V8.7585L52.7546 7.17108H56.5078ZM56.546 14.4627L57.5955 13.4303V9.84695L56.546 8.79592H53.6933L53.1478 9.33236V13.9262L53.6933 14.4627H56.546Z" fill="#1347FF"/>
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0_36_1097">
|
<clipPath id="clip0_231_1524">
|
||||||
<rect width="53" height="13" fill="white"/>
|
<rect width="77" height="19" fill="white"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,4 +1,5 @@
|
|||||||
function loadChartsList() {
|
function loadChartsList() {
|
||||||
|
$("body").removeClass("bg-variant1 bg-variant2").addClass("bg-variant1")
|
||||||
$("#sectionList").show()
|
$("#sectionList").show()
|
||||||
const chartsCards = $("#installedList .body")
|
const chartsCards = $("#installedList .body")
|
||||||
chartsCards.empty().append("<div><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span> Loading...</div>")
|
chartsCards.empty().append("<div><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span> Loading...</div>")
|
||||||
@@ -11,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>
|
||||||
@@ -33,7 +37,7 @@ function buildChartCard(elm) {
|
|||||||
|
|
||||||
statusStyle(elm.status, card, card.find(".rel-status span"))
|
statusStyle(elm.status, card, card.find(".rel-status span"))
|
||||||
|
|
||||||
card.find("a").attr("href", '#namespace=' + elm.namespace + '&name=' + elm.name)
|
card.find("a").attr("href", '#context=' + getHashParam('context') + '&namespace=' + elm.namespace + '&name=' + elm.name)
|
||||||
|
|
||||||
card.find(".rel-name span").data("chart", elm).click(function () {
|
card.find(".rel-name span").data("chart", elm).click(function () {
|
||||||
const self = $(this)
|
const self = $(this)
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const revRow = $("#sectionDetails .rev-list ul");
|
const revRow = $("#sectionDetails .rev-list ul");
|
||||||
|
|
||||||
function loadChartHistory(namespace, name) {
|
function loadChartHistory(namespace, name) {
|
||||||
|
$("body").removeClass("bg-variant1 bg-variant2").addClass("bg-variant2")
|
||||||
$("#sectionDetails").show()
|
$("#sectionDetails").show()
|
||||||
$("#sectionDetails .name").text(name)
|
$("#sectionDetails .name").text(name)
|
||||||
revRow.empty().append("<li><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span></li>")
|
revRow.empty().append("<li><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span></li>")
|
||||||
@@ -25,10 +26,10 @@ function fillChartHistory(data, namespace, name) {
|
|||||||
data.reverse()
|
data.reverse()
|
||||||
for (let x = 0; x < data.length; x++) {
|
for (let x = 0; x < data.length; x++) {
|
||||||
const elm = data[x]
|
const elm = data[x]
|
||||||
$("#specRev").val(elm.revision).data("first-rev", elm.revision)
|
$("#specRev").data("first-rev", elm.revision)
|
||||||
|
|
||||||
if (!x) {
|
if (!x) {
|
||||||
$("#specRev").data("last-rev", elm.revision).data("last-chart-ver", elm.chart_ver)
|
$("#specRev").val(elm.revision).data("last-rev", elm.revision).data("last-chart-ver", elm.chart_ver)
|
||||||
}
|
}
|
||||||
|
|
||||||
const rev = $(`<li class="px-2 pt-5 pb-4 mb-2 rounded border border-secondary bg-secondary position-relative">
|
const rev = $(`<li class="px-2 pt-5 pb-4 mb-2 rounded border border-secondary bg-secondary position-relative">
|
||||||
@@ -45,6 +46,11 @@ function fillChartHistory(data, namespace, name) {
|
|||||||
rev.find(".rev-age").text(getAge(elm, data[x - 1])).parent().attr("title", elm.updated)
|
rev.find(".rev-age").text(getAge(elm, data[x - 1])).parent().attr("title", elm.updated)
|
||||||
statusStyle(elm.status, rev.find(".rev-status"), rev.find(".rev-status"))
|
statusStyle(elm.status, rev.find(".rev-status"), rev.find(".rev-status"))
|
||||||
|
|
||||||
|
if (elm.description.startsWith("Rollback to ")) {
|
||||||
|
//rev.find(".rev-status").append(" <span class='small fw-normal text-lowercase'>(rollback)</span>")
|
||||||
|
rev.find(".rev-status").append(" <i class='bi-arrow-counterclockwise text-muted' title='"+elm.description+"'></i>")
|
||||||
|
}
|
||||||
|
|
||||||
const nxt = data[x + 1];
|
const nxt = data[x + 1];
|
||||||
if (nxt && isNewerVersion(elm.chart_ver, nxt.chart_ver)) {
|
if (nxt && isNewerVersion(elm.chart_ver, nxt.chart_ver)) {
|
||||||
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='text-danger bi-arrow-down-right'></i> " + elm.chart_ver)
|
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='text-danger bi-arrow-down-right'></i> " + elm.chart_ver)
|
||||||
|
|||||||
@@ -1,16 +1,43 @@
|
|||||||
$(function () {
|
$(function () {
|
||||||
const clusterSelect = $("#cluster");
|
const clusterSelect = $("#cluster");
|
||||||
clusterSelect.change(function () {
|
clusterSelect.change(function () {
|
||||||
Cookies.set("context", clusterSelect.find("input:radio:checked").val())
|
window.location.href = "/#context=" + clusterSelect.find("input:radio:checked").val()
|
||||||
window.location.href = "/"
|
window.location.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
$.getJSON("/api/kube/contexts").fail(function (xhr) {
|
$.getJSON("/api/kube/contexts").fail(function (xhr) {
|
||||||
reportError("Failed to get list of clusters", xhr)
|
reportError("Failed to get list of clusters", xhr)
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
const context = Cookies.get("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,34 @@ $(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')
|
||||||
|
myAlert.addEventListener('close.bs.alert', event => {
|
||||||
|
event.preventDefault()
|
||||||
|
$("#errorAlert").hide()
|
||||||
|
})
|
||||||
|
|
||||||
function reportError(err, xhr) {
|
function reportError(err, xhr) {
|
||||||
$("#errorAlert h4 span").text(err)
|
$("#errorAlert h4 span").text(err)
|
||||||
@@ -31,15 +83,20 @@ function reportError(err, xhr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function getHashParam(name) {
|
function getHashParam(name) {
|
||||||
const params = new URLSearchParams(window.location.hash.substring(1))
|
const params = new URLSearchParams(window.location.hash.substring(1))
|
||||||
return params.get(name)
|
return params.get(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))
|
||||||
params.set(name, val)
|
if (!name) {
|
||||||
|
params = new URLSearchParams()
|
||||||
|
} else if (!val) {
|
||||||
|
params.delete(name)
|
||||||
|
} else {
|
||||||
|
params.set(name, val)
|
||||||
|
}
|
||||||
window.location.hash = new URLSearchParams(params).toString()
|
window.location.hash = new URLSearchParams(params).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,30 +120,48 @@ function statusStyle(status, card, txt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCleanClusterName(rawClusterName) {
|
||||||
|
if (rawClusterName.indexOf('arn') === 0) {
|
||||||
|
// AWS cluster
|
||||||
|
const clusterSplit = rawClusterName.split(':')
|
||||||
|
const clusterName = clusterSplit.at(-1).split("/").at(-1)
|
||||||
|
const region = clusterSplit.at(-3)
|
||||||
|
return region + "/" + clusterName + ' [AWS]'
|
||||||
|
}
|
||||||
|
if (rawClusterName.indexOf('gke') === 0) {
|
||||||
|
// GKE cluster
|
||||||
|
return rawClusterName.split('_').at(-2) + '/' + rawClusterName.split('_').at(-1) + ' [GKE]'
|
||||||
|
}
|
||||||
|
return rawClusterName
|
||||||
|
}
|
||||||
|
|
||||||
function fillClusterList(data, context) {
|
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(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)
|
||||||
} else if (context && elm.Name === context) {
|
} else if (context && elm.Name === context) {
|
||||||
opt.find("input").prop("checked", true)
|
opt.find("input").prop("checked", true)
|
||||||
$.ajaxSetup({
|
setCurrentContext(elm.Name)
|
||||||
headers: {
|
|
||||||
'x-kubecontext': context
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
$("#cluster").append(opt)
|
$("#cluster").append(opt)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCurrentContext(ctx) {
|
||||||
|
setHashParam("context", ctx)
|
||||||
|
$.ajaxSetup({
|
||||||
|
headers: {
|
||||||
|
'x-kubecontext': ctx
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getAge(obj1, obj2) {
|
function getAge(obj1, obj2) {
|
||||||
const date = luxon.DateTime.fromISO(obj1.updated);
|
const date = luxon.DateTime.fromISO(obj1.updated);
|
||||||
let dateNext = luxon.DateTime.now()
|
let dateNext = luxon.DateTime.now()
|
||||||
@@ -119,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++) {
|
||||||
@@ -128,4 +211,12 @@ function isNewerVersion(oldVer, newVer) {
|
|||||||
if (a < b) return false
|
if (a < b) return false
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -46,21 +50,41 @@
|
|||||||
color: #333333 !important;
|
color: #333333 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-danger {
|
||||||
|
background-color: #FC1683 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-success {
|
||||||
|
background-color: #A4F8D7 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #1347FF;
|
||||||
|
}
|
||||||
|
|
||||||
.text-uppercase {
|
.text-uppercase {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offcanvas {
|
.offcanvas {
|
||||||
width: auto!important;
|
width: auto !important;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
min-width: 60%;
|
min-width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fs-80 {
|
||||||
|
font-size: 0.8rem!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 50rem;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
background-color: #F4F7FA;
|
background-color: #F4F7FA;
|
||||||
@@ -68,21 +92,23 @@ body {
|
|||||||
color: #3D4048;
|
color: #3D4048;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.bg-variant1 {
|
||||||
|
background-color: #F4F7FA;
|
||||||
|
background-image: url("topographic.svg");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: bottom left;
|
||||||
|
background-size: auto 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.bg-variant2 {
|
||||||
|
background-color: #E8EDF2;
|
||||||
|
}
|
||||||
|
|
||||||
body > .container-fluid {
|
body > .container-fluid {
|
||||||
min-height: 100% !important;
|
min-height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#topNav.navbar {
|
#topNav.navbar {
|
||||||
box-shadow: 0 1px 4px rgba(22, 24, 31, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand > div {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand > div p span {
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
@@ -90,18 +116,6 @@ body > .container-fluid {
|
|||||||
font-size: 0.6rem !important;
|
font-size: 0.6rem !important;
|
||||||
color: #707583;
|
color: #707583;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand h1 {
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
line-height: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand h1 a {
|
|
||||||
font-size: 1.1rem !important;
|
|
||||||
color: #0023A3 !important;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,17 +126,16 @@ body > .container-fluid {
|
|||||||
margin: 0.5rem 0.8rem
|
margin: 0.5rem 0.8rem
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand p {
|
.navbar-brand > div {
|
||||||
margin-bottom: 0;
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand p a {
|
.navbar-brand h1 a {
|
||||||
vertical-align: text-bottom;
|
font-size: 1.2rem !important;
|
||||||
}
|
color: #0023A3 !important;
|
||||||
|
text-decoration: none;
|
||||||
.navbar-brand p img {
|
vertical-align: middle;
|
||||||
height: 1.25rem;
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#topNav .navbar i.btn {
|
#topNav .navbar i.btn {
|
||||||
@@ -267,6 +280,10 @@ span.link {
|
|||||||
color: #707583;
|
color: #707583;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#actionButtons .link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
#actionButtons button > * {
|
#actionButtons button > * {
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
@@ -282,9 +299,74 @@ span.link {
|
|||||||
|
|
||||||
.nav-tabs .nav-link {
|
.nav-tabs .nav-link {
|
||||||
padding-bottom: 0.25rem;
|
padding-bottom: 0.25rem;
|
||||||
|
color: #3B3D45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs .nav-link.active {
|
.nav-tabs .nav-link.active {
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 3px solid #3B3D45;
|
border-bottom: 3px solid #3B3D45;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#installedList .b-shadow:hover {
|
||||||
|
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
#btnUpgradeCheck {
|
||||||
|
color: #3B3D45;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btnUpgrade {
|
||||||
|
min-width: 9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sectionDetails > .bg-white {
|
||||||
|
background-color: #F4F7FA !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sectionDetails .list-unstyled .bg-secondary {
|
||||||
|
background-color: #F4F7FA !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-resources .badge {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-resources .bg-secondary {
|
||||||
|
background-color: #E6E7EB !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.res-actions .btn-sm {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offcanvas-header h5 {
|
||||||
|
font-family: Poppins, serif;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offcanvas-header h5 .badge {
|
||||||
|
font-family: Roboto, serif;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.offcanvas-header p {
|
||||||
|
font-family: Inter, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#describeModalBody pre {
|
||||||
|
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"
|
||||||
"io/ioutil"
|
"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"
|
||||||
@@ -25,37 +24,19 @@ 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 "", fmt.Errorf("failed to run command %s:\nError: %s\nSTDERR:%s", cmd, eerr, serr)
|
|
||||||
}
|
|
||||||
return "", 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) {
|
||||||
@@ -96,12 +77,10 @@ func (d *DataLayer) CheckConnectivity() error {
|
|||||||
return errors.New("did not find any kubectl contexts configured")
|
return errors.New("did not find any kubectl contexts configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
_, err = d.runCommandHelm("--help") // no point in doing is, since the default context may be invalid
|
||||||
_, err = d.runCommandHelm("env") // no point in doing is, since the default context may be invalid
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -148,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
|
||||||
@@ -161,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 {
|
||||||
@@ -174,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
|
||||||
}
|
}
|
||||||
@@ -186,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
|
||||||
@@ -200,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) {
|
||||||
@@ -215,7 +240,7 @@ func (d *DataLayer) RevisionManifests(namespace string, chartName string, revisi
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string, revision int) ([]*GenericResource, error) {
|
func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string, revision int) ([]*v1.Carp, error) {
|
||||||
out, err := d.RevisionManifests(namespace, chartName, revision, false)
|
out, err := d.RevisionManifests(namespace, chartName, revision, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -223,7 +248,7 @@ func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string,
|
|||||||
|
|
||||||
dec := yaml.NewDecoder(bytes.NewReader([]byte(out)))
|
dec := yaml.NewDecoder(bytes.NewReader([]byte(out)))
|
||||||
|
|
||||||
res := make([]*GenericResource, 0)
|
res := make([]*v1.Carp, 0)
|
||||||
var tmp interface{}
|
var tmp interface{}
|
||||||
for dec.Decode(&tmp) == nil {
|
for dec.Decode(&tmp) == nil {
|
||||||
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
|
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
|
||||||
@@ -233,12 +258,17 @@ func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var doc GenericResource
|
var doc v1.Carp
|
||||||
err = json.Unmarshal(jsoned, &doc)
|
err = json.Unmarshal(jsoned, &doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,11 +300,11 @@ func (d *DataLayer) RevisionValues(namespace string, chartName string, revision
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) GetResource(namespace string, def *GenericResource) (*GenericResource, error) {
|
func (d *DataLayer) GetResource(namespace string, def *v1.Carp) (*v1.Carp, error) {
|
||||||
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "json")
|
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.HasSuffix(strings.TrimSpace(err.Error()), " not found") {
|
if strings.HasSuffix(strings.TrimSpace(err.Error()), " not found") {
|
||||||
return &GenericResource{
|
return &v1.Carp{
|
||||||
Status: v1.CarpStatus{
|
Status: v1.CarpStatus{
|
||||||
Phase: "NotFound",
|
Phase: "NotFound",
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
@@ -286,7 +316,7 @@ func (d *DataLayer) GetResource(namespace string, def *GenericResource) (*Generi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var res GenericResource
|
var res v1.Carp
|
||||||
err = json.Unmarshal([]byte(out), &res)
|
err = json.Unmarshal([]byte(out), &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -310,6 +340,15 @@ func (d *DataLayer) GetResource(namespace string, def *GenericResource) (*Generi
|
|||||||
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 {
|
||||||
@@ -318,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
|
||||||
@@ -348,42 +387,72 @@ 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) (string, error) {
|
func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string, version string, justTemplate bool, values string, reuseVals bool) (string, error) {
|
||||||
oldVals, err := d.RevisionValues(namespace, name, 0, false)
|
if values == "" && reuseVals {
|
||||||
|
oldVals, err := d.RevisionValues(namespace, name, 0, true)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
values = oldVals
|
||||||
|
}
|
||||||
|
|
||||||
|
valsFile, close1, err := utils.TempFile(values)
|
||||||
|
defer close1()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := ioutil.TempFile("", "helm_vals_")
|
cmd := []string{"upgrade", "--install", "--create-namespace", name, repoChart, "--version", version, "--namespace", namespace, "--values", valsFile, "--output", "json"}
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer os.Remove(file.Name())
|
|
||||||
|
|
||||||
err = ioutil.WriteFile(file.Name(), []byte(oldVals), 0600)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := []string{name, repoChart, "--version", version, "--namespace", namespace, "--values", file.Name()}
|
|
||||||
if justTemplate {
|
if justTemplate {
|
||||||
cmd = append([]string{"template", "--skip-tests"}, cmd...)
|
cmd = append(cmd, "--dry-run")
|
||||||
} else {
|
|
||||||
cmd = append([]string{"upgrade"}, cmd...)
|
|
||||||
cmd = append(cmd, "--output", "json")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := d.runCommandHelm(cmd...)
|
out, err := d.runCommandHelm(cmd...)
|
||||||
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 {
|
||||||
manifests, err := d.RevisionManifests(namespace, name, 0, false)
|
out = strings.TrimSpace(res.Manifest)
|
||||||
if err != nil {
|
}
|
||||||
return "", err
|
|
||||||
}
|
return out, nil
|
||||||
out = getDiff(strings.TrimSpace(manifests), strings.TrimSpace(out), "current.yaml", "upgraded.yaml")
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ShowValues(chart string, ver string) (string, error) {
|
||||||
|
return d.runCommandHelm("show", "values", chart, "--version", ver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ChartRepoList() (res []RepositoryElement, err error) {
|
||||||
|
out, err := d.runCommandHelm("repo", "list", "--output", "json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(out), &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ChartRepoAdd(name string, url string) (string, error) {
|
||||||
|
out, err := d.runCommandHelm("repo", "add", "--force-update", name, url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ChartRepoDelete(name string) (string, error) {
|
||||||
|
out, err := d.runCommandHelm("repo", "remove", name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
@@ -405,16 +474,14 @@ 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)
|
||||||
log.Debugf("The diff is: %s", diff)
|
log.Debugf("The diff is: %s", diff)
|
||||||
return diff
|
return diff
|
||||||
}
|
}
|
||||||
|
|
||||||
type GenericResource = v1.Carp
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
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"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -44,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)
|
||||||
}
|
}
|
||||||
@@ -63,7 +65,7 @@ func TestFlow(t *testing.T) {
|
|||||||
_ = manifests
|
_ = manifests
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
res := make([]*GenericResource, 0)
|
res := make([]*v1.Carp, 0)
|
||||||
for _, m := range manifests {
|
for _, m := range manifests {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
mc := m // fix the clojure
|
mc := m // fix the clojure
|
||||||
@@ -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
@@ -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,17 +0,0 @@
|
|||||||
package dashboard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
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
@@ -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,8 +1,8 @@
|
|||||||
name: "dashboard"
|
name: "dashboard"
|
||||||
version: "0.0.5"
|
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/dashboard"
|
command: "$HELM_PLUGIN_DIR/bin/helm-dashboard"
|
||||||
hooks:
|
hooks:
|
||||||
install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
|
install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
|
||||||
update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
|
update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
|
||||||
|
|||||||
BIN
screenshot.png
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 270 KiB |
BIN
screenshot_scan_manifest.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
screenshot_scan_resource.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Copied w/ love from the chartmuseum/helm-push :)
|
# Copied w/ love from the chartmuseum/helm-push :)
|
||||||
|
|
||||||
name="helm-push"
|
name="helm-dashboard"
|
||||||
repo="https://github.com/komodorio/${name}"
|
repo="https://github.com/komodorio/${name}"
|
||||||
|
|
||||||
if [ -n "${HELM_PUSH_PLUGIN_NO_INSTALL_HOOK}" ]; then
|
if [ -n "${HELM_PUSH_PLUGIN_NO_INSTALL_HOOK}" ]; then
|
||||||
@@ -41,9 +41,9 @@ esac
|
|||||||
|
|
||||||
|
|
||||||
if [ "$(uname)" = "Darwin" ]; then
|
if [ "$(uname)" = "Darwin" ]; then
|
||||||
url="${repo}/releases/download/v${version}/${name}_${version}_darwin_${arch}.tar.gz"
|
url="${repo}/releases/download/v${version}/${name}_${version}_Darwin_${arch}.tar.gz"
|
||||||
elif [ "$(uname)" = "Linux" ] ; then
|
elif [ "$(uname)" = "Linux" ] ; then
|
||||||
url="${repo}/releases/download/v${version}/${name}_${version}_linux_${arch}.tar.gz"
|
url="${repo}/releases/download/v${version}/${name}_${version}_Linux_${arch}.tar.gz"
|
||||||
else
|
else
|
||||||
url="${repo}/releases/download/v${version}/${name}_${version}_windows_${arch}.tar.gz"
|
url="${repo}/releases/download/v${version}/${name}_${version}_windows_${arch}.tar.gz"
|
||||||
fi
|
fi
|
||||||
@@ -55,10 +55,10 @@ mkdir -p "releases/v${version}"
|
|||||||
|
|
||||||
# Download with curl if possible.
|
# Download with curl if possible.
|
||||||
if [ -x "$(which curl 2>/dev/null)" ]; then
|
if [ -x "$(which curl 2>/dev/null)" ]; then
|
||||||
curl -sSL "${url}" -o "releases/v${version}.tar.gz"
|
curl --fail -sSL "${url}" -o "releases/v${version}.tar.gz"
|
||||||
else
|
else
|
||||||
wget -q "${url}" -O "releases/v${version}.tar.gz"
|
wget -q "${url}" -O "releases/v${version}.tar.gz"
|
||||||
fi
|
fi
|
||||||
tar xzf "releases/v${version}.tar.gz" -C "releases/v${version}"
|
tar xzf "releases/v${version}.tar.gz" -C "releases/v${version}"
|
||||||
mv "releases/v${version}/bin/${name}" "bin/${name}" || \
|
mv "releases/v${version}/${name}" "bin/${name}" || \
|
||||||
mv "releases/v${version}/bin/${name}.exe" "bin/${name}"
|
mv "releases/v${version}/${name}.exe" "bin/${name}"
|
||||||
|
|||||||