206 Commits

Author SHA1 Message Date
Andrey Pokhilko
b61adf133f Second attempt to do multiarch image (#370)
* Second attempt to do multiarch image

* Build it

* Arch Args

* Auth for push

* Remove condition

* Fix tag name

* Another try

* Finalize changes
2023-06-21 12:42:14 +01:00
Andrey Pokhilko
27eb7949e5 Change commit message of chart release (#369) 2023-06-19 11:23:31 +01:00
komodor-bot
b90198915e Increment chart versions [skip ci] 2023-06-10 17:48:04 +00:00
Andrey Pokhilko
64975cac42 Build and release ARM docker images (#367)
* Build and release ARM docker images

* Build and release ARM docker images

* Build and release ARM docker images

* Build and release ARM docker images
2023-06-10 17:56:30 +01:00
dependabot[bot]
087399ad49 Bump github.com/docker/distribution (#366)
Bumps [github.com/docker/distribution](https://github.com/docker/distribution) from 2.8.1+incompatible to 2.8.2+incompatible.
- [Release notes](https://github.com/docker/distribution/releases)
- [Commits](https://github.com/docker/distribution/compare/v2.8.1...v2.8.2)

---
updated-dependencies:
- dependency-name: github.com/docker/distribution
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-10 14:12:54 +01:00
Pushkar Pandey
fc385344f4 Doc feature: Release Detail (#364)
Help user to know the capability of Helm-Dashboard
2023-06-10 13:49:46 +01:00
Andrei Pohilko
56932f2c34 Fix build 2023-06-02 13:22:22 +01:00
Andrei Pohilko
24df4a21d6 Fix linter errors after go 1.20 2023-06-02 13:06:10 +01:00
Andrei Pohilko
bea75cb011 Bump up golang to 1.20 2023-06-02 12:22:35 +01:00
dependabot[bot]
a07c8f273d Bump github.com/docker/distribution (#361)
Bumps [github.com/docker/distribution](https://github.com/docker/distribution) from 2.8.1+incompatible to 2.8.2+incompatible.
- [Release notes](https://github.com/docker/distribution/releases)
- [Commits](https://github.com/docker/distribution/compare/v2.8.1...v2.8.2)

---
updated-dependencies:
- dependency-name: github.com/docker/distribution
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 12:19:34 +01:00
dependabot[bot]
eb11a8f26e Bump github.com/gin-gonic/gin from 1.9.0 to 1.9.1 (#363)
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.9.0...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 12:19:17 +01:00
Andrei Pohilko
f0545d35f1 Bump up library dependencies 2023-05-30 09:20:35 +01:00
sachi3050
57f7c47dd1 Update build.yml (#359)
just a little bit of grammar correction.
2023-05-28 17:44:01 +01:00
Pushkar Pandey
0b4031bf24 Doc Feature: Installed Releases List (#356)
* Doc Feature: Installed Releases List

Helps user to know the features of Helm-Dashboard

* updated doc

* doc modify

* modify doc

* modify doc
2023-05-24 10:53:14 +01:00
Pushkar Pandey
e143963d46 Doc Features: Repository (#353)
* Doc Features: Repository

* Docs improvement

* modify doc

* doc correction

* doc improvement
2023-05-15 08:58:10 +01:00
dependabot[bot]
b933e2dd9b Bump github.com/docker/distribution (#354)
Bumps [github.com/docker/distribution](https://github.com/docker/distribution) from 2.8.1+incompatible to 2.8.2+incompatible.
- [Release notes](https://github.com/docker/distribution/releases)
- [Commits](https://github.com/docker/distribution/compare/v2.8.1...v2.8.2)

---
updated-dependencies:
- dependency-name: github.com/docker/distribution
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 08:22:51 +01:00
Andrey Pohilko
0e15fe2001 Fix sorting of versions
Fixes #326
Closes #349
2023-05-10 15:10:58 +03:00
Andrey Pohilko
021fe9c897 Add codecov badge 2023-05-09 11:16:41 +03:00
Andrey Pohilko
5f6104dbba Change name of UT coverage file 2023-05-09 11:09:25 +03:00
Andrey Pohilko
8e9a464d62 Merge remote-tracking branch 'origin/main' 2023-05-09 11:03:40 +03:00
Andrey Pohilko
3a7bb3efb6 Add codecov integration 2023-05-09 11:03:23 +03:00
Pushkar Pandey
d2259241e6 Doc features: Multicluster (#352)
* Doc features: Multicluster

Helps the user to switch to a different cluster.

* Added the screenshot

* Update FEATURES.md

* screenshot added
2023-05-08 20:34:07 +03:00
dependabot[bot]
aad9992302 Bump github.com/gin-gonic/gin from 1.8.1 to 1.9.0 (#350)
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.8.1...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-05 09:47:19 +03:00
Pushkar Pandey
30eb209043 Doc Features: Application shutdown (#347)
* Docs: To know to capability of application

* Create screenshot_shut_down.png

* Update FEATURES.md
2023-04-27 15:50:20 +01:00
Kashish Lakhara
1dcb77812f docs: fixed typos (#340) 2023-04-17 10:51:54 +01:00
Harshit Mehta
245863b2f9 bug: fix sorting of release versions (#336)
Signed-off-by: Harshit Mehta <hdm23061993@gmail.com>
2023-04-14 11:37:31 +01:00
Itiel shwartz
dd1fe05d65 fix yaml parsing (#330) 2023-04-11 17:06:42 +01:00
komodor-bot
450804ba24 Increment chart versions [skip ci] 2023-04-11 11:48:31 +00:00
Andrei Pohilko
a2ddb94c16 Merge branch 'main' of github.com:komodorio/helm-dashboard 2023-04-11 12:15:27 +01:00
Andrei Pohilko
861de33bfe Record event when error is shown 2023-04-11 12:15:09 +01:00
Andrei Pohilko
26d82dd5ab Omit storageClassName if it is null
Fixes #226
2023-04-11 12:10:30 +01:00
Andrei Pohilko
b1294cbe1a Make failed manifest parse be shown less breaking
Relates to #328
2023-04-11 12:09:12 +01:00
dependabot[bot]
d4583a222e Bump github.com/docker/docker (#317)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.21+incompatible to 20.10.24+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v20.10.21...v20.10.24)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-11 09:37:34 +01:00
Satyam Singh
a0bf59edc6 Added Link to the v1.3.0 badge (#319)
* Added Link to the v1.3.0 badge

Added link of Release v1.3.0 to the badge of v1.3.0 and it now redirects to the v1.3.0

* Added link to the release badge 

Added link to the release badge which will now redirect to the latest release
2023-04-09 16:30:07 +01:00
komodor-bot
79a79979e2 Increment chart versions [skip ci] 2023-03-30 12:38:29 +00:00
ElisarEisenbach
76e4fe51b5 Refactoring analytics (#311)
* exampple

* send call type in function

* remove heap check

---------

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

* refactoring analytics functions

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

* Reuse old API request, show icons

* Take progress indication from deployment conditions

* Improve status

* Cleanup

* Fixups

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

* Add support for Username and Passowrd in Add Repo API
2023-03-09 13:34:05 +00:00
Harshit Mehta
0ac8eec368 Minor UI fixes (#234) 2023-03-09 08:45:41 +00:00
komodor-bot
aec46d43f7 Increment chart versions [skip ci] 2023-03-08 09:27:49 +00:00
Andrei Pohilko
37e1d44bf1 Remove survey banner code 2023-03-07 15:07:59 +00:00
Andrei Pohilko
362cb09e6d Improve logo source 2023-03-07 15:00:49 +00:00
Andrei Pohilko
209f5b5e44 Improve upgradable status display 2023-03-07 14:51:46 +00:00
Andrei Pohilko
a0680a4820 Add links to Komodor 2023-03-07 12:51:39 +00:00
Andrei Pohilko
d95cac94d5 Auto-update repositories each 10 minutes, unless HD_NO_AUTOUPDATE is set 2023-03-06 11:38:33 +00:00
Andrey Pokhilko
bbb425bfea Query ArtifactHub for repo suggestion (#225)
* Query ArtifactHub for repo suggestion

* Refactor & improve

* Add notice on local chart support
2023-03-02 10:22:32 +00:00
komodor-bot
679d31e4ab Increment chart versions [skip ci] 2023-02-22 12:45:52 +00:00
Andrei Pohilko
3119d17738 Ignore test file 2023-02-22 11:35:02 +00:00
Andrei Pohilko
778e58360c Fix the values not considered correctly when loading the upgrade preview 2023-02-22 11:24:03 +00:00
Andrei Pohilko
a7c7ba80fe Overwrite old values correctly, don't cache current resources list 2023-02-21 15:22:49 +00:00
dependabot[bot]
d86c46aabf Bump golang.org/x/net from 0.5.0 to 0.7.0 (#220)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.5.0 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.5.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-18 09:16:04 +00:00
Harshit Mehta
c79259275a Execute install script in debug mode only when HELM_DEBUG flag is set (#219) 2023-02-17 12:52:50 +00:00
Andrey Pokhilko
4a4760d5b8 Update README.md 2023-02-16 16:36:13 +00:00
dependabot[bot]
244e35bb6b Bump github.com/containerd/containerd from 1.6.15 to 1.6.18 (#216)
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.6.15 to 1.6.18.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.6.15...v1.6.18)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-16 15:08:46 +00:00
Andrei Pohilko
709c3c600b Fix inability to reconfigure charts without corresponding repository 2023-02-16 12:32:48 +00:00
komodor-bot
3060b92f8e Increment chart versions [skip ci] 2023-02-15 18:00:36 +00:00
Andrey Pokhilko
f49f52efe4 Support working with local charts (#215)
* Basic functioning

* Support reconfiguring

* Improve tests coverage

* Always update local repo, don't offer to delete it

* Handle multi-repo correctly

* Document local charts usage

* Screenshot for docs
2023-02-15 16:45:28 +00:00
Bhargav Ravuri
6a4ca793c9 New CLI Flag --devel To Include Development/Prerelease Versions of Charts (#139)
* Include devel Flag for Toggling Dev Chart Versions

The flag `--devel` for enabling/disabling dev versions
of charts in following endpoints:
1. /api/helm/repositories/kafka-operator
2. /api/helm/repositories/versions
3. /api/helm/repositories/latestver

Signed-off-by: Bhargav Ravuri <bhargav.ravuri@infracloud.io>

* Run Tests on Devel Flag Related Changes

Signed-off-by: Bhargav Ravuri <bhargav.ravuri@infracloud.io>

---------

Signed-off-by: Bhargav Ravuri <bhargav.ravuri@infracloud.io>
2023-02-11 16:02:01 +00:00
Andrey Pokhilko
61b67f8bed Display UI indication if chart is upgradable (#211) 2023-02-10 12:36:40 +00:00
dependabot[bot]
ac690b6332 Bump helm.sh/helm/v3 from 3.10.3 to 3.11.1 (#212)
Bumps [helm.sh/helm/v3](https://github.com/helm/helm) from 3.10.3 to 3.11.1.
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.10.3...v3.11.1)

---
updated-dependencies:
- dependency-name: helm.sh/helm/v3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-09 11:25:58 +00:00
Andrei Pohilko
b613e4e9dc Fix list of releases not displaying pending 2023-02-08 12:07:49 +00:00
Andrei Pohilko
a9939d5067 Improve tests for repositories
Relates to #210
2023-02-08 10:11:18 +00:00
Andrei Pohilko
7a25335028 Fix PVC+PV definition in chart 2023-02-05 15:56:45 +00:00
Itiel shwartz
8befc1d017 Update README.md
Update survey in the readme
2023-02-02 11:46:04 +02:00
komodor-bot
aaf6ae80c5 Increment chart versions [skip ci] 2023-02-01 16:20:28 +00:00
Andrei Pohilko
8faa5dabd5 New features of v1.0 2023-02-01 16:01:35 +00:00
Andrei Pohilko
0b891e7c28 Banner with survey link 2023-02-01 15:38:20 +00:00
Andrei Pohilko
2736777b8a Mod tidy 2023-02-01 13:41:39 +00:00
dependabot[bot]
96c789a012 Bump github.com/containerd/containerd from 1.6.6 to 1.6.12 (#207)
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.6.6 to 1.6.12.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.6.6...v1.6.12)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-01 13:39:55 +00:00
Andrey Pokhilko
e13aa2fde6 [WIP] Major release 1.0 (#147)
* Object model with self-sufficient binary (#131)

* Code cosmetics

* Experimenting with object model and direct HELM usage

* Experiment with object model

* replacing the kubectl

* Progressing

* Save the progress

* Able to start with migration in mind

* Migrated two pieces

* List releases via Helm

* Forgotten field

* Cristallized the problem of ctx switcher

* Reworked to multi-context

* Rollback is also new style

* More migration

* Refactoring

* Describe via code

* Bye-bye kubectl binary

* Eliminate more old code

* Refactor a bit

* Merges

* No binaries in dockerfile

* Commit

* Progress with getting the data

* Learned the thing about get

* One field less

* Sstart with repos

* Repo add

* repo remove

* Repos! Icons!

* Simplified access to data

* Ver listing works

* Ver check works

* Caching and values

* fixup

* Done with repos

* Working on install

* Install work-ish

* Fix UI failing on install

* Upgrade flow works

* Fix image building

* Remove outdated test file

* Move files around

* REfactorings

* Cosmetics

* Test for cache control (#151)

* Files import formatted

* Added go-test tools

* Added test for no-cache header

* added changes

* test for cache behaviour of app

* test for static route (#153)

* Tests: route configuration & context setter (#154)

* Test for route configuration

* Test for context setter middleware

* implemented changes

* Restore coverage profile

Fixes #156

* Cosmetics

* Test for `NewRouter` function (#157)

* Test for `configureScanners` (#158)

* Test for `configureKubectls` (#163)

* Test for repository loading (#169)

- Created `repos_test.go`
- Test: `Load()` of Repositories

* Build all PRs

* Fixes failing test (#171)

* Fixes failing test
- Fixes failing test of repo loading

* handles error for

* Did some changes

* Test for listing of repos (#173)

- and did some code formatting

Signed-off-by: OmAxiani0 <aximaniom@gmail.com>

Signed-off-by: OmAxiani0 <aximaniom@gmail.com>

* Test for adding repo (#175)

- Modified the `repositories.yml` file

Signed-off-by: OmAxiani0 <aximaniom@gmail.com>

Signed-off-by: OmAxiani0 <aximaniom@gmail.com>

* Test for deleting the repository (#176)

* Test for deleting the repository
- Also added cleanup function for `TestAdd`

* Fixes failing test

* Add auto labeler for PR's (#174)

* Add auto labeler for PR's

* Add all file under .github/workflow to 'ci' label

Co-authored-by: Harshit Mehta <harshitm@nvidia.com>

* Test for getting repository (#177)

* Add github workflow for auto PR labeling (#181)

Co-authored-by: Harshit Mehta <harshitm@nvidia.com>

* Stub compilation

* Fixes around installing

* More complex test

* Using object model to execute helm test (#191)

* Expand test

* More test

* Coverage

* Add mutex for operations

* Rectore cluster detection code

* Change receiver to pointer

* Support multiple namespaces

* Cosmetics

* Update repos periodically

* fix tests

* Fix error display

* Allow reconfiguring chart without repo

* mute  linter

* Cosmetics

* Failing approach to parse manifests

Relates to #30

* Report the error properly

*  Add test for dashboard/objects/data.go NewDataLayer (#199)

* Fix problem of wrong namespace

* Added unit tests for releases (#204)

* Rework API routes (#197)

* Bootstrap OpenAPI doc

* Renaming some routes

* Listing namespaces

* k8s part of things

* Repositories section

* Document scanners API

* One more API call

* Progress

* Reworked install flow

* History endpoint

* Textual info section

* Resources endpoint

* Rollback endpoint

* Rollback endpoint

* Unit tests

* Cleanup

* Forgotten tags

* Fix tests

* TODOs

* Rework manifest scanning

* add hasTests flag

* Adding more information on UI for helm test API response (#195)

* Hide test button when no tests

Fixes #115
Improves #195

---------

Signed-off-by: OmAxiani0 <aximaniom@gmail.com>
Co-authored-by: Om Aximani <75031769+OmAximani0@users.noreply.github.com>
Co-authored-by: Harshit Mehta <hdm23061993@gmail.com>
Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
Co-authored-by: Todd Turner <todd@toddtee.sh>
Co-authored-by: arvindsundararajan98 <109727359+arvindsundararajan98@users.noreply.github.com>
2023-02-01 13:24:34 +00:00
Andrei Pohilko
6ffcdf2b8e Commit plugin.yaml update upon release 2023-02-01 09:58:26 +00:00
Andrei Pohilko
e6d6ff41a9 Revert "Update release workflow (#202)"
This reverts commit 2df533ab5b.
2023-02-01 09:34:44 +00:00
Dennis Ploeger
0e45e89a1e fix: Make PVC use storageClass from values (#206) 2023-01-31 15:34:13 +00:00
ronahk
2df533ab5b Update release workflow (#202)
* Update release workflow

* Update release workflow

* Change label
2023-01-26 14:12:51 +00:00
Todd Turner
7e32008bfe 🔧 Allow binary arguments via helm chart (#201)
This commit allows to pass arguments to the helm chart.
This is useful if you want to specify arguments for the
helm-dashboard binary whilst deploying via helm.
2023-01-26 14:12:16 +00:00
iiTidgex
23cfd2d61b support for deployment strategy (#196)
Co-authored-by: larmitage_wh <luke.armitage@williamhill.com>
2023-01-23 08:38:57 +00:00
Andrei Pohilko
0e6231dfbd Better debug flag logic 2023-01-20 13:32:55 +00:00
Andrei Pohilko
4ebf67095a Add note about auth proxies 2023-01-18 20:04:40 +00:00
Andrei Pohilko
5902bc010e remove undesired output from version check 2023-01-17 10:44:49 +00:00
Andrei Pohilko
36217401c8 Merge branch 'main' of github.com:komodorio/helm-dashboard 2023-01-17 10:39:30 +00:00
Andrei Pohilko
e6381f96e9 Add check for install script 2023-01-17 10:38:51 +00:00
Harshit Mehta
efe394c8c4 Provide execution rights to install script (#192) 2023-01-17 10:30:34 +00:00
Andrei Pohilko
a91287c2ff Convert line endings back 2023-01-16 20:09:42 +00:00
Andrei Pohilko
28a4b37bb5 Add namespace parameter to chart values
closes #189
2023-01-16 19:15:36 +00:00
Andrei Pohilko
2e1f2e481b Conditional debug flag for install script 2023-01-16 18:44:36 +00:00
Andrei Pohilko
714c0f9e02 Merge branch 'main' of github.com:komodorio/helm-dashboard 2023-01-16 09:58:29 +00:00
Andrei Pohilko
35421ede58 Change install_script.sh line endings to \r\n
Relates to #188
2023-01-16 09:58:08 +00:00
ronahk
26e0b1db32 Fix typo in readme 2023-01-15 13:40:06 +02:00
ronahk
50947e585d Break filters text when resizing window (#190) 2023-01-15 13:25:41 +02:00
ronahk
40ae829186 Use v3 checkout action in all places 2023-01-15 12:46:21 +02:00
ronahk
87ee388bfb Handle errors in regex match 2023-01-15 12:18:40 +02:00
Andrei Pohilko
b76fbb130a Merge branch 'main' of github.com:komodorio/helm-dashboard 2023-01-13 12:41:05 +00:00
Todd Turner
f3c66ecf03 🎨 Remove Superfluous Rule from helm-dashboard ClusterRole (#187)
A superfluous rule is added to the ClusterRole upon creation,
when the dashboard.allowWriteActions value is set to true.
This commit will ensure that only a single rule is created within
the ClusterRole, regardless of whether the dashboard.allowWriteActions
value is enabled or not.
The verbs within this rule will update accordingly.
2023-01-13 10:56:48 +00:00
Harshit Mehta
83e4348ace Add option to execute Tests for release (#178)
* Add button to execute tests

* Create API to execute tests

* Add modal for Test response

* Make API call to execute tests and show response in modal

* Clean up

* Update docs - feature execute tests for a release

* Add arg '--logs' to 'helm test' cmd

* Wait for API to complete before sending back response to frontend

* Add loading spinner until reponse for 'helm test' is returned from backend by API

* Clean-up

Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2023-01-12 12:35:11 +00:00
Harshit Mehta
af1c09ae02 Update Kommunity link (#184) 2023-01-12 11:21:04 +00:00
Udi Hofesh
6e5bce26e8 Update Kommunity link (#183)
The previous link has expired. Adding a never failing signup page: https://komodorkommunity.slack.com/
2023-01-12 09:29:21 +00:00
Harshit Mehta
d0d2ed3ef1 Add github workflow for auto PR labeling (#179)
Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2023-01-09 14:35:02 +00:00
Harshit Mehta
450190a1aa Add auto labeler for PR's (#172)
Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2023-01-06 08:26:13 +00:00
Harshit Mehta
74aab13e3e Use github api to fetch latest release version (#170)
* Use github api to fetch latest release version

* Use grep instead of jq for querying json response

Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2023-01-05 11:42:47 +00:00
Om Aximani
c5cd12b6b2 Ignore cover profile file (#165)
- Added `.cov` extension to gitignore
2023-01-02 13:29:10 +00:00
Om Aximani
fd650f10b6 Changed version of action (#160)
- fix: #159
2023-01-01 16:30:45 +00:00
ronahk
3e1e4be4b3 Update plugin.yaml to latest release version (#155) 2022-12-31 09:26:55 +00:00
Andrei Pohilko
d0c9de9718 Merge branch 'main' of github.com:komodorio/helm-dashboard 2022-12-24 12:14:51 +00:00
ronahk
1a1b28d09f Markdown additions (#150)
* beautify readme

* beautify readme

* merge

* remove file

* center icon
2022-12-24 09:42:22 +00:00
Nirav_Prajapati
6c4ee06d27 feat: added pull request template (#149) 2022-12-22 15:51:13 +02:00
Nirav_Prajapati
7c5ffe9c07 feat: Add issue template (#146)
added issue template
2022-12-22 10:01:47 +02:00
Om Aximani
196644683c Added 'debug' flag in values (#143)
* Added 'debug' flag in values

- Helps to set environment on debug mode
- Can fix #142

* Addressed changes
2022-12-19 19:08:30 +02:00
komodor-bot
20ee6e9695 Increment chart versions [skip ci] 2022-12-19 12:30:52 +00:00
Andrey Pohilko
e9abedbadb Merge branch 'main' of github.com:komodorio/helm-dashboard 2022-12-19 12:38:17 +02:00
Andrey Pohilko
5d0a148fea Add installMode to analytics 2022-12-19 12:38:03 +02:00
ronahk
c0cf6237e6 Helm chart updates (#141)
* update plugin version

* fix pipeline

* fix pipeline

* add needs

* update chart icon

* allow all api groups

* allow write actions by default
2022-12-19 12:29:52 +02:00
Andrey Pohilko
9876b6a12e Merge branch 'main' of github.com:komodorio/helm-dashboard 2022-12-19 12:27:20 +02:00
Andrey Pohilko
90815f2271 Pass version to container image correctly 2022-12-19 12:27:02 +02:00
Eddie Garcia
e83ddbb15d Release 0.3.0 (#140) 2022-12-19 11:35:18 +02:00
komodor-bot
78f458112b Increment chart versions [skip ci] 2022-12-18 13:07:55 +00:00
Andrey Pokhilko
f6d3e519e2 Allow installing into cluster (#128)
* Dockerize it

* Default chart layout

* Installable

* Starts and loads

* Progressing

* Hide cluster block

* Add scanners

* Add icon for helm chart (#130)

Co-authored-by: Harshit Mehta <harshitm@nvidia.com>

* Image build and push scripts

* Build local img

* Local img

* ci stuff

* ci and chart changes

* add readme

* modify readme

* move readme location

* update docs and delete file

* remove file

* allow write actions

* allow write actions

* update .gitignore

* update readme

* delete file

* add persistence and update documentation

* update logo

* update volume paths and documentation

* change pvc size

* Comment

Co-authored-by: Harshit Mehta <hdm23061993@gmail.com>
Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
Co-authored-by: ronahk <rona@komodor.io>
2022-12-18 14:09:07 +02:00
dependabot[bot]
9bac7306a4 Bump helm.sh/helm/v3 from 3.9.4 to 3.10.3 (#137)
Bumps [helm.sh/helm/v3](https://github.com/helm/helm) from 3.9.4 to 3.10.3.
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.9.4...v3.10.3)

---
updated-dependencies:
- dependency-name: helm.sh/helm/v3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-15 20:51:34 +02:00
ronahk
030708e7cc Create artifacthub-repo.yml (#136) 2022-12-13 11:11:08 +02:00
Andrey Pohilko
0b8a258f7f Add logo for dark theme 2022-12-11 15:04:16 +02:00
Andrei Pohilko
2c25193adf Code cosmetics 2022-12-06 15:31:18 +00:00
Christfried
2c1883c835 Fix typo in readme (#129)
Co-authored-by: Christfried BALIZOU <christfriedbalizou.gmail.com>
2022-12-04 17:37:45 +00:00
Harshit Mehta
960c268224 Display correct message on installed charts page in case of an error instead of the current loading spinner (#127)
Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2022-11-30 14:32:38 +00:00
Itiel shwartz
c4e5094ce5 Update README.md (#126) 2022-11-30 09:35:07 +00:00
ronahk
74f6236ba6 Display better status message (#125) 2022-11-29 16:10:52 +00:00
Andrei Pohilko
9b8edb6a39 Merge branch 'main' of github.com:komodorio/helm-dashboard 2022-11-29 13:22:23 +00:00
Andrei Pohilko
56e9430155 Fix wrong links show for release without repo 2022-11-29 13:22:12 +00:00
siddhikhapare
a89ccbdab7 Overlay with explaination modified (#123) 2022-11-29 11:11:22 +00:00
ronahk
fa4819b353 Update installed charts number according to filter (#121) 2022-11-28 11:02:16 +00:00
Andrei Pohilko
8de7941063 Fix JS error on version parsing 2022-11-27 09:48:28 +00:00
ronahk
34158a7a9c Fix CI - change output name (#119) 2022-11-27 09:46:12 +00:00
Andrei Pohilko
3384db7193 In case no repositories configured, fall back to empty search 2022-11-24 16:25:30 +00:00
Andrei Pohilko
7de7c85426 Release 0.2.8 2022-11-24 16:01:36 +00:00
Andrei Pohilko
8e65c555e0 Fix analytics is enabled while in dev 2022-11-24 15:22:50 +00:00
Andrei Pohilko
2557e6b73d Don't complain if repo is not found for chart
Fixes #116
2022-11-24 15:15:20 +00:00
Andrei Pohilko
4f75ee06a0 Release 0.2.7 2022-11-24 12:07:29 +00:00
ronahk
717adc9e9c fix filter bug (#111) 2022-11-24 11:40:30 +00:00
Andrey Pokhilko
15adeb7cfa Only display NS that has charts (#113) 2022-11-24 13:21:20 +02:00
Itiel shwartz
0b06036a39 add stats and change heap (#108)
* add stats and change heap

* improve analytics

* revert main changes

* add repo
2022-11-24 10:35:45 +00:00
Andrei Pohilko
f7d4dcbff4 If cluster access is failed, still display UI
Fixes #109
2022-11-24 09:59:44 +00:00
Harshit Mehta
8334f2b0b2 Make the entire card clickable on Installed Charts view (#87)
Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2022-11-24 09:15:33 +00:00
Harshit Mehta
5cccb1caa0 Fixes JS part of #95 (#110)
Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2022-11-24 08:24:57 +00:00
Andrei Pohilko
db9cdeb1c9 Split data layer into separate files 2022-11-23 12:38:03 +00:00
ronahk
3abae8e49e Filter installed charts by namespace (#101)
* filter by namespace

* exists

* Some improvements, one thing resolved

* cleanup

* merge

* allow filtering by name

* filter by namespace

* changes

* change url parameter name

* keep filtered namespaces when refreshing and combine inpt and namespace filtering

* Refactoring

* Cleanup

* Forced NS handle

* remove else

Co-authored-by: Andrei Pohilko <andrei.pokhilko@gmail.com>
2022-11-23 13:38:09 +02:00
Andrey Pokhilko
bedb356b02 In-memory cache for speed-up (#88)
* Experiment with local cache

* Commit

* Cache all we can, invalidate later

* Commit

* separate cache class

* More cached

* Proper invalidate

* Complete the repos

* Fix the build

* Fix build

* Status reporting
2022-11-22 17:17:32 +02:00
ronahk
34a7dc57b2 Use namespace defined in kubeconfig as default in install view (#99)
* respect defualt namespace from kubeconfig

* respect defualt namespace from kubeconfig

* Replace exists with available - avoid confusion in status

* use data attribute

* remove commented line

* fix
2022-11-16 11:50:32 +00:00
ronahk
d0dbb42492 fix namespaces not updating when switching cluster (#102) 2022-11-16 09:59:38 +00:00
denganliang
1393b117cf fixbug: ChartAndVersion did not handle chart version correct in some case (#98)
* fixbug: ChartAndVersion did not handle chart version correct in some case

https://github.com/komodorio/helm-dashboard/issues/95

* use regexp to get version

* add test case
2022-11-15 15:12:07 +00:00
Harshit Mehta
cf407c63a2 Fixes #91 (#93)
Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2022-11-11 08:43:55 +00:00
ronahk
3f00e8ef6d Sort resources by interesting items (#89)
* sort resources by intersting items

* add body class

* reuse the name data
2022-11-10 16:51:14 +00:00
ronahk
758b03de36 Validate tag name matches plugin version in gh actions (#80)
* validate that tag name matches plugin version in gh actions

* fail workflow if tag name doesn't match plugin version
2022-11-09 13:35:28 +00:00
Harshit Mehta
76d55f8e44 Enhancement/show chart icon and description (#70)
* New API to fetch chart.yaml for an installec chart

* Show icon and description for installed chart list

Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2022-11-09 08:23:44 +00:00
Andrei Pohilko
b9392ab4c9 Merge branch 'main' of github.com:komodorio/helm-dashboard 2022-11-08 14:38:01 +00:00
Andrei Pohilko
dadf2d1bde Cosmetics 2022-11-08 14:37:48 +00:00
Harshit Mehta
74c2a3d6e7 Few UI Enhancements (#79)
- Add Install/Upgrade keywork dynamically on popup
- Sort Repo list alphabetically
- Sort CLuster list alphabetically

Co-authored-by: Harshit Mehta <harshitm@nvidia.com>
2022-11-08 14:28:13 +00:00
Andrei Pohilko
2454fcf47c Show better message for empty contexts 2022-11-08 10:10:19 +00:00
siddhikhapare
96a7a429e1 getCleanClusterName fixed (#78) 2022-11-08 10:07:30 +00:00
Bhargav Ravuri
f29800ed5b CLI Flag --bind (#77)
* CLI Flag for Bind Address

Signed-off-by: Bhargav Ravuri <bhargav.ravuri@infracloud.io>

* Update Documentation for --bind

Signed-off-by: Bhargav Ravuri <bhargav.ravuri@infracloud.io>

Signed-off-by: Bhargav Ravuri <bhargav.ravuri@infracloud.io>
2022-11-07 16:05:01 +00:00
Andrei Pohilko
15ce9170f3 Don't take Helm's default namespace into account 2022-11-06 16:40:53 +00:00
Andrei Pohilko
9a144c1c6f Release 0.2.5 2022-11-06 16:22:30 +00:00
Andrei Pohilko
f897c0f197 Display forced namespace in UI 2022-11-06 16:16:36 +00:00
Andrey Pokhilko
671fa949df Improve resource scanning flow (#68)
* List supported resources for scanners

* Don't warn on scanner discovery commands

* Use scanner-to-resource map

* Save changes

* Scan result tabs

* Own table render for Checkov

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

* add namespace dropdown

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

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

* Mention analytics in README

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

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

* Use a library to parse CLI flags

* Use less env vars

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

* Check for newer version and offer upgrade

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

* Create Help section, no charts placeholder

* Revive missed "user-defined" values

* Fix namespace undefined upon install

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

* add unit test chart and version
2022-10-25 14:02:48 +01:00
Dimas Yudha P
bd058ee912 removing else flow (#38) 2022-10-25 12:57:25 +01:00
Andrei Pohilko
997f951d0c Fix error when no scanners are present 2022-10-24 15:42:19 +01:00
103 changed files with 7920 additions and 1741 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
Dockerfile
*.md
bin
.idea
dashboard/node_modules

59
.github/ISSUE_TEMPLATE/bug.yaml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: 🐛 Bug
description: Report an issue to help improve the project.
labels: ["🛠 goal: fix"]
body:
- type: textarea
id: description
attributes:
label: Description
description: A brief description of the question or issue, also include what you tried and what didn't work
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
validations:
required: false
- type: textarea
id: extrainfo
attributes:
label: Additional information
description: Is there anything else we should know about this bug?
validations:
required: false
- type: markdown
attributes:
value: |
You can also join our slack community [here](https://komodorkommunity.slack.com)
Feel free to check out other cool repositories of the [komodorio](https://github.com/komodorio)

68
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
# This configures label matching for PR's.
#
# The keys are labels, and the values are lists of minimatch patterns
# to which those labels apply.
#
# NOTE: This can only add labels, not remove them.
# NOTE: Due to YAML syntax limitations, patterns or labels which start
# with a character that is part of the standard YAML syntax must be
# quoted.
#
# Please keep the labels sorted and deduplicated.
api:
- pkg/dashboard/api.go
app:
- main.go
- pkg/dashboard/server.go
- pkg/dashboard/subproc/*
- pkg/dashboard/utils/*
backend:
- pkg/dashboard/handlers/*
- pkg/dashboard/scanners/*
ci:
- .github/workflow/build.yml
- ci/*
- Makefile
- scripts/*
docs:
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- LICENSE
- README.md
- screenshot*.png
- screenshot*.svg
docker:
- .dockerignore
- Dockerfile
helm-charts:
- charts/*
github-actions:
- .github/ISSUE_TEMPLATE/*
- .github/labeler.yml
- .github/pull_request_template
- .github/workflow/pull-request-labeler.yaml
release:
- .github/workflows/publish-chart.yaml
- .github/workflows/release.yaml
- .goreleaser.yml
- artifacthub-repo.yml
- plugin.yaml
scanners:
- pkg/dashboard/scanners/*
tests:
- pkg/dashboard/**/*_test.go
- pkg/dashboard/objects/testdata/*
frontend:
- pkg/dashboard/static/*

21
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,21 @@
## Changes Proposed
<!-- Describe the proposed changes and any additional information -->
<!-- Add all the screenshots which illustrate your changes -->
## Check List
<!-- Mark all the applicable boxes. To mark the box as done follow the following conventions -->
<!--
[x] - Correct; marked as done
[X] - Correct; marked as done
[ ] - Not correct; marked as **not** done
-->
- [ ] The title of my pull request is a short description of the changes
- [ ] This PR relates to some issue: <!-- use "Closes #999" to auto-close related issue -->
- [ ] I have documented the changes made (if applicable)
- [ ] I have covered the changes with unit tests

View File

@@ -2,9 +2,11 @@ name: Build
on:
push:
branches: [ "main" ]
branches:
- main
pull_request:
branches: [ "main" ]
branches:
- "*"
jobs:
build:
@@ -15,10 +17,12 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: "1.20"
- name: Unit tests
run: |
go test -v -race ./... # Run all the tests with the race detector enabled
go test -v -race ./... -covermode=atomic -coverprofile=coverage.out # Run all the tests with the race detector enabled
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
- name: Static analysis
run: |
go vet ./... # go vet is the official Go static analyzer
@@ -31,13 +35,69 @@ jobs:
with:
version: latest
args: release --snapshot --rm-dist
- name: Test Binary is Runnable
- name: Test if the Binary is Runnable
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
- uses: actions/upload-artifact@v3
with:
name: binaries
path: dist/
retention-days: 1
- name: golangci-lint
uses: golangci/golangci-lint-action@v3.2.0
uses: golangci/golangci-lint-action@v3.3.1
with:
# version: latest
# skip-go-installation: true
skip-pkg-cache: true
skip-build-cache: true
# args: --timeout=15m
# args: --timeout=15m
image:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Docker meta
uses: docker/metadata-action@v3
id: meta
with:
images: komodorio/helm-dashboard
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_PASS }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: komodorio/helm-dashboard:unstable
labels: ${{ steps.meta.outputs.labels }}
build-args: VER=0.0.0-dev
platforms: linux/amd64,linux/arm64
helm_check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Helm Template Check For Sanity
uses: igabaydulin/helm-check-action@0.1.4
env:
CHART_LOCATION: ./charts/helm-dashboard
CHART_VALUES: ./charts/helm-dashboard/values.yaml
- name: Test if the Helm plugin install script is runnable
run: |
scripts/install_plugin.sh

43
.github/workflows/publish-chart.yaml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: publish helm chart
# for manual running in case we need to update the chart without releasing the dashboard app
on:
workflow_dispatch:
env:
HELM_REP: helm-charts
GH_OWNER: komodorio
CHART_DIR: charts/helm-dashboard
jobs:
publish_chart:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Bump versions
run: |
git config user.email komi@komodor.io
git config user.name komodor-bot
git fetch --tags
git checkout main
sh ./ci/bump-versions.sh
git add charts/helm-dashboard/Chart.yaml
git commit -m "Increment chart versions [skip ci]" || echo "Already up-to-date"
git push -f || echo "Nothing to push!"
env:
APP_VERSION: ${{ needs.pre_release.outputs.release_tag }}
- name: Push folder to helm-charts repository
uses: crykn/copy_folder_to_another_repo_action@v1.0.6
env:
API_TOKEN_GITHUB: ${{ secrets.KOMI_WORKFLOW_TOKEN }}
with:
source_folder: "charts/helm-dashboard"
destination_repo: "komodorio/helm-charts"
destination_folder: "charts/helm-dashboard"
user_email: "komi@komodor.io"
user_name: "komodor-bot"
destination_branch: "master"
commit_msg: "feat(helm-dashboard): update chart" #important!! don't change this commit message unless you change the condition in pipeline.yml on helm-charts repo

View File

@@ -0,0 +1,14 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -3,10 +3,29 @@ name: release
on:
push:
tags:
- "*"
- "v*"
env:
HELM_REP: helm-charts
GH_OWNER: komodorio
CHART_DIR: charts/helm-dashboard
jobs:
pre_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get tag name
id: get_tag_name
run: echo "TAG_NAME=$(echo ${{ github.ref_name }} | cut -d 'v' -f2)" >> $GITHUB_OUTPUT
outputs:
release_tag: ${{ steps.get_tag_name.outputs.TAG_NAME }}
release:
needs: pre_release
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -16,7 +35,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: "1.20"
- name: git cleanup
run: git clean -f
- name: Run GoReleaser
@@ -28,3 +47,75 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Test Binary Versions
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
image:
runs-on: ubuntu-latest
needs: [release, pre_release]
timeout-minutes: 60
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: Docker meta
uses: docker/metadata-action@v3
id: meta
with:
images: komodorio/helm-dashboard
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_PASS }}
- name: Build and push
uses: docker/build-push-action@v4
if: github.event_name != 'pull_request'
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: komodorio/helm-dashboard:${{ needs.pre_release.outputs.release_tag }},komodorio/helm-dashboard:latest
labels: ${{ steps.meta.outputs.labels }}
build-args: VER=${{ needs.pre_release.outputs.release_tag }}
platforms: linux/amd64,linux/arm64
publish_chart:
runs-on: ubuntu-latest
needs: [image, pre_release]
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Bump versions
run: |
git config user.email komi@komodor.io
git config user.name komodor-bot
git fetch --tags
git checkout main
sh ./ci/bump-versions.sh
git add charts/helm-dashboard/Chart.yaml
git add plugin.yaml
git commit -m "Increment chart versions [skip ci]" || echo "Already up-to-date"
git push -f || echo "Nothing to push!"
env:
APP_VERSION: ${{ needs.pre_release.outputs.release_tag }}
- name: Push folder to helm-charts repository
uses: crykn/copy_folder_to_another_repo_action@v1.0.6
env:
API_TOKEN_GITHUB: ${{ secrets.KOMI_WORKFLOW_TOKEN }}
with:
source_folder: "charts/helm-dashboard"
destination_repo: "komodorio/helm-charts"
destination_folder: "charts/helm-dashboard"
user_email: "komi@komodor.io"
user_name: "komodor-bot"
destination_branch: "master"
commit_msg: "feat(OSS helm-dashboard): ${{ github.event.head_commit.message }}" #important!! don't change this commit message unless you change the condition in pipeline.yml on helm-charts repo

5
.gitignore vendored
View File

@@ -15,6 +15,7 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
*.cov
# Dependency directories (remove the comment below to include it)
# vendor/
@@ -24,3 +25,7 @@ go.work
/bin
/.idea/
.DS_Store
.vscode/
/pkg/dashboard/objects/testdata/hello-world-0.1.0.tgz

View File

@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
https://komodorkommunity.slack.com/archives/C044U1B0265.
itiel@komodor.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the

55
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,55 @@
# Contributing to Helm Dashboard
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
## We Develop with GitHub
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
## First-time contributors
We've tagged some issues to make it easy to get started :smile:
[Good first issues](https://github.com/komodorio/helm-dashboard/labels/good%20first%20issue)
Add a comment on the issue and wait for the issue to be assigned before you start working on it. This helps to avoid multiple people working on similar issues.
## All Code Changes Happen Through Pull Requests
Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:
1. Fork the repo and create your branch from `main`.
2. If you've added code that should be tested, add tests.
3. Ensure the test suite passes.
4. Make sure your code lints.
5. Issue that pull request!
## Any contributions you make will be under the Apache License 2.0
In short, when you submit code changes, your submissions are understood to be under the same [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) that covers the project.
## Report bugs using GitHub's [issues](https://github.com/komodorio/helm-dashboard/issues)
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/komodorio/helm-dashboard/issues/new) and labeling it with the `bug` label. It's that easy!
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
## License
By contributing, you agree that your contributions will be licensed under its Apache License 2.0.
## Questions?
Contact us on [Slack](https://komodorkommunity.slack.com).

51
Dockerfile Normal file
View File

@@ -0,0 +1,51 @@
# Stage - builder
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang as builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS:-linux}
ENV GOARCH=${TARGETARCH:-amd64}
ENV CGO_ENABLED=0
WORKDIR /build
COPY go.mod ./
COPY go.sum ./
COPY main.go ./
RUN go mod download
ARG VER=0.0.0
ENV VERSION=${VER}
ADD . src
WORKDIR /build/src
RUN make build
# Stage - runner
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine
ARG TARGETPLATFORM
ARG BUILDPLATFORM
EXPOSE 8080
# Python
RUN apk add --update --no-cache python3 curl && python3 -m ensurepip && pip3 install --upgrade pip setuptools
# Trivy
RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.18.3
RUN trivy --version
# Checkov scanner
RUN (pip3 install checkov packaging==21.3 && checkov --version) || echo Failed to install optional Checkov
COPY --from=builder /build/src/bin/dashboard /bin/helm-dashboard
ENTRYPOINT ["/bin/helm-dashboard", "--no-browser", "--bind=0.0.0.0", "--port=8080"]
# docker build . -t komodorio/helm-dashboard:0.0.0 && kind load docker-image komodorio/helm-dashboard:0.0.0

57
FEATURES.md Normal file
View File

@@ -0,0 +1,57 @@
# Shutting down the app
To close Helm-dashboard, click on the button in the rightmost corner of the screen. Once you click on it, your Helm-dashboard will be shut down.
![Shutdown_screenshot](images/screenshot_shut_down.png)
# Multicluster
If you want to switch to a different cluster, simply click on the corresponding cluster as shown in the figure. [Click here](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) to learn how to access multiple clusters.
![Multicluster_screenshot](images/screenshot_multicluster.png)
# Repository
Essentially, a repository is a location where charts are gathered and can be shared. If you want to learn more about repositories, [click here](https://helm.sh/docs/topics/chart_repository/). You can find the repository in the home section, as depicted in the figure.
![Repository3](images/screenshot_repository3.png)
You can add the repository by clicking on 'Add Repository', as shown in the figure.
![Repository](images/screenshot_repository.png)
After completing that step, enter the following data: the repository name and its URL. You can also add the username and password, although this is optional.
![Repository2](images/screenshot_repository2.png)
Updating means refreshing your repository. You can update your repository as shown in the figure.
![Repository4](images/screenshot_repository4.png)
If you want to remove your repository from the Helm dashboard, click on the 'Remove' button as shown in the figure.
![Repository5](images/screenshot_repository5.png)
Use the filter option to find the desired chart quicker from the list of charts.
![Repository6](images/screenshot_repository6.png)
If you want to install a particular chart, simply hover the pointer over the chart name and an 'Install' button will appear, as shown in the figure.
![Repository7](images/screenshot_repository7.png)
# Installed Releases list
A release is an instance of your selected chart running on your Kubernetes Cluster. That means every time that you install a Helm chart there, it creates a new release or instance that coexists with other releases without conflict. You can filter releases based on namespaces or search for release names
![Releases](images/screenshot_release.png)
The squares represent k8s resources installed by the release. Hover over each square to view a tooltip with details. Yellow indicates "pending," green signifies a healthy state, and red indicates an unhealthy state.
![Releases1](images/screenshot_release1.png)
It indicates the version of chart that corresponds to this release.
![Releases2](images/screenshot_release2.png)
A revision is linked to a release to track the number of updates/changes that release encounters.
![Releases3](images/screenshot_release3.png)
Namespaces are a way to organize clusters into virtual sub-clusters — they can be helpful when different teams or projects share a Kubernetes cluster. Any number of namespaces are supported within a cluster, each logically separated from others but with the ability to communicate with each other.
![Releases4](images/screenshot_release4.png)
Updated" refers to the amount of time that has passed since the last revision of the release. Whenever you install or upgrade the release, a new revision is created. You can think of it as the "age" of the latest revision.
![Releases5](images/screenshot_release5.png)
# Release details
This indicates the status of the deployed release, and 'Age' represents the amount of time that has passed since the creation of the revision until now.
![Detail](images/screenshot_release_detail.png)
You can use the Upgrade/Downgrade button to switch to different release versions, as shown in the figure.
![Detail1](images/screenshot_release_detail1.png)

View File

@@ -1,9 +1,24 @@
pull:
git pull
DATE ?= $(shell date +%FT%T%z)
VERSION ?= $(git describe --tags --always --dirty --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo "v0")
build:
go build -o bin/dashboard .
.PHONY: test
test: ; $(info $(M) start unit testing...) @
@go test $$(go list ./... | grep -v /mocks/) --race -v -short -coverpkg=./... -coverprofile=profile.cov
@echo "\n*****************************"
@echo "** TOTAL COVERAGE: $$(go tool cover -func profile.cov | grep total | grep -Eo '[0-9]+\.[0-9]+')% **"
@echo "*****************************\n"
.PHONY: pull
pull: ; $(info $(M) Pulling source...) @
@git pull
debug:
DEBUG=1 ./bin/dashboard
.PHONY: build
build: $(BIN) ; $(info $(M) Building executable...) @ ## Build program binary
go build \
-ldflags '-X main.version=$(VERSION) -X main.buildDate=$(DATE)' \
-o bin/dashboard .
.PHONY: debug
debug: ; $(info $(M) Running dashboard in debug mode...) @
@DEBUG=1 ./bin/dashboard

121
README.md
View File

@@ -1,30 +1,49 @@
# <img src="pkg/dashboard/static/logo.png" height=30 style="height: 2rem"> Helm Dashboard
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="pkg/dashboard/static/logo-header-inverted.svg">
<source media="(prefers-color-scheme: light)" srcset="pkg/dashboard/static/logo-header.svg#gh-light-mode-only">
<img alt="Helm Dashboard" src="pkg/dashboard/static/logo-header.svg#gh-light-mode-only">
</picture>
</p>
A simplified way of working with Helm.
<p align="center">A simplified way of working with Helm.</p>
<kbd>[<img src="screenshot.png" style="width: 100%; border: 1px solid silver;" border="1" alt="Screenshot">](screenshot.png)</kbd>
## What it Does?
![GitHub contributors](https://img.shields.io/github/contributors/komodorio/helm-dashboard) [![GitHub issues](https://img.shields.io/github/issues-raw/komodorio/helm-dashboard)](https://github.com/komodorio/helm-dashboard/issues) ![GitHub stars](https://img.shields.io/github/stars/komodorio/helm-dashboard?style=social) ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/komodorio/helm-dashboard) ![GitHub pull requests](https://img.shields.io/github/issues-pr/komodorio/helm-dashboard) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/komodorio/helm-dashboard)](https://github.com/komodorio/helm-dashboard/releases) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/komodorio/helm-dashboard) [![GitHub license](https://img.shields.io/github/license/komodorio/helm-dashboard)](https://github.com/komodorio/helm-dashboard) [![codecov](https://codecov.io/gh/komodorio/helm-dashboard/branch/main/graph/badge.svg?token=PXPSNVHI2T)](https://codecov.io/gh/komodorio/helm-dashboard)
The _Helm Dashboard_ plugin offers a UI-driven way to view the installed Helm charts, see their revision history and
<kbd>[<img src="images/screenshot.png" style="width: 100%; border: 1px solid silver;" border="1" alt="Screenshot">](images/screenshot.png)</kbd>
## Description
_Helm Dashboard_ is an **open-source project** which offers a UI-driven way to view the installed Helm charts, see their revision history and
corresponding k8s resources. Also, you can perform simple actions like roll back to a revision or upgrade to newer
version.
This project is part of [Komodor's](https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash-gh) vision of
helping Kubernetes users to navigate and troubleshoot their clusters.
helping Kubernetes users to navigate and troubleshoot their clusters, the project is **NOT** an official project by the [helm team](https://helm.sh/).
Some of the key capabilities of the tool:
Key capabilities of the tool:
- See all installed charts and their revision history
- 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
- Can be used locally, or installed into Kubernetes cluster
- Does not require Helm or Kubectl installed
## Installing
## Setup
To install it, simply run Helm command:
### Standalone Binary
Since version 1.0, the recommended install method is to just use standalone binary. It does not require Helm or kubectl to be installed.
Download the appropriate [release package](https://github.com/komodorio/helm-dashboard/releases) for your platform, unpack it and just run `dashboard` binary from it.
### Using Helm plugin manager
To install dashboard as Helm plugin, simply run Helm command:
```shell
helm plugin install https://github.com/komodorio/helm-dashboard.git
@@ -42,9 +61,7 @@ To uninstall, run:
helm plugin uninstall dashboard
```
## Running
To use the plugin, your machine needs to have working `helm` and also `kubectl` commands.
To use the plugin, your machine needs to have working `helm` and also `kubectl` commands. Helm version 3.4.0+ is required.
After installing, start the UI by running:
@@ -55,46 +72,104 @@ helm dashboard
The command above will launch the local Web server and will open the UI in new browser tab. The command will hang
waiting for you to terminate it in command-line or web UI.
You can see the list of available command-line flags by running `helm dashboard --help`.
By default, the web server is only available locally. You can change that by specifying `HD_BIND` environment variable
to the desired value. For example, `0.0.0.0` would bind to all IPv4 addresses or `[::0]` would be all IPv6 addresses.
This can also be specified using flag `--bind <host>`, for example `--bind=0.0.0.0` or `--bind 0.0.0.0`.
If your port 8080 is busy, you can specify a different port to use via `HD_PORT` environment variable.
> Precedence order: flag `--bind=<host>` > env `HD_BIND=<host>` > default value `localhost`
If you don't want browser tab to automatically open, set `HD_NOBROWSER=1` in your environment variables.
If your port 8080 is busy, you can specify a different port to use via `--port <number>` command-line flag.
If you want to increase the logging verbosity and see all the debug info, set `DEBUG=1` environment variable.
If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line. You can specify multiple namespaces, separated by commas.
## Scanner Integrations
If you don't want browser tab to automatically open, add `--no-browser` flag in your command line.
If you want to increase the logging verbosity and see all the debug info, use the `--verbose` flag.
> Disclaimer: For the sake of improving the project quality, there is user analytics collected by the tool. You can disable this collecting with `--no-analytics` option. The collection is done via DataDog RUM and Heap Analytics. Only the anonymous data is collected, no sensitive information is used.
### Deploying Helm Dashboard on Kubernetes
The official helm chart is [available here](https://github.com/komodorio/helm-charts/blob/master/charts/helm-dashboard)
## Selected Features
### Support for Local Charts
Local Helm chart is a directory with specially named files and a `Chart.yaml` file, which you can install via Helm, without the need to publish the chart into Helm repository. Chart developers might want to experiment with the chart locally, before uploading into public repository. Also, a proprietary application might only use non-published chart as an approach to deploy the software.
For all the above use-cases, you may use Helm Dashboard UI, specifying the location of your local chart folders via special `--local-chart` command-line parameter. The parameter might be specified multiple times, for example:
```shell
helm-dashboard --local-chart=/opt/charts/my-private-app --local-chart=/home/dev/sources/app/chart
```
When _valid_ local chart sources specified, the repository list would contain a surrogate `[local]` entry, with those charts listed inside. All the chart operations are normal: installing, reconfiguring and upgrading.
![](images/screenshot_local_charts.png)
### Execute Helm tests
For all the release(s) (installed helm charts), you can execute helm tests for that release. For the tests to execute successfully, you need to have existing tests for that helm chart.
You can execute `helm test` for the specific release as below:
![](images/screenshot_run_test.png)
The result of executed `helm test` for the release will be displayed as below:
![](images/screenshot_run_test_result.png)
### Scanner Integrations
Upon startup, Helm Dashboard detects the presence of [Trivy](https://github.com/aquasecurity/trivy)
and [Checkov](https://github.com/bridgecrewio/checkov) scanners. When available, these scanners are offered on k8s
resources page, as well as install/upgrade preview page.
You can request scanning of the specific k8s resource in your cluster:
![](screenshot_scan_resource.png)
![](images/screenshot_scan_resource.png)
If you want to validate the k8s manifest prior to installing/reconfiguring a Helm chart, look for "Scan for Problems"
button at the bottom of the dialog:
![](screenshot_scan_manifest.png)
![](images/screenshot_scan_manifest.png)
## Support Channels
We have two main channels for supporting the Helm Dashboard
users: [Slack community](https://komodorkommunity.slack.com/archives/C044U1B0265) for general conversations
users: [Slack community](https://komodorkommunity.slack.com) for general conversations
and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
## Contributing
Kindly read our [Contributing Guide](CONTRIBUTING.md) to learn and understand about our development process, how to propose bug fixes and improvements, and how to build and test your changes to Helm Dashboard. <br>
## Contributors
<a href="https://github.com/komodorio/helm-dashboard/graphs/contributors">
<img src="https://contrib.rocks/image?repo=komodorio/helm-dashboard" />
</a>
## Local Dev Testing
Prerequisites: `helm` and `kubectl` binaries installed and operational.
Prerequisites, binaries installed and operational:
- [Go](https://go.dev/doc/install)
There is a need to build binary for plugin to function, run:
### Linux
```shell
go build -o bin/dashboard .
```
You can just run the `bin/dashboard` binary directly, it will just work.
### Windows
```bat
go build -o bin\dashboard.exe .
```
You can just run the `dashboard` or `dashboard.exe` binary directly, it will just work.
To install, checkout the source code and run from source dir:

5
artifacthub-repo.yml Normal file
View File

@@ -0,0 +1,5 @@
# Artifact Hub repository metadata file
repositoryID: 9ed6d12d-b3d5-4efd-836e-3ac9fa9dd3d1
owners:
- name: komodor-bot
email: komi@komodor.io

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,9 @@
apiVersion: v2
type: application
name: helm-dashboard
description: A GUI Dashboard for Helm by Komodor
icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/main/pkg/dashboard/static/logo.svg"
version: 0.1.9
appVersion: "1.3.2"

View File

@@ -0,0 +1,85 @@
# Helm Dashboard
## TL;DR;
```bash
helm repo add komodorio https://helm-charts.komodor.io
helm repo update
helm upgrade --install helm-dashboard komodorio/helm-dashboard
```
## Introduction
This chart bootstraps a Helm Dashboard deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
While installed inside cluster, Helm Dashboard will run some additional backgroud actions, for example, will automatically update Helm repositories. To enable that behavior locally, set `HD_CLUSTER_MODE` env variable.
## Prerequisites
- Kubernetes 1.16+
## Installing the Chart
To install the chart with the release name `helm-dashboard`:
```bash
helm install helm-dashboard .
```
The command deploys Helm Dashboard on the Kubernetes cluster in the default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation.
> **Tip**: List all releases using `helm list`
## Uninstalling the Chart
To uninstall/delete the `helm-dashboard` deployment:
```bash
helm uninstall helm-dashboard
```
The command removes all the Kubernetes components associated with the chart and deletes the release.
## Adding Authentication
The task of authentication and user control is out of scope for Helm Dashboard. Luckily, there are third-party solutions which are dedicated to provide that functionality.
For instance, you can place authentication proxy in front of Helm Dashboard, like this one: https://github.com/oauth2-proxy/oauth2-proxy
## Parameters
The following table lists the configurable parameters of the chart and their default values.
| Parameter | Description | Default |
| ------------------------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------------ |
| `image.repository` | Image registry/name | `docker.io/komodorio/helm-dashboard` |
| `image.tag` | Image tag | |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `replicaCount` | Number of dashboard Pods to run | `1` |
| `dashboard.allowWriteActions` | Enables write actions. Allow modifying, deleting and creating charts and kubernetes resources. | `true` |
| `resources.requests.cpu` | CPU resource requests | `200m` |
| `resources.limits.cpu` | CPU resource limits | `1` |
| `resources.requests.memory` | Memory resource requests | `256Mi` |
| `resources.limits.memory` | Memory resource limits | `1Gi` |
| `service.type ` | Kubernetes service type | `ClusterIP` |
| `service.port ` | Kubernetes service port | `8080` |
| `serviceAccount.create` | Creates a service account | `true` |
| `serviceAccount.name` | Optional name for the service account | `{RELEASE_FULLNAME}` |
| `nodeSelector` | Node labels for pod assignment | |
| `affinity` | Affinity settings for pod assignment | |
| `tolerations` | Tolerations for pod assignment | |
| `dashboard.persistence.enabled` | Enable helm data persistene using PVC | `true` |
| `dashboard.persistence.accessModes` | Persistent Volume access modes | `["ReadWriteOnce"]` |
| `dashboard.persistence.storageClass` | Persistent Volume storage class | `""` |
| `dashboard.persistence.size` | Persistent Volume size | `100M` |
| `dashboard.persistence.hostPath` | Set path in case you want to use local host path volumes (not recommended in production) | `""`
| `updateStrategy.type` | Set up update strategy for helm-dashboard installation. | `RollingUpdate` |
| `extraArgs` | Set the arguments to be supplied to the helm-dashboard binary | `[--no-browser, --bind=0.0.0.0]`
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`.
```bash
helm upgrade --install helm-dashboard komodorio/helm-dashboard --set dashboard.allowWriteActions=true --set service.port=9090
```
> **Tip**: You can use the default [values.yaml](values.yaml)

View File

@@ -0,0 +1,16 @@
Thank you for installing Helm Dashboard.
Helm Dashboard can be accessed:
* Within your cluster, at the following DNS name at port {{ .Values.service.port }}:
{{ template "helm-dashboard.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local
* From outside the cluster, run these commands in the same shell:
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "helm-dashboard.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
Visit our repo at:
https://github.com/komodorio/helm-dashboard

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "helm-dashboard.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "helm-dashboard.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "helm-dashboard.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "helm-dashboard.labels" -}}
helm.sh/chart: {{ include "helm-dashboard.chart" . }}
{{ include "helm-dashboard.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "helm-dashboard.selectorLabels" -}}
app.kubernetes.io/name: {{ include "helm-dashboard.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "helm-dashboard.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "helm-dashboard.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,93 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "helm-dashboard.fullname" . }}
labels:
{{- include "helm-dashboard.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "helm-dashboard.selectorLabels" . | nindent 6 }}
strategy: {{- toYaml .Values.updateStrategy | nindent 4 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "helm-dashboard.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "helm-dashboard.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
command:
- /bin/helm-dashboard
args:
{{- with .Values.extraArgs }}
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: HELM_CACHE_HOME
value: /opt/dashboard/helm/cache
- name: HELM_CONFIG_HOME
value: /opt/dashboard/helm/config
- name: HELM_DATA_HOME
value: /opt/dashboard/helm/data
- name: DEBUG
value: {{- ternary " 1" "" .Values.debug }}
{{- if .Values.dashboard.namespace }}
- name: HELM_NAMESPACE
value: {{ .Values.dashboard.namespace }}
{{end}}
ports:
- name: http
containerPort: 8080
protocol: TCP
livenessProbe:
httpGet:
path: /status
port: http
readinessProbe:
httpGet:
path: /status
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /opt/dashboard/helm
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: data
{{- if .Values.dashboard.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "helm-dashboard.fullname" . }}
{{- else }}
emptyDir: { }
{{- end }}

View File

@@ -0,0 +1,61 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "helm-dashboard.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "helm-dashboard.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,56 @@
{{- if .Values.dashboard.persistence.enabled -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "helm-dashboard.fullname" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "helm-dashboard.labels" . | nindent 4 }}
{{- with .Values.dashboard.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.dashboard.persistence.hostPath }}
storageClassName: ""
{{- else }}
{{- if kindIs "string" .Values.dashboard.persistence.storageClass }}
storageClassName: "{{ .Values.dashboard.persistence.storageClass }}"
{{- end }}
{{- end }}
accessModes:
{{- if not (empty .Values.dashboard.persistence.accessModes) }}
{{- range .Values.dashboard.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
{{- end }}
resources:
requests:
storage: {{ .Values.dashboard.persistence.size | quote }}
{{- end }}
---
{{- if and .Values.dashboard.persistence.enabled .Values.dashboard.persistence.hostPath -}}
apiVersion: v1
kind: PersistentVolume
metadata:
name: {{ include "helm-dashboard.fullname" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "helm-dashboard.labels" . | nindent 4 }}
{{- with .Values.dashboard.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- if not (empty .Values.dashboard.persistence.accessModes) }}
{{- range .Values.dashboard.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
{{- end }}
capacity:
storage: {{ .Values.dashboard.persistence.size | quote }}
hostPath:
path: {{ .Values.dashboard.persistence.hostPath | quote }}
{{- end -}}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "helm-dashboard.fullname" . }}
labels:
{{- include "helm-dashboard.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "helm-dashboard.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,39 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "helm-dashboard.serviceAccountName" . }}
labels:
{{- include "helm-dashboard.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: {{ include "helm-dashboard.serviceAccountName" . }}
rules:
- apiGroups: ["*"]
resources: ["*"]
{{- if .Values.dashboard.allowWriteActions }}
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
{{- else }}
verbs: ["get", "list", "watch"]
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "helm-dashboard.serviceAccountName" . }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "helm-dashboard.serviceAccountName" . }}
subjects:
- kind: ServiceAccount
namespace: {{ .Release.Namespace }}
name: {{ include "helm-dashboard.serviceAccountName" . }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "helm-dashboard.fullname" . }}-test-connection"
labels:
{{- include "helm-dashboard.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['--timeout=5', '{{ include "helm-dashboard.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,121 @@
replicaCount: 1
# Flag for setting environment to debug mode
debug: false
image:
repository: komodorio/helm-dashboard
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 1
memory: 1Gi
dashboard:
allowWriteActions: true
# default namespace for Helm operations
namespace: ""
persistence:
enabled: true
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
storageClass: null
## Helm Dashboard Persistent Volume access modes
## Must match those of existing PV or dynamic provisioner
## Ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
##
accessModes:
- ReadWriteOnce
## Helm Dashboard Persistent Volume labels
##
labels: {}
## Helm Dashboard Persistent Volume annotations
##
annotations: {}
## Set path in case you want to use local host path volumes (not recommended in production)
##
hostPath: ""
## Helm Dashboard data Persistent Volume size
##
size: 100M
## @param.updateStrategy.type Set up update strategy for helm-dashboard installation.
## Set to Recreate if you use persistent volume that cannot be mounted by more than one pods to make sure the pods is destroyed first.
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
## Example:
## updateStrategy:
## type: RollingUpdate
## rollingUpdate:
## maxSurge: 25%
## maxUnavailable: 25%
##
updateStrategy:
type: RollingUpdate
podAnnotations: {}
podSecurityContext: {}
securityContext: {}
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
nodeSelector: {}
extraArgs:
- --no-browser
- --bind=0.0.0.0
tolerations: []
affinity: {}

15
ci/bump-versions.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash -e
WORKING_DIRECTORY="$PWD"
[ -z "$HELM_CHARTS_SOURCE" ] && HELM_CHARTS_SOURCE="$WORKING_DIRECTORY/charts/helm-dashboard"
[ -z "$APP_VERSION" ] && {
APP_VERSION=$(cat ${HELM_CHARTS_SOURCE}/Chart.yaml | grep 'appVersion:' | awk -F'"' '{print $2}')
}
sed -i -e "s/appVersion.*/appVersion: \"${APP_VERSION}\" /g" ${HELM_CHARTS_SOURCE}/Chart.yaml
sed -i -e "s/version.*/version: \"${APP_VERSION}\" /g" plugin.yaml
CURRENT_VERSION=$(cat ${HELM_CHARTS_SOURCE}/Chart.yaml | grep 'version:' | awk '{print $2}')
NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g')
sed -i -e "s/${CURRENT_VERSION}/${NEW_VERSION}/g" ${HELM_CHARTS_SOURCE}/Chart.yaml

211
go.mod
View File

@@ -1,124 +1,177 @@
module github.com/komodorio/helm-dashboard
go 1.18
go 1.20
require (
github.com/aquasecurity/trivy v0.32.1
github.com/gin-gonic/gin v1.8.1
github.com/Masterminds/semver/v3 v3.2.1
github.com/eko/gocache/v3 v3.1.2
github.com/gin-gonic/gin v1.9.1
github.com/hashicorp/go-version v1.6.0
github.com/hexops/gotextdiff v1.0.3
github.com/sirupsen/logrus v1.9.0
github.com/toqueteos/webbrowser v1.2.0
github.com/jessevdk/go-flags v1.5.0
github.com/joomcode/errorx v1.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/pkg/errors v0.9.1
github.com/rogpeppe/go-internal v1.10.0
github.com/sirupsen/logrus v1.9.2
github.com/stretchr/testify v1.8.4
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.9.4
k8s.io/apimachinery v0.25.0-alpha.2
gotest.tools/v3 v3.4.0
helm.sh/helm/v3 v3.12.0
k8s.io/api v0.27.2
k8s.io/apimachinery v0.27.2
k8s.io/cli-runtime v0.27.2
k8s.io/client-go v0.27.2
k8s.io/kubectl v0.27.2
k8s.io/utils v0.0.0-20230505201702-9f6742963106
)
require (
github.com/CycloneDX/cyclonedx-go v0.6.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
github.com/Sirupsen/logrus v1.0.6 // indirect
github.com/aquasecurity/go-dep-parser v0.0.0-20220928105313-d3a51fe400e4 // indirect
github.com/aquasecurity/table v1.8.0 // indirect
github.com/aquasecurity/trivy-db v0.0.0-20220627104749-930461748b63 // indirect
github.com/aquasecurity/trivy-kubernetes v0.3.1-0.20220823151349-b90b48958b91 // indirect
github.com/caarlos0/env/v6 v6.10.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Masterminds/squirrel v1.5.3 // indirect
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/containerd/containerd v1.7.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/libnetwork v0.5.6 // indirect
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/cli v20.10.21+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v20.10.24+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/camelcase v1.0.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-gorp/gorp/v3 v3.0.5 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-containerregistry v0.11.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/liamg/tml v0.6.0 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/owenrumney/go-sarif/v2 v2.1.2 // indirect
github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a // indirect
github.com/pelletier/go-toml/v2 v2.0.3 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
github.com/pegasus-kv/thrift v0.13.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/samber/lo v1.27.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spdx/tools-golang v0.3.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rubenv/sql-migrate v1.3.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.5.0 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.4.0 // indirect
github.com/stretchr/testify v1.8.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/vishvananda/netlink v1.1.0 // indirect
github.com/vishvananda/netns v0.0.0-20220913150850-18c4f4234207 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xlab/treeprint v1.1.0 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.opentelemetry.io/otel v1.14.0 // indirect
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/exp v0.0.0-20221110155412-d0897a79cd37 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.4.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.8.0 // indirect
golang.org/x/text v0.9.0 // 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/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.25.0-alpha.2 // indirect
k8s.io/cli-runtime v0.24.4 // indirect
k8s.io/client-go v0.25.0-alpha.2 // indirect
k8s.io/klog/v2 v2.70.0 // indirect
k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/kustomize/api v0.11.4 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
k8s.io/apiextensions-apiserver v0.27.1 // indirect
k8s.io/apiserver v0.27.1 // indirect
k8s.io/component-base v0.27.2 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
oras.land/oras-go v1.2.2 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.13.2 // indirect
sigs.k8s.io/kustomize/kyaml v0.14.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

885
go.sum

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
images/screenshot_run_test.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

135
main.go
View File

@@ -1,50 +1,145 @@
package main
import (
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard"
log "github.com/sirupsen/logrus"
"github.com/toqueteos/webbrowser"
"context"
"fmt"
"github.com/joomcode/errorx"
"os"
"os/signal"
"strings"
"syscall"
"github.com/gin-gonic/gin"
"github.com/jessevdk/go-flags"
"github.com/komodorio/helm-dashboard/pkg/dashboard"
"github.com/pkg/browser"
log "github.com/sirupsen/logrus"
)
var (
version = "dev"
version = "0.0.0"
commit = "none"
date = "unknown"
)
func main() {
setupLogging()
type options struct {
Version bool `long:"version" description:"Show tool version"`
Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"`
NoBrowser bool `short:"b" long:"no-browser" description:"Do not attempt to open Web browser upon start"`
NoTracking bool `long:"no-analytics" description:"Disable user analytics (Heap, DataDog etc.)"`
BindHost string `long:"bind" description:"Host binding to start server (default: localhost)"` // default should be printed but not assigned as the precedence: flag > env > default
Port uint `short:"p" long:"port" description:"Port to start server on" default:"8080"`
Namespace string `short:"n" long:"namespace" description:"Namespace for HELM operations"`
Devel bool `long:"devel" description:"Include development versions of charts"`
LocalChart []string `long:"local-chart" description:"Specify location of local chart to include into UI"`
}
// TODO: proper command-line parsing
if len(os.Args) > 1 { // dirty thing to allow --help to work
os.Exit(0)
func main() {
err := os.Setenv("HD_VERSION", version) // for anyone willing to access it
if err != nil {
fmt.Println("Failed to remember app version because of error: " + err.Error())
}
address, webServerDone := dashboard.StartServer(version)
opts := parseFlags()
if opts.BindHost == "" {
host := os.Getenv("HD_BIND")
if host == "" {
host = "localhost"
}
opts.BindHost = host
}
if os.Getenv("HD_NOBROWSER") == "" {
opts.Verbose = opts.Verbose || os.Getenv("DEBUG") != ""
setupLogging(opts.Verbose)
server := dashboard.Server{
Version: version,
Namespaces: strings.Split(opts.Namespace, ","),
Address: fmt.Sprintf("%s:%d", opts.BindHost, opts.Port),
Debug: opts.Verbose,
NoTracking: opts.NoTracking,
Devel: opts.Devel,
LocalCharts: opts.LocalChart,
}
ctx, cancel := context.WithCancel(context.Background())
osSignal := make(chan os.Signal, 1)
signal.Notify(osSignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
oscall := <-osSignal
log.Warnf("Stopping on signal: %s\n", oscall)
cancel()
}()
address, webServerDone, err := server.StartServer(ctx, cancel)
if err != nil {
if errorx.IsOfType(err, errorx.InitializationFailed) {
log.Debugf("Full error: %+v", err)
log.Errorf("No Kubernetes cluster connection possible. Make sure you have valid kubeconfig file or run dashboard from inside cluster. See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/")
os.Exit(1)
} else {
log.Fatalf("Failed to start Helm Dashboard: %+v", err)
}
}
if !opts.NoTracking {
log.Infof("User analytics is collected to improve the quality, disable it with --no-analytics")
}
if opts.NoBrowser {
log.Infof("Access web UI at: %s", address)
} else {
log.Infof("Opening web UI: %s", address)
err := webbrowser.Open(address)
err := browser.OpenURL(address)
if err != nil {
log.Warnf("Failed to open Web browser for URL: %s", err)
}
} else {
log.Infof("Access web UI at: %s", address)
}
<-webServerDone
log.Infof("Done.")
}
func setupLogging() {
if os.Getenv("DEBUG") == "" {
log.SetLevel(log.InfoLevel)
gin.SetMode(gin.ReleaseMode)
} else {
func parseFlags() options {
ns := os.Getenv("HELM_NAMESPACE")
if ns == "default" { // it's how Helm passes to plugin the empty NS, we have to reset it back
ns = ""
}
opts := options{Namespace: ns}
args, err := flags.Parse(&opts)
if err != nil {
if e, ok := err.(*flags.Error); ok {
if e.Type == flags.ErrHelp {
os.Exit(0)
}
}
// we rely on default behavior to print the problem inside `flags` library
os.Exit(1)
}
if opts.Version {
fmt.Println(version)
os.Exit(0)
}
if len(args) > 0 {
fmt.Println("The program does not take arguments, see --help for usage")
os.Exit(1)
}
return opts
}
func setupLogging(verbose bool) {
if verbose {
log.SetLevel(log.DebugLevel)
gin.SetMode(gin.DebugMode)
log.Debugf("Debug logging is enabled")
} else {
log.SetLevel(log.InfoLevel)
gin.SetMode(gin.ReleaseMode)
}
log.Infof("Helm Dashboard by Komodor, version %s (%s @ %s)", version, commit, date)
}

View File

@@ -1,12 +1,13 @@
package dashboard
import (
"context"
"embed"
"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"
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
log "github.com/sirupsen/logrus"
"html"
"net/http"
"os"
"path"
@@ -16,7 +17,9 @@ import (
var staticFS embed.FS
func noCache(c *gin.Context) {
c.Header("Cache-Control", "no-cache")
if c.GetHeader("Cache-Control") == "" { // default policy is not to cache
c.Header("Cache-Control", "no-cache")
}
c.Next()
}
@@ -25,28 +28,41 @@ func errorHandler(c *gin.Context) {
errs := ""
for _, err := range c.Errors {
log.Debugf("Error: %s", err)
log.Debugf("Error: %+v", err)
errs += err.Error() + "\n"
}
if errs != "" {
c.String(http.StatusInternalServerError, errs)
c.String(http.StatusInternalServerError, html.EscapeString(errs))
}
}
func contextSetter(data *subproc.DataLayer) gin.HandlerFunc {
func contextSetter(data *objects.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]
ctxName := ""
if ctx, ok := c.Request.Header["X-Kubecontext"]; ok {
ctxName = ctx[0]
if err := data.SetContext(ctxName); err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
}
app, err := data.AppForCtx(ctxName)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
c.Set(handlers.APP, app)
c.Next()
}
}
func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, version string) *gin.Engine {
func NewRouter(abortWeb context.CancelFunc, data *objects.DataLayer, debug bool) *gin.Engine {
var api *gin.Engine
if os.Getenv("DEBUG") == "" {
if debug {
api = gin.New()
api.Use(gin.Recovery())
} else {
@@ -58,53 +74,93 @@ func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, version stri
api.Use(errorHandler)
configureStatic(api)
configureRoutes(abortWeb, data, api, version)
configureRoutes(abortWeb, data, api)
return api
}
func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *gin.Engine, version string) {
func configureRoutes(abortWeb context.CancelFunc, data *objects.DataLayer, api *gin.Engine) {
// server shutdown handler
api.DELETE("/", func(c *gin.Context) {
abortWeb <- struct{}{}
abortWeb()
c.Status(http.StatusAccepted)
})
api.GET("/status", func(c *gin.Context) {
c.String(http.StatusOK, version)
c.Header("X-Application-Name", "Helm Dashboard by Komodor.io") // to identify ourselves by ourselves
c.IndentedJSON(http.StatusOK, data.GetStatus())
})
api.GET("/api/cache", func(c *gin.Context) { // TODO: included into OpenAPI or not?
c.IndentedJSON(http.StatusOK, data.Cache)
})
api.DELETE("/api/cache", func(c *gin.Context) { // TODO: included into OpenAPI or not?
err := data.Cache.Clear()
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
c.Status(http.StatusAccepted)
})
api.POST("/diff", func(c *gin.Context) { // TODO: included into OpenAPI or not?
a := c.PostForm("a")
b := c.PostForm("b")
out := handlers.GetDiff(a, b, "current.yaml", "upgraded.yaml")
c.Header("Content-Type", "text/plain")
c.String(http.StatusOK, out)
})
api.GET("/api-docs", func(c *gin.Context) { // https://github.com/OAI/OpenAPI-Specification/search?q=api-docs
c.Redirect(http.StatusFound, "static/api-docs.html")
})
configureHelms(api.Group("/api/helm"), data)
configureKubectls(api.Group("/api/kube"), data)
configureKubectls(api.Group("/api/k8s"), data)
configureScanners(api.Group("/api/scanners"), data)
}
func configureHelms(api *gin.RouterGroup, data *subproc.DataLayer) {
h := handlers.HelmHandler{Data: data}
func configureHelms(api *gin.RouterGroup, data *objects.DataLayer) {
h := handlers.HelmHandler{
Contexted: &handlers.Contexted{
Data: data,
},
}
api.GET("/charts", h.GetCharts)
api.DELETE("/charts", h.Uninstall)
rels := api.Group("/releases")
rels.GET("", h.GetReleases)
rels.POST(":ns", h.Install)
rels.POST(":ns/:name", h.Upgrade)
rels.DELETE(":ns/:name", h.Uninstall)
rels.GET(":ns/:name/history", h.History)
rels.GET(":ns/:name/:section", h.GetInfoSection)
rels.GET(":ns/:name/resources", h.Resources)
rels.POST(":ns/:name/rollback", h.Rollback)
rels.POST(":ns/:name/test", h.RunTests)
api.GET("/charts/history", h.History)
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.POST("/repo/update", h.RepoUpdate)
api.GET("/repo/values", h.RepoValues)
repos := api.Group("/repositories")
repos.GET("", h.RepoList)
repos.POST("", h.RepoAdd)
repos.GET("/:name", h.RepoCharts)
repos.POST("/:name", h.RepoUpdate)
repos.DELETE("/:name", h.RepoDelete)
repos.GET("/latestver", h.RepoLatestVer) // TODO: use /versions in client insted and remove this?
repos.GET("/versions", h.RepoVersions)
repos.GET("/values", h.RepoValues)
}
func configureKubectls(api *gin.RouterGroup, data *subproc.DataLayer) {
h := handlers.KubeHandler{Data: data}
func configureKubectls(api *gin.RouterGroup, data *objects.DataLayer) {
h := handlers.KubeHandler{
Contexted: &handlers.Contexted{
Data: data,
},
}
api.GET("/contexts", h.GetContexts)
api.GET("/resources/:kind", h.GetResourceInfo)
api.GET("/describe/:kind", h.Describe)
api.GET("/:kind/get", h.GetResourceInfo)
api.GET("/:kind/describe", h.Describe)
api.GET("/:kind/list", h.GetNameSpaces)
}
func configureStatic(api *gin.Engine) {
@@ -137,9 +193,13 @@ func configureStatic(api *gin.Engine) {
}
}
func configureScanners(api *gin.RouterGroup, data *subproc.DataLayer) {
h := handlers.ScannersHandler{Data: data}
func configureScanners(api *gin.RouterGroup, data *objects.DataLayer) {
h := handlers.ScannersHandler{
Contexted: &handlers.Contexted{
Data: data,
},
}
api.GET("", h.List)
api.POST("/manifests", h.ScanDraftManifest)
api.POST("/manifests", h.ScanManifest)
api.GET("/resource/:kind", h.ScanResource)
}

412
pkg/dashboard/api_test.go Normal file
View File

@@ -0,0 +1,412 @@
package dashboard
import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard/handlers"
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
log "github.com/sirupsen/logrus"
"gotest.tools/v3/assert"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
)
var inMemStorage *storage.Storage
var repoFile string
func TestMain(m *testing.M) { // fixture to set logging level via env variable
if os.Getenv("DEBUG") != "" {
log.SetLevel(log.DebugLevel)
log.Debugf("Set logging level")
}
inMemStorage = storage.Init(driver.NewMemory())
d, err := os.MkdirTemp("", "helm")
if err != nil {
panic(err)
}
repoFile = filepath.Join(d, "repositories.yaml")
m.Run()
inMemStorage = nil
repoFile = ""
}
func GetTestGinContext(w *httptest.ResponseRecorder) *gin.Context {
gin.SetMode(gin.TestMode)
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
Header: make(http.Header),
}
return ctx
}
func TestNoCacheMiddleware(t *testing.T) {
w := httptest.NewRecorder()
con := GetTestGinContext(w)
noCache(con)
assert.Equal(t, w.Header().Get("Cache-Control"), "no-cache")
}
func TestEnableCacheControl(t *testing.T) {
w := httptest.NewRecorder()
con := GetTestGinContext(w)
// Sets deafault policy to `no-cache`
noCache(con)
h := handlers.HelmHandler{
Contexted: &handlers.Contexted{
Data: &objects.DataLayer{},
},
}
h.EnableClientCache(con)
assert.Equal(t, w.Header().Get("Cache-Control"), "max-age=43200")
}
func TestConfigureStatic(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
// Create an API Engine
api := gin.Default()
// Configure static routes
configureStatic(api)
// Start the server
api.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
}
func TestConfigureRoutes(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/status", nil)
if err != nil {
t.Fatal(err)
}
// Create a API Engine
api := gin.Default()
// Required arguements for route configuration
abortWeb := func() {}
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
if err != nil {
t.Fatal(err)
}
// Configure routes to API engine
configureRoutes(abortWeb, data, api)
// Start the server
api.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
}
func TestContextSetter(t *testing.T) {
w := httptest.NewRecorder()
con := GetTestGinContext(w)
// Required arguements
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
if err != nil {
t.Fatal(err)
}
// Set the context
ctxHandler := contextSetter(data)
ctxHandler(con)
appName, exists := con.Get("app")
if !exists {
t.Fatal("Value app doesn't exist in context")
}
tmp := handlers.Contexted{Data: data}
assert.Equal(t, appName, tmp.GetApp(con))
}
func TestNewRouter(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/status", nil)
if err != nil {
t.Fatal(err)
}
// Required arguemnets
abortWeb := func() {}
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
if err != nil {
t.Fatal(err)
}
// Create a new router with the function
newRouter := NewRouter(abortWeb, data, false)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
}
func TestConfigureScanners(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/api/scanners", nil)
if err != nil {
t.Fatal(err)
}
// Required arguemnets
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
if err != nil {
t.Fatal(err)
}
apiEngine := gin.Default()
configureScanners(apiEngine.Group("/api/scanners"), data)
apiEngine.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
}
func TestConfigureKubectls(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/api/kube/contexts", nil)
if err != nil {
t.Fatal(err)
}
// Required arguemnets
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
if err != nil {
t.Fatal(err)
}
apiEngine := gin.Default()
// Required middleware for kubectl api configuration
apiEngine.Use(contextSetter(data))
configureKubectls(apiEngine.Group("/api/kube"), data)
apiEngine.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
}
func TestE2E(t *testing.T) {
// Initialize data layer
data, err := objects.NewDataLayer([]string{""}, "0.0.0-test", getFakeHelmConfig, false)
assert.NilError(t, err)
// Create a new router with the function
abortWeb := func() {}
newRouter := NewRouter(abortWeb, data, false)
// initially, we don't have any releases
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/api/helm/releases", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
assert.Equal(t, w.Body.String(), "[]")
// initially, we don't have any repositories
w = httptest.NewRecorder()
req, err = http.NewRequest("GET", "/api/helm/repositories", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
assert.Equal(t, w.Body.String(), "[]")
// then we add one repository
w = httptest.NewRecorder()
form := url.Values{}
form.Add("name", "komodorio")
form.Add("url", "https://helm-charts.komodor.io")
req, err = http.NewRequest("POST", "/api/helm/repositories", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusNoContent)
assert.Equal(t, w.Body.String(), "")
// now, we have one repo
w = httptest.NewRecorder()
req, err = http.NewRequest("GET", "/api/helm/repositories", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
assert.Equal(t, w.Body.String(), `[
{
"name": "komodorio",
"url": "https://helm-charts.komodor.io"
}
]`)
// what's the latest version of that chart
w = httptest.NewRecorder()
req, err = http.NewRequest("GET", "/api/helm/repositories/latestver?name=helm-dashboard", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
// generate template for potential release
w = httptest.NewRecorder()
form = url.Values{}
form.Add("preview", "true")
form.Add("name", "release1")
form.Add("chart", "komodorio/helm-dashboard")
req, err = http.NewRequest("POST", "/api/helm/releases/test1", strings.NewReader(form.Encode()))
assert.NilError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
// install the release
w = httptest.NewRecorder()
form = url.Values{}
form.Add("name", "release1")
form.Add("chart", "komodorio/helm-dashboard")
req, err = http.NewRequest("POST", "/api/helm/releases/test1", strings.NewReader(form.Encode()))
assert.NilError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusAccepted)
// get list of releases
w = httptest.NewRecorder()
req, err = http.NewRequest("GET", "/api/helm/releases", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
t.Logf("Release: %s", w.Body.String())
//assert.Equal(t, w.Body.String(), "[]")
// upgrade/reconfigure release
w = httptest.NewRecorder()
form = url.Values{}
form.Add("chart", "komodorio/helm-dashboard")
form.Add("values", "dashboard:\n allowWriteActions: true\n")
req, err = http.NewRequest("POST", "/api/helm/releases/test1/release1", strings.NewReader(form.Encode()))
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
assert.Equal(t, w.Code, http.StatusAccepted)
// get history of revisions for release
w = httptest.NewRecorder()
req, err = http.NewRequest("GET", "/api/helm/releases/test1/release1/history", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
t.Logf("Revs: %s", w.Body.String())
//assert.Equal(t, w.Body.String(), "[]")
// get values for revision
w = httptest.NewRecorder()
req, err = http.NewRequest("GET", "/api/helm/releases/test1/release1/values?revision=2&userDefined=true", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
//assert.Equal(t, w.Body.String(), "[]")
// rollback
w = httptest.NewRecorder()
form = url.Values{}
form.Add("revision", "1")
req, err = http.NewRequest("POST", "/api/helm/releases/test1/release1/rollback", strings.NewReader(form.Encode()))
assert.NilError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusAccepted)
// get manifest diff for release
w = httptest.NewRecorder()
req, err = http.NewRequest("GET", "/api/helm/releases/test1/release1/manifests?revision=1&revisionDiff=2", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
//assert.Equal(t, w.Body.String(), "[]")
// delete repo
w = httptest.NewRecorder()
req, err = http.NewRequest("DELETE", "/api/helm/repositories/komodorio", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusNoContent)
// reconfigure release without repo connection
w = httptest.NewRecorder()
form = url.Values{}
form.Add("chart", "komodorio/helm-dashboard")
form.Add("values", "dashboard:\n allowWriteActions: false\n")
req, err = http.NewRequest("POST", "/api/helm/releases/test1/release1", strings.NewReader(form.Encode()))
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
assert.Equal(t, w.Code, http.StatusAccepted)
t.Logf("Upgraded: %s", w.Body.String())
// uninstall
w = httptest.NewRecorder()
req, err = http.NewRequest("DELETE", "/api/helm/releases/test1/release1", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusAccepted)
// check we don't have releases again
w = httptest.NewRecorder()
req, err = http.NewRequest("GET", "/api/helm/releases", nil)
assert.NilError(t, err)
newRouter.ServeHTTP(w, req)
assert.Equal(t, w.Code, http.StatusOK)
assert.Equal(t, w.Body.String(), "[]")
}
func getFakeHelmConfig(settings *cli.EnvSettings, _ string) (*action.Configuration, error) {
settings.RepositoryConfig = repoFile
registryClient, err := registry.NewClient()
if err != nil {
return nil, err
}
return &action.Configuration{
Releases: inMemStorage,
KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: os.Stderr}},
Capabilities: chartutil.DefaultCapabilities,
RegistryClient: registryClient,
Log: log.Infof,
}, nil
}

View File

@@ -0,0 +1,31 @@
package handlers
import (
"github.com/gin-gonic/gin"
"github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
"net/http"
)
const APP = "app"
type Contexted struct {
Data *objects.DataLayer
}
func (h *Contexted) GetApp(c *gin.Context) *objects.Application {
var app *objects.Application
if a, ok := c.Get(APP); ok {
app = a.(*objects.Application)
} else {
err := errorx.IllegalState.New("No application context found")
_ = c.AbortWithError(http.StatusBadRequest, err)
return nil
}
return app
}
func (h *Contexted) EnableClientCache(c *gin.Context) {
c.Header("Cache-Control", "max-age=43200")
}

View File

@@ -1,37 +1,78 @@
package handlers
import (
"encoding/json"
"errors"
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"net/http"
"sort"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
"github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
"github.com/rogpeppe/go-internal/semver"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/repo"
helmtime "helm.sh/helm/v3/pkg/time"
"k8s.io/utils/strings/slices"
)
type HelmHandler struct {
Data *subproc.DataLayer
*Contexted
}
func (h *HelmHandler) GetCharts(c *gin.Context) {
res, err := h.Data.ListInstalled()
func (h *HelmHandler) getRelease(c *gin.Context) *objects.Release {
app := h.GetApp(c)
if app == nil {
return nil
}
rel, err := app.Releases.ByName(c.Param("ns"), c.Param("name"))
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return nil
}
return rel
}
func (h *HelmHandler) GetReleases(c *gin.Context) {
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
rels, err := app.Releases.List()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
res := []*ReleaseElement{}
for _, r := range rels {
res = append(res, HReleaseToJSON(r.Orig))
}
c.IndentedJSON(http.StatusOK, res)
}
// TODO: helm show chart komodorio/k8s-watcher to get the icon URL
func (h *HelmHandler) Uninstall(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
rel := h.getRelease(c)
if rel == nil {
return // error state is set inside
}
err = h.Data.ChartUninstall(qp.Namespace, qp.Name)
err := rel.Uninstall()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -40,13 +81,18 @@ func (h *HelmHandler) Uninstall(c *gin.Context) {
}
func (h *HelmHandler) Rollback(c *gin.Context) {
qp, err := utils.GetQueryProps(c, true)
rel := h.getRelease(c)
if rel == nil {
return // error state is set inside
}
revn, err := strconv.Atoi(c.PostForm("revision"))
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
err = h.Data.Revert(qp.Namespace, qp.Name, qp.Revision)
err = rel.Rollback(revn)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -55,73 +101,224 @@ func (h *HelmHandler) Rollback(c *gin.Context) {
}
func (h *HelmHandler) History(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
rel := h.getRelease(c)
if rel == nil {
return // error state is set inside
}
res, err := h.Data.ChartHistory(qp.Namespace, qp.Name)
revs, err := rel.History()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
res := []*HistoryElement{}
for _, r := range revs {
res = append(res, HReleaseToHistElem(r.Orig))
}
sort.Slice(res, func(i, j int) bool {
return res[i].Revision < res[j].Revision
})
c.IndentedJSON(http.StatusOK, res)
}
func (h *HelmHandler) Resources(c *gin.Context) {
qp, err := utils.GetQueryProps(c, true)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
// can't enable the client cache because resource list changes with time
rel := h.getRelease(c)
if rel == nil {
return // error state is set inside
}
res, err := h.Data.RevisionManifestsParsed(qp.Namespace, qp.Name, qp.Revision)
res, err := objects.ParseManifests(rel.Orig.Manifest)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
res = append(res, &v1.Carp{
TypeMeta: metav1.TypeMeta{Kind: "ManifestParseError"},
ObjectMeta: metav1.ObjectMeta{
Name: err.Error(),
},
Spec: v1.CarpSpec{},
Status: v1.CarpStatus{
Phase: "BrokenManifest",
Message: err.Error(),
},
})
//_ = c.AbortWithError(http.StatusInternalServerError, err)
//return
}
if c.Query("health") != "" { // we need to query k8s for health status
app := h.GetApp(c)
if app == nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
for _, obj := range res {
ns := obj.Namespace
if ns == "" {
ns = c.Param("ns")
}
info, err := app.K8s.GetResourceInfo(obj.Kind, ns, obj.Name)
if err != nil {
log.Warnf("Failed to get resource info for %s %s/%s: %+v", obj.Name, ns, obj.Name, err)
info = &v1.Carp{}
}
obj.Status = *EnhanceStatus(info, err)
}
}
c.IndentedJSON(http.StatusOK, res)
}
func (h *HelmHandler) RepoSearch(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
func (h *HelmHandler) RepoVersions(c *gin.Context) {
qp, err := utils.GetQueryProps(c)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
res, err := h.Data.ChartRepoVersions(qp.Name)
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
repos, err := app.Repositories.Containing(qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
res := []*RepoChartElement{}
for _, r := range repos {
res = append(res, &RepoChartElement{
Name: r.Name,
Version: r.Version,
AppVersion: r.AppVersion,
Description: r.Description,
Repository: r.Annotations[objects.AnnRepo],
URLs: r.URLs,
})
}
c.IndentedJSON(http.StatusOK, res)
}
func (h *HelmHandler) RepoLatestVer(c *gin.Context) {
qp, err := utils.GetQueryProps(c)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
rep, err := app.Repositories.Containing(qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
res := []*RepoChartElement{}
for _, r := range rep {
res = append(res, &RepoChartElement{
Name: r.Name,
Version: r.Version,
AppVersion: r.AppVersion,
Description: r.Description,
Repository: r.Annotations[objects.AnnRepo],
URLs: r.URLs,
})
}
sort.Slice(res, func(i, j int) bool {
return semver.Compare(res[i].Version, res[j].Version) > 0
})
if len(res) > 0 {
c.IndentedJSON(http.StatusOK, res[:1])
} else {
// caching it to avoid too many requests
found, err := h.Data.Cache.String("chart-artifacthub-query/"+qp.Name, nil, func() (string, error) {
return h.repoFromArtifactHub(qp.Name)
})
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
if found == "" {
c.Status(http.StatusNoContent)
} else {
c.Header("Content-Type", "application/json")
c.String(http.StatusOK, found)
}
}
}
func (h *HelmHandler) RepoCharts(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
res, err := h.Data.ChartRepoCharts(qp.Name)
rep, err := app.Repositories.Get(c.Param("name"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, res)
}
func (h *HelmHandler) RepoUpdate(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
charts, err := rep.Charts()
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
err = h.Data.ChartRepoUpdate(qp.Name)
installed, err := app.Releases.List()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
enrichRepoChartsWithInstalled(charts, installed)
sort.Slice(charts, func(i, j int) bool {
return charts[i].Name < charts[j].Name
})
c.IndentedJSON(http.StatusOK, charts)
}
func enrichRepoChartsWithInstalled(charts []*repo.ChartVersion, installed []*objects.Release) {
for _, rchart := range charts {
for _, rel := range installed {
if rchart.Metadata.Name == rel.Orig.Chart.Name() {
log.Debugf("Matched") // TODO: restore implementation
// TODO: there can be more than one
//rchart.InstalledNamespace = rel.Orig.Namespace
//rchart.InstalledName = rel.Orig.Name
}
}
}
}
func (h *HelmHandler) RepoUpdate(c *gin.Context) {
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
rep, err := app.Repositories.Get(c.Param("name"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
err = rep.Update()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -130,47 +327,147 @@ func (h *HelmHandler) RepoUpdate(c *gin.Context) {
}
func (h *HelmHandler) Install(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
values := map[string]interface{}{}
err := yaml.Unmarshal([]byte(c.PostForm("values")), &values)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
_ = c.AbortWithError(http.StatusInternalServerError, 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)
repoChart, err := h.checkLocalRepo(c.PostForm("chart"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
justTemplate := c.PostForm("preview") == "true"
ns := c.Param("ns")
if ns == "[empty]" {
ns = ""
}
rel, err := app.Releases.Install(ns, c.PostForm("name"), repoChart, c.PostForm("version"), justTemplate, values)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
if justTemplate {
manifests := ""
if isInitial {
manifests, err = h.Data.RevisionManifests(qp.Namespace, qp.Name, 0, false)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
out = subproc.GetDiff(strings.TrimSpace(manifests), out, "current.yaml", "upgraded.yaml")
c.IndentedJSON(http.StatusOK, rel)
} else {
c.Header("Content-Type", "application/json")
c.IndentedJSON(http.StatusAccepted, rel)
}
c.String(http.StatusAccepted, out)
}
func (h *HelmHandler) GetInfoSection(c *gin.Context) {
qp, err := utils.GetQueryProps(c, true)
func (h *HelmHandler) checkLocalRepo(repoChart string) (string, error) {
if strings.HasPrefix(repoChart, "file://") {
repoChart = repoChart[len("file://"):]
if !slices.Contains(h.Data.LocalCharts, repoChart) {
return "", fmt.Errorf("chart path is not present in local charts: %s", repoChart)
}
}
return repoChart, nil
}
func (h *HelmHandler) Upgrade(c *gin.Context) {
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
existing, err := app.Releases.ByName(c.Param("ns"), c.Param("name"))
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
flag := c.Query("flag") == "true"
rDiff := c.Query("revisionDiff")
res, err := handleGetSection(h.Data, c.Param("section"), rDiff, qp, flag)
values := map[string]interface{}{}
err = yaml.Unmarshal([]byte(c.PostForm("values")), &values)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
repoChart, err := h.checkLocalRepo(c.PostForm("chart"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
justTemplate := c.PostForm("preview") == "true"
rel, err := existing.Upgrade(repoChart, c.PostForm("version"), justTemplate, values)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
if justTemplate {
c.IndentedJSON(http.StatusOK, rel)
} else {
c.IndentedJSON(http.StatusAccepted, rel)
}
}
func (h *HelmHandler) RunTests(c *gin.Context) {
rel := h.getRelease(c)
if rel == nil {
return // error state is set inside
}
out, err := rel.RunTests()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, out)
}
func (h *HelmHandler) GetInfoSection(c *gin.Context) {
if c.Query("revision") != "" { // don't cache if latest is requested
h.EnableClientCache(c)
}
rel := h.getRelease(c)
if rel == nil {
return // error state is set inside
}
revn, err := strconv.Atoi(c.Query("revision"))
if c.Query("revision") != "" && err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
rev, err := rel.GetRev(revn)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
var revDiff *objects.Release
revS := c.Query("revisionDiff")
if revS != "" {
revN, err := strconv.Atoi(revS)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
revDiff, err = rel.GetRev(revN)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
flag := c.Query("userDefined") == "true"
res, err := h.handleGetSection(rev, c.Param("section"), revDiff, flag)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -179,25 +476,59 @@ func (h *HelmHandler) GetInfoSection(c *gin.Context) {
}
func (h *HelmHandler) RepoValues(c *gin.Context) {
out, err := h.Data.ShowValues(c.Query("chart"), c.Query("version"))
h.EnableClientCache(c)
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
repoChart, err := h.checkLocalRepo(c.Query("chart"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
out, err := app.Repositories.GetChartValues(repoChart, c.Query("version"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.String(http.StatusOK, out)
}
func (h *HelmHandler) RepoList(c *gin.Context) {
out, err := h.Data.ChartRepoList()
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
repos, err := app.Repositories.List()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
out := []RepositoryElement{}
for _, r := range repos {
out = append(out, RepositoryElement{
Name: r.Name(),
URL: r.URL(),
})
}
c.IndentedJSON(http.StatusOK, out)
}
func (h *HelmHandler) RepoAdd(c *gin.Context) {
_, err := h.Data.ChartRepoAdd(c.PostForm("name"), c.PostForm("url"))
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
// TODO: more repo options to accept
err := app.Repositories.Add(c.PostForm("name"), c.PostForm("url"), c.PostForm("username"), c.PostForm("password"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -206,13 +537,12 @@ func (h *HelmHandler) RepoAdd(c *gin.Context) {
}
func (h *HelmHandler) RepoDelete(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
_, err = h.Data.ChartRepoDelete(qp.Name)
err := app.Repositories.Delete(c.Param("name"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -220,11 +550,30 @@ func (h *HelmHandler) RepoDelete(c *gin.Context) {
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,
"values": data.RevisionValues,
"notes": data.RevisionNotes,
func (h *HelmHandler) handleGetSection(rel *objects.Release, section string, rDiff *objects.Release, flag bool) (string, error) {
sections := map[string]objects.SectionFn{
"manifests": func(qp *release.Release, b bool) (string, error) { return qp.Manifest, nil },
"notes": func(qp *release.Release, b bool) (string, error) { return qp.Info.Notes, nil },
"values": func(qp *release.Release, b bool) (string, error) {
allVals := qp.Config
if !b {
merged, err := chartutil.CoalesceValues(qp.Chart, qp.Config)
if err != nil {
return "", errorx.Decorate(err, "failed to merge chart vals with user defined")
}
allVals = merged
}
if len(allVals) > 0 {
data, err := yaml.Marshal(allVals)
if err != nil {
return "", errorx.Decorate(err, "failed to serialize values into YAML")
}
return string(data), nil
}
return "", nil
},
}
functor, found := sections[section]
@@ -232,27 +581,192 @@ func handleGetSection(data *subproc.DataLayer, section string, rDiff string, qp
return "", errors.New("unsupported section: " + section)
}
if rDiff != "" {
cRevDiff, err := strconv.Atoi(rDiff)
if err != nil {
return "", err
}
if rDiff != nil {
ext := ".yaml"
if section == "notes" {
ext = ".txt"
}
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)
res, err := RevisionDiff(functor, ext, rDiff.Orig, rel.Orig, flag)
if err != nil {
return "", err
}
return res, nil
}
res, err := functor(rel.Orig, flag)
if err != nil {
return "", errorx.Decorate(err, "failed to get section info")
}
return res, nil
}
func (h *HelmHandler) repoFromArtifactHub(name string) (string, error) {
results, err := objects.QueryArtifactHub(name)
if err != nil {
log.Warnf("Failed to query ArtifactHub: %s", err)
return "", nil // swallowing the error to not annoy users
}
if len(results) == 0 {
return "", nil
}
sort.SliceStable(results, func(i, j int) bool {
ri, rj := results[i], results[j]
// we prefer official repos
if ri.Repository.Official && !rj.Repository.Official {
return true
}
// more popular
if ri.Stars != rj.Stars {
return ri.Stars > rj.Stars
}
// or from verified publishers
if ri.Repository.VerifiedPublisher && !rj.Repository.VerifiedPublisher {
return true
}
// or with more recent app version
c := semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion)
if c != 0 {
return c > 0
}
// shorter repo name is usually closer to officials
return len(ri.Repository.Name) < len(rj.Repository.Name)
})
r := results[0]
buf, err := json.Marshal([]*RepoChartElement{{
Name: r.Name,
Version: r.Version,
AppVersion: r.AppVersion,
Description: r.Description,
Repository: r.Repository.Name,
URLs: []string{r.Repository.Url},
IsSuggestedRepo: true,
}})
if err != nil {
return "", err
}
return string(buf), nil
}
type RepoChartElement struct { // TODO: do we need it at all? there is existing repo.ChartVersion in Helm
Name string `json:"name"`
Version string `json:"version"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
InstalledNamespace string `json:"installed_namespace"`
InstalledName string `json:"installed_name"`
Repository string `json:"repository"`
URLs []string `json:"urls"`
IsSuggestedRepo bool `json:"isSuggestedRepo"`
}
func HReleaseToJSON(o *release.Release) *ReleaseElement {
return &ReleaseElement{
Name: o.Name,
Namespace: o.Namespace,
Revision: strconv.Itoa(o.Version),
Updated: o.Info.LastDeployed,
Status: o.Info.Status,
Chart: fmt.Sprintf("%s-%s", o.Chart.Name(), o.Chart.Metadata.Version),
ChartName: o.Chart.Name(),
ChartVersion: o.Chart.Metadata.Version,
AppVersion: o.Chart.AppVersion(),
Icon: o.Chart.Metadata.Icon,
Description: o.Chart.Metadata.Description,
}
}
type ReleaseElement struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Revision string `json:"revision"`
Updated helmtime.Time `json:"updated"`
Status release.Status `json:"status"`
Chart string `json:"chart"`
ChartName string `json:"chartName"`
ChartVersion string `json:"chartVersion"`
AppVersion string `json:"app_version"`
Icon string `json:"icon"`
Description string `json:"description"`
}
type RepositoryElement struct {
Name string `json:"name"`
URL string `json:"url"`
}
type HistoryElement struct {
Revision int `json:"revision"`
Updated helmtime.Time `json:"updated"`
Status release.Status `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
ChartName string `json:"chart_name"` // custom addition on top of Helm
ChartVer string `json:"chart_ver"` // custom addition on top of Helm
HasTests bool `json:"has_tests"`
}
func HReleaseToHistElem(o *release.Release) *HistoryElement {
return &HistoryElement{
Revision: o.Version,
Updated: o.Info.LastDeployed,
Status: o.Info.Status,
Chart: fmt.Sprintf("%s-%s", o.Chart.Name(), o.Chart.Metadata.Version),
AppVersion: o.Chart.AppVersion(),
Description: o.Info.Description,
ChartName: o.Chart.Name(),
ChartVer: o.Chart.Metadata.Version,
HasTests: releaseHasTests(o),
}
}
func RevisionDiff(functor objects.SectionFn, ext string, revision1 *release.Release, revision2 *release.Release, flag bool) (string, error) {
if revision1 == nil || revision2 == nil {
log.Debugf("One of revisions is nil: %v %v", revision1, revision2)
return "", nil
}
manifest1, err := functor(revision1, flag)
if err != nil {
return "", err
}
manifest2, err := functor(revision2, flag)
if err != nil {
return "", err
}
diff := GetDiff(manifest1, manifest2, strconv.Itoa(revision1.Version)+ext, strconv.Itoa(revision2.Version)+ext)
return diff, nil
}
func GetDiff(text1 string, text2 string, name1 string, name2 string) string {
edits := myers.ComputeEdits(span.URIFromPath(""), text1, text2)
unified := gotextdiff.ToUnified(name1, name2, text1, edits)
diff := fmt.Sprint(unified)
log.Debugf("The diff is: %s", diff)
return diff
}
func releaseHasTests(o *release.Release) bool {
for _, h := range o.Hooks {
for _, e := range h.Events {
if e == release.HookTest {
return true
}
}
}
return false
}

View File

@@ -2,18 +2,30 @@ package handlers
import (
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
"k8s.io/apimachinery/pkg/apis/meta/v1"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/errors"
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"k8s.io/utils/strings/slices"
"net/http"
)
const Unknown = "Unknown"
const Healthy = "Healthy"
const Unhealthy = "Unhealthy"
const Progressing = "Progressing"
type KubeHandler struct {
Data *subproc.DataLayer
*Contexted
}
func (h *KubeHandler) GetContexts(c *gin.Context) {
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
res, err := h.Data.ListContexts()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
@@ -23,45 +35,103 @@ func (h *KubeHandler) GetContexts(c *gin.Context) {
}
func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
qp, err := utils.GetQueryProps(c)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
res, err := h.Data.GetResource(qp.Namespace, &v12.Carp{
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
ObjectMeta: v1.ObjectMeta{Name: qp.Name},
})
if err != nil {
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
res, err := app.K8s.GetResourceInfo(c.Param("kind"), qp.Namespace, qp.Name)
if errors.IsNotFound(err) {
res = &v12.Carp{Status: v12.CarpStatus{Phase: "NotFound", Message: err.Error()}}
//_ = c.AbortWithError(http.StatusNotFound, err)
//return
} else if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
if res.Status.Phase == "Active" || res.Status.Phase == "Error" {
_ = res.Name + ""
} else if res.Status.Phase == "" && len(res.Status.Conditions) > 0 {
res.Status.Phase = v12.CarpPhase(res.Status.Conditions[len(res.Status.Conditions)-1].Type)
res.Status.Message = res.Status.Conditions[len(res.Status.Conditions)-1].Message
res.Status.Reason = res.Status.Conditions[len(res.Status.Conditions)-1].Reason
if res.Status.Conditions[len(res.Status.Conditions)-1].Status == "False" {
res.Status.Phase = "Not" + res.Status.Phase
}
} else if res.Status.Phase == "" {
res.Status.Phase = "Exists"
}
EnhanceStatus(res, nil)
c.IndentedJSON(http.StatusOK, res)
}
func EnhanceStatus(res *v12.Carp, err error) *v12.CarpStatus {
s := res.Status
if s.Conditions == nil {
s.Conditions = []v12.CarpCondition{}
}
c := v12.CarpCondition{
Type: "hdHealth",
Status: Unknown,
Reason: s.Reason,
Message: s.Message,
}
// custom logic to provide most meaningful status for the resource
if err != nil {
c.Reason = "ErrorGettingStatus"
c.Message = err.Error()
} else if s.Phase == "Error" {
c.Status = Unhealthy
} else if slices.Contains([]string{"Available", "Active", "Established", "Bound", "Ready"}, string(s.Phase)) {
c.Status = Healthy
} else if s.Phase == "" && len(s.Conditions) > 0 {
for _, cond := range s.Conditions {
if cond.Type == "Progressing" { // https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
if cond.Status == "False" {
c.Status = Unhealthy
c.Reason = cond.Reason
c.Message = cond.Message
} else if cond.Reason != "NewReplicaSetAvailable" {
c.Status = Progressing
c.Reason = cond.Reason
c.Message = cond.Message
}
} else if cond.Type == "Available" && c.Status == Unknown {
if cond.Status == "False" {
c.Status = Unhealthy
} else {
c.Status = Healthy
}
c.Reason = cond.Reason
c.Message = cond.Message
}
}
} else if s.Phase == "Pending" {
c.Status = Progressing
c.Reason = string(s.Phase)
} else if s.Phase == "" {
c.Status = Healthy
c.Reason = "Exists"
} else {
log.Warnf("Unhandled status: %v", s)
c.Reason = string(s.Phase)
}
s.Conditions = append(s.Conditions, c)
return &s
}
func (h *KubeHandler) Describe(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
qp, err := utils.GetQueryProps(c)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
res, err := h.Data.DescribeResource(qp.Namespace, c.Param("kind"), qp.Name)
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
res, err := app.K8s.DescribeResource(c.Param("kind"), qp.Namespace, qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -69,3 +139,23 @@ func (h *KubeHandler) Describe(c *gin.Context) {
c.String(http.StatusOK, res)
}
func (h *KubeHandler) GetNameSpaces(c *gin.Context) {
if c.Param("kind") != "namespaces" {
_ = c.AbortWithError(http.StatusBadRequest, errorx.AssertionFailed.New("Only 'namespaces' kind is allowed for listing"))
return
}
app := h.GetApp(c)
if app == nil {
return // sets error inside
}
res, err := app.K8s.GetNameSpaces()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, res)
}

View File

@@ -8,34 +8,28 @@ import (
)
type ScannersHandler struct {
Data *subproc.DataLayer
*Contexted
}
func (h *ScannersHandler) List(c *gin.Context) {
var res []string
for _, scanner := range h.Data.Scanners {
res = append(res, scanner.Name())
type ScannerInfo struct {
SupportedResourceKinds []string
ManifestScannable bool
}
c.JSON(http.StatusOK, res)
res := map[string]ScannerInfo{}
for _, scanner := range h.Data.Scanners {
res[scanner.Name()] = ScannerInfo{
SupportedResourceKinds: scanner.SupportedResourceKinds(),
ManifestScannable: scanner.ManifestIsScannable(),
}
}
c.IndentedJSON(http.StatusOK, res)
}
func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) {
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
}
func (h *ScannersHandler) ScanManifest(c *gin.Context) {
reps := map[string]*subproc.ScanResults{}
for _, scanner := range h.Data.Scanners {
sr, err := scanner.ScanManifests(mnf)
sr, err := scanner.ScanManifests(c.PostForm("manifest"))
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -48,7 +42,7 @@ func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) {
}
func (h *ScannersHandler) ScanResource(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
qp, err := utils.GetQueryProps(c)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return

View File

@@ -0,0 +1,56 @@
package objects
import (
"github.com/joomcode/errorx"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
// Import to initialize client auth plugins.
// From https://github.com/kubernetes/client-go/issues/242
_ "k8s.io/client-go/plugin/pkg/client/auth"
)
type HelmConfigGetter = func(sett *cli.EnvSettings, ns string) (*action.Configuration, error)
type HelmNSConfigGetter = func(ns string) (*action.Configuration, error)
type Application struct {
Settings *cli.EnvSettings
HelmConfig HelmNSConfigGetter
K8s *K8s
Releases *Releases
Repositories *Repositories
}
func NewApplication(settings *cli.EnvSettings, helmConfig HelmNSConfigGetter, namespaces []string, devel bool) (*Application, error) {
hc, err := helmConfig(settings.Namespace())
if err != nil {
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
}
k8s, err := NewK8s(hc, namespaces)
if err != nil {
return nil, errorx.Decorate(err, "failed to get k8s client")
}
semVerConstraint, err := versionConstaint(devel)
if err != nil {
return nil, errorx.Decorate(err, "failed to create semantic version constraint")
}
return &Application{
HelmConfig: helmConfig,
K8s: k8s,
Releases: &Releases{
Namespaces: namespaces,
Settings: settings,
HelmConfig: helmConfig,
},
Repositories: &Repositories{
Settings: settings,
HelmConfig: hc,
versionConstraint: semVerConstraint,
},
}, nil
}

View File

@@ -0,0 +1,90 @@
package objects
import (
"encoding/json"
"fmt"
log "github.com/sirupsen/logrus"
"net/http"
neturl "net/url"
"os"
"sync"
)
var mxArtifactHub sync.Mutex
func QueryArtifactHub(chartName string) ([]*ArtifactHubResult, error) {
mxArtifactHub.Lock() // to avoid parallel request spike
defer mxArtifactHub.Unlock()
url := os.Getenv("HD_ARTIFACT_HUB_URL")
if url == "" {
url = "https://artifacthub.io/api/v1/packages/search"
}
p, err := neturl.Parse(url)
if err != nil {
return nil, err
}
p.RawQuery = "offset=0&limit=5&facets=false&kind=0&deprecated=false&sort=relevance&ts_query_web=" + neturl.QueryEscape(chartName)
req, err := http.NewRequest("GET", p.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Komodor Helm Dashboard/"+os.Getenv("HD_VERSION")) // TODO
log.Debugf("Making HTTP request: %v", req)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("failed to fetch %s : %s", p.String(), res.Status)
}
result := ArtifactHubResults{}
err = json.NewDecoder(res.Body).Decode(&result)
if err != nil {
return nil, err
}
return result.Packages, nil
}
type ArtifactHubResults struct {
Packages []*ArtifactHubResult `json:"packages"`
}
type ArtifactHubResult struct {
PackageId string `json:"package_id"`
Name string `json:"name"`
NormalizedName string `json:"normalized_name"`
LogoImageId string `json:"logo_image_id"`
Stars int `json:"stars"`
Description string `json:"description"`
Version string `json:"version"`
AppVersion string `json:"app_version"`
Deprecated bool `json:"deprecated"`
Signed bool `json:"signed"`
ProductionOrganizationsCount int `json:"production_organizations_count"`
Ts int `json:"ts"`
Repository ArtifactHubRepo `json:"repository"`
}
type ArtifactHubRepo struct {
Url string `json:"url"`
Kind int `json:"kind"`
Name string `json:"name"`
Official bool `json:"official"`
DisplayName string `json:"display_name"`
RepositoryId string `json:"repository_id"`
ScannerDisabled bool `json:"scanner_disabled"`
OrganizationName string `json:"organization_name"`
VerifiedPublisher bool `json:"verified_publisher"`
OrganizationDisplayName string `json:"organization_display_name"`
}

View File

@@ -0,0 +1,76 @@
package objects
import (
"context"
"errors"
"github.com/eko/gocache/v3/marshaler"
"github.com/eko/gocache/v3/store"
gocache "github.com/patrickmn/go-cache"
log "github.com/sirupsen/logrus"
"time"
)
type CacheKey = string
type Cache struct {
Marshaler *marshaler.Marshaler `json:"-"`
HitCount int
MissCount int
}
func NewCache() *Cache {
gocacheClient := gocache.New(60*time.Minute, 10*time.Minute)
gocacheStore := store.NewGoCache(gocacheClient)
// TODO: use tiered cache with some disk backend, allow configuring that static cache folder
// Initializes marshaler
marshal := marshaler.New(gocacheStore)
return &Cache{
Marshaler: marshal,
}
}
func (c *Cache) String(key CacheKey, tags []string, callback func() (string, error)) (string, error) {
if tags == nil {
tags = make([]string, 0)
}
tags = append(tags, key)
ctx := context.Background()
out := ""
_, err := c.Marshaler.Get(ctx, key, &out)
if err == nil {
log.Debugf("Using cached value for %s", key)
c.HitCount++
return out, nil
} else if !errors.Is(err, store.NotFound{}) {
return "", err
}
c.MissCount++
out, err = callback()
if err != nil {
return "", err
}
err = c.Marshaler.Set(ctx, key, out, store.WithTags(tags))
if err != nil {
return "", err
}
return out, nil
}
func (c *Cache) Invalidate(tags ...CacheKey) {
log.Debugf("Invalidating tags %v", tags)
err := c.Marshaler.Invalidate(context.Background(), store.WithInvalidateTags(tags))
if err != nil {
log.Warnf("Failed to invalidate tags %v: %s", tags, err)
}
}
func (c *Cache) Clear() error {
c.HitCount = 0
c.MissCount = 0
return c.Marshaler.Clear(context.Background())
}

View File

@@ -0,0 +1,238 @@
package objects
import (
"context"
"encoding/json"
"os"
"strings"
"sync"
"time"
"io"
"github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/release"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/tools/clientcmd"
//"sigs.k8s.io/yaml"
)
type DataLayer struct {
KubeContext string
Scanners []subproc.Scanner
StatusInfo *StatusInfo
Namespaces []string
Cache *Cache
ConfGen HelmConfigGetter
appPerContext map[string]*Application
appPerContextMx *sync.Mutex
devel bool
LocalCharts []string
}
type StatusInfo struct {
CurVer string
LatestVer string
Analytics bool
CacheHitRatio float64
ClusterMode bool
}
func NewDataLayer(ns []string, ver string, cg HelmConfigGetter, devel bool) (*DataLayer, error) {
if cg == nil {
return nil, errors.New("HelmConfigGetter can't be nil")
}
return &DataLayer{
Namespaces: ns,
Cache: NewCache(),
StatusInfo: &StatusInfo{
CurVer: ver,
Analytics: false,
},
ConfGen: cg,
appPerContext: map[string]*Application{},
appPerContextMx: new(sync.Mutex),
devel: devel,
}, nil
}
func (d *DataLayer) ListContexts() ([]KubeContext, error) {
res := []KubeContext{}
if d.StatusInfo.ClusterMode {
return res, nil
}
cfg, err := clientcmd.NewDefaultPathOptions().GetStartingConfig()
if err != nil {
return nil, errorx.Decorate(err, "failed to get kubectl config")
}
for name, ctx := range cfg.Contexts {
res = append(res, KubeContext{
IsCurrent: cfg.CurrentContext == name,
Name: name,
Cluster: ctx.Cluster,
AuthInfo: ctx.AuthInfo,
Namespace: ctx.Namespace,
})
}
return res, nil
}
func (d *DataLayer) GetStatus() *StatusInfo {
sum := float64(d.Cache.HitCount + d.Cache.MissCount)
if sum > 0 {
d.StatusInfo.CacheHitRatio = float64(d.Cache.HitCount) / sum
} else {
d.StatusInfo.CacheHitRatio = 0
}
return d.StatusInfo
}
type SectionFn = func(*release.Release, bool) (string, error)
func ParseManifests(out string) ([]*v1.Carp, error) {
dec := yaml.NewYAMLOrJSONDecoder(strings.NewReader(out), 4096)
res := make([]*v1.Carp, 0)
var tmp interface{}
for {
err := dec.Decode(&tmp)
if err == io.EOF {
break
}
if err != nil {
return res, errorx.Decorate(err, "failed to parse manifest document #%d", len(res)+1)
}
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
// we can juggle it
jsoned, err := json.Marshal(tmp)
if err != nil {
return res, err
}
var doc v1.Carp
err = json.Unmarshal(jsoned, &doc)
if err != nil {
return res, err
}
if doc.Kind == "" {
log.Warnf("Manifest piece is not k8s resource: %s", jsoned)
continue
}
res = append(res, &doc)
}
return res, nil
}
func (d *DataLayer) SetContext(ctx string) error {
if d.KubeContext != ctx {
err := d.Cache.Clear()
if err != nil {
return errors.Wrap(err, "failed to set context")
}
}
d.KubeContext = ctx
return nil
}
func (d *DataLayer) AppForCtx(ctx string) (*Application, error) {
d.appPerContextMx.Lock()
defer d.appPerContextMx.Unlock()
app, ok := d.appPerContext[ctx]
if !ok {
settings := cli.New()
settings.KubeContext = ctx
settings.SetNamespace(d.nsForCtx(ctx))
cfgGetter := func(ns string) (*action.Configuration, error) {
return d.ConfGen(settings, ns)
}
a, err := NewApplication(settings, cfgGetter, d.Namespaces, d.devel)
if err != nil {
return nil, errorx.Decorate(err, "Failed to create application for context '%s'", ctx)
}
a.Repositories.LocalCharts = d.LocalCharts
app = a
d.appPerContext[ctx] = app
}
return app, nil
}
func (d *DataLayer) nsForCtx(ctx string) string {
lst, err := d.ListContexts()
if err != nil {
log.Debugf("Failed to get contexts for NS lookup: %+v", err)
}
for _, c := range lst {
if c.Name == ctx {
return c.Namespace
}
}
log.Debugf("Strange: no context found for '%s'", ctx)
return ""
}
func (d *DataLayer) PeriodicTasks(ctx context.Context) {
// TODO: separate scanning setup for in-cluster?
if os.Getenv("HD_NO_AUTOUPDATE") == "" {
// auto-update repos
go d.loopUpdateRepos(ctx, 10*time.Minute) // TODO: parameterize interval?
}
// auto-scan
}
func (d *DataLayer) loopUpdateRepos(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
for {
app, err := d.AppForCtx("")
if err != nil {
log.Warnf("Failed to get app object while in background repo update: %v", err)
break // no point in retrying
} else {
repos, err := app.Repositories.List()
if err != nil {
log.Warnf("Failed to get list of repos while in background update: %v", err)
}
for _, repo := range repos {
err := repo.Update()
if err != nil {
log.Warnf("Failed to update repo %s: %v", repo.Name(), err)
}
}
}
select {
case <-ctx.Done():
ticker.Stop()
return
case <-ticker.C:
continue
}
}
log.Debugf("Update repo loop done.")
}

View File

@@ -0,0 +1,61 @@
package objects
import (
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
)
func TestNewDataLayer(t *testing.T) {
testCases := []struct {
name string
namespaces []string
version string
helmConfig HelmConfigGetter
devel bool
errorExpected bool
}{
{
name: "should return error when helm config is nil",
namespaces: []string{"namespace1", "namespace2"},
version: "1.0.0",
helmConfig: nil,
devel: false,
errorExpected: true,
},
{
name: "should return data layer when all parameters are correct",
namespaces: []string{
"namespace1",
"namespace2",
},
version: "1.0.0",
helmConfig: func(sett *cli.EnvSettings, ns string) (*action.Configuration, error) {
return &action.Configuration{}, nil
},
devel: false,
errorExpected: false,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
dl, err := NewDataLayer(tt.namespaces, tt.version, tt.helmConfig, tt.devel)
if tt.errorExpected {
assert.Error(t, err, "Expected error but got nil")
} else {
assert.Nil(t, err, "NewDataLayer returned an error: %v", err)
assert.NotNil(t, dl, "NewDataLayer returned nil")
assert.Equal(t, tt.namespaces, dl.Namespaces, "NewDataLayer returned incorrect namespaces: %v", dl.Namespaces)
assert.NotNil(t, dl.Cache, "NewDataLayer returned nil cache")
assert.Equal(t, tt.version, dl.StatusInfo.CurVer, "NewDataLayer returned incorrect version: %v", dl.StatusInfo.CurVer)
assert.False(t, dl.StatusInfo.Analytics, "NewDataLayer returned incorrect version: %v", dl.StatusInfo.CurVer)
assert.NotNil(t, dl.appPerContext, "NewDataLayer returned nil appPerContext")
assert.NotNil(t, dl.ConfGen, "NewDataLayer returned nil ConfGen")
}
})
}
}

View File

@@ -0,0 +1,206 @@
package objects
import (
"context"
"encoding/json"
"github.com/joomcode/errorx"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/kube"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
testapiv1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
describecmd "k8s.io/kubectl/pkg/cmd/describe"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/describe"
"k8s.io/utils/strings/slices"
"sort"
)
type KubeContext struct {
IsCurrent bool
Name string
Cluster string
AuthInfo string
Namespace string
}
// maps action.RESTClientGetter into genericclioptions.RESTClientGetter
type cfgProxyObject struct {
Impl action.RESTClientGetter
}
func (p *cfgProxyObject) ToRESTConfig() (*rest.Config, error) {
return p.Impl.ToRESTConfig()
}
func (p *cfgProxyObject) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
return p.Impl.ToDiscoveryClient()
}
func (p *cfgProxyObject) ToRESTMapper() (meta.RESTMapper, error) {
return p.Impl.ToRESTMapper()
}
func (p *cfgProxyObject) ToRawKubeConfigLoader() clientcmd.ClientConfig {
panic("Not implemented, stub")
}
type K8s struct {
Namespaces []string
Factory kube.Factory
RestClientGetter genericclioptions.RESTClientGetter
}
func NewK8s(helmConfig *action.Configuration, namespaces []string) (*K8s, error) {
factory := cmdutil.NewFactory(&cfgProxyObject{Impl: helmConfig.RESTClientGetter})
return &K8s{
Namespaces: namespaces,
Factory: factory,
RestClientGetter: factory,
}, nil
}
func (k *K8s) GetNameSpaces() (res *corev1.NamespaceList, err error) {
clientset, err := k.Factory.KubernetesClientSet()
if err != nil {
return nil, errors.Wrap(err, "failed to get KubernetesClientSet")
}
lst, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to get list of namespaces")
}
if !slices.Contains(k.Namespaces, "") {
filtered := []corev1.Namespace{}
for _, ns := range lst.Items {
if slices.Contains(k.Namespaces, ns.Name) {
filtered = append(filtered, ns)
}
}
lst.Items = filtered
}
return lst, nil
}
func (k *K8s) DescribeResource(kind string, ns string, name string) (string, error) {
log.Debugf("Describing resource: %s %s in %s", kind, name, ns)
streams, _, out, errout := genericclioptions.NewTestIOStreams()
o := &describecmd.DescribeOptions{
Describer: func(mapping *meta.RESTMapping) (describe.ResourceDescriber, error) {
return describe.DescriberFn(k.RestClientGetter, mapping)
},
FilenameOptions: &resource.FilenameOptions{},
DescriberSettings: &describe.DescriberSettings{
ShowEvents: true,
ChunkSize: cmdutil.DefaultChunkSize,
},
IOStreams: streams,
NewBuilder: k.Factory.NewBuilder,
}
o.Namespace = ns
o.BuilderArgs = []string{kind, name}
err := o.Run()
if err != nil {
return "", errorx.Decorate(err, "Failed to run describe command: %s", errout.String())
}
return out.String(), nil
}
func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.Object, error) {
builder := k.Factory.NewBuilder()
builder = builder.Unstructured().SingleResourceType()
if namespace != "" {
builder = builder.NamespaceParam(namespace)
} else {
builder = builder.DefaultNamespace()
}
resp := builder.Flatten().ResourceNames(kind, name).Do()
if resp.Err() != nil {
return nil, errorx.Decorate(resp.Err(), "failed to get k8s resource")
}
obj, err := resp.Object()
if err != nil {
return nil, errorx.Decorate(err, "failed to get k8s resulting object")
}
return &obj, nil
}
func (k *K8s) GetResourceInfo(kind string, namespace string, name string) (*testapiv1.Carp, error) {
// TODO: mutex to avoid a lot of requests?
obj, err := k.GetResource(kind, namespace, name)
if err != nil {
return nil, errorx.Decorate(err, "failed to get k8s object")
}
data, err := json.Marshal(obj)
if err != nil {
return nil, errorx.Decorate(err, "failed to marshal k8s object into JSON")
}
res := new(testapiv1.Carp)
err = json.Unmarshal(data, &res)
if err != nil {
return nil, errorx.Decorate(err, "failed to decode k8s object from JSON")
}
sort.Slice(res.Status.Conditions, func(i, j int) bool {
// some condition types always bubble up
if res.Status.Conditions[i].Type == "Available" {
return false
}
if res.Status.Conditions[j].Type == "Available" {
return true
}
t1 := res.Status.Conditions[i].LastTransitionTime
t2 := res.Status.Conditions[j].LastTransitionTime
return t1.Time.Before(t2.Time)
})
return res, nil
}
func (k *K8s) GetResourceYAML(kind string, namespace string, name string) (string, error) {
obj, err := k.GetResource(kind, namespace, name)
if err != nil {
return "", errorx.Decorate(err, "failed to get k8s object")
}
data, err := json.Marshal(obj)
if err != nil {
return "", errorx.Decorate(err, "failed to marshal k8s object into JSON")
}
res := map[string]interface{}{}
err = json.Unmarshal(data, &res)
if err != nil {
return "", errorx.Decorate(err, "failed to decode k8s object from JSON")
}
ydata, err := yaml.Marshal(res)
if err != nil {
return "", errorx.Decorate(err, "failed to marshal k8s object into JSON")
}
return string(ydata), nil
}

View File

@@ -0,0 +1,401 @@
package objects
import (
"bytes"
"fmt"
"gopkg.in/yaml.v3"
"io"
"os"
"path"
"sync"
"github.com/joomcode/errorx"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/release"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
)
type Releases struct {
Namespaces []string
HelmConfig HelmNSConfigGetter
Settings *cli.EnvSettings
mx sync.Mutex
}
func (a *Releases) List() ([]*Release, error) {
a.mx.Lock()
defer a.mx.Unlock()
releases := []*Release{}
for _, ns := range a.Namespaces {
log.Debugf("Listing releases in namespace: %s", ns)
hc, err := a.HelmConfig(ns)
if err != nil {
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
}
client := action.NewList(hc)
client.All = true
client.AllNamespaces = true
client.Limit = 0
client.SetStateMask() // required to apply proper filtering
rels, err := client.Run()
if err != nil {
return nil, errorx.Decorate(err, "failed to get list of releases")
}
for _, r := range rels {
releases = append(releases, &Release{HelmConfig: a.HelmConfig, Orig: r, Settings: a.Settings})
}
}
return releases, nil
}
func (a *Releases) ByName(namespace string, name string) (*Release, error) {
rels, err := a.List()
if err != nil {
return nil, errorx.Decorate(err, "failed to get list of releases")
}
for _, r := range rels {
if r.Orig.Namespace == namespace && r.Orig.Name == name {
return r, nil
}
}
return nil, errorx.DataUnavailable.New(fmt.Sprintf("release '%s' is not found in namespace '%s'", name, namespace))
}
func (a *Releases) Install(namespace string, name string, repoChart string, version string, justTemplate bool, values map[string]interface{}) (*release.Release, error) {
a.mx.Lock()
defer a.mx.Unlock()
if namespace == "" {
namespace = a.Settings.Namespace()
}
hc, err := a.HelmConfig(namespace)
if err != nil {
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
}
cmd := action.NewInstall(hc)
cmd.ReleaseName = name
cmd.CreateNamespace = true
cmd.Namespace = namespace
cmd.Version = version
cmd.DryRun = justTemplate
chrt, err := locateChart(cmd.ChartPathOptions, repoChart, a.Settings)
if err != nil {
return nil, err
}
res, err := cmd.Run(chrt, values)
if err != nil {
return nil, err
}
if !justTemplate {
log.Infof("Installed new release: %s/%s", namespace, name)
}
return res, nil
}
func locateChart(pathOpts action.ChartPathOptions, chart string, settings *cli.EnvSettings) (*chart.Chart, error) {
// from cmd/helm/install.go and cmd/helm/upgrade.go
cp, err := pathOpts.LocateChart(chart, settings)
if err != nil {
return nil, err
}
log.Debugf("Located chart %s: %s\n", chart, cp)
p := getter.All(settings)
// Check chart dependencies to make sure all are present in /charts
chartRequested, err := loader.Load(cp)
if err != nil {
return nil, err
}
if err := checkIfInstallable(chartRequested); err != nil {
return nil, err
}
if req := chartRequested.Metadata.Dependencies; req != nil {
// If CheckDependencies returns an error, we have unfulfilled dependencies.
// As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209
if err := action.CheckDependencies(chartRequested, req); err != nil {
err = errorx.Decorate(err, "An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies")
if true { // client.DependencyUpdate
man := &downloader.Manager{
Out: io.Discard,
ChartPath: cp,
Keyring: pathOpts.Keyring,
SkipUpdate: false,
Getters: p,
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug,
}
if err := man.Update(); err != nil {
return nil, err
}
// Reload the chart with the updated Chart.lock file.
if chartRequested, err = loader.Load(cp); err != nil {
return nil, errorx.Decorate(err, "failed reloading chart after repo update")
}
} else {
return nil, err
}
}
}
return chartRequested, nil
}
type Release struct {
Settings *cli.EnvSettings
HelmConfig HelmNSConfigGetter
Orig *release.Release
revisions []*Release
mx sync.Mutex
restoredChartPath string
}
func (r *Release) History() ([]*Release, error) {
r.mx.Lock()
defer r.mx.Unlock()
hc, err := r.HelmConfig(r.Orig.Namespace)
if err != nil {
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
}
client := action.NewHistory(hc)
revs, err := client.Run(r.Orig.Name)
if err != nil {
return nil, errorx.Decorate(err, "failed to get revisions of release")
}
r.revisions = []*Release{}
for _, rev := range revs {
r.revisions = append(r.revisions, &Release{HelmConfig: r.HelmConfig, Orig: rev, Settings: r.Settings})
}
return r.revisions, nil
}
func (r *Release) Uninstall() error {
r.mx.Lock()
defer r.mx.Unlock()
hc, err := r.HelmConfig(r.Orig.Namespace)
if err != nil {
return errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
}
client := action.NewUninstall(hc)
_, err = client.Run(r.Orig.Name)
if err != nil {
return errorx.Decorate(err, "failed to uninstall release")
}
return nil
}
func (r *Release) Rollback(toRevision int) error {
r.mx.Lock()
defer r.mx.Unlock()
hc, err := r.HelmConfig(r.Orig.Namespace)
if err != nil {
return errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
}
client := action.NewRollback(hc)
client.Version = toRevision
err = client.Run(r.Orig.Name)
if err != nil {
return errorx.Decorate(err, "failed to rollback the release")
}
log.Infof("Rolled back %s/%s to %d=>%d", r.Orig.Namespace, r.Orig.Name, r.Orig.Version, toRevision)
return nil
}
func (r *Release) RunTests() (string, error) {
r.mx.Lock()
defer r.mx.Unlock()
hc, err := r.HelmConfig(r.Orig.Namespace)
if err != nil {
return "", errorx.Decorate(err, "failed to get helm config for namespace '%s'", r.Orig.Namespace)
}
client := action.NewReleaseTesting(hc)
client.Namespace = r.Orig.Namespace
rel, err := client.Run(r.Orig.Name)
if err != nil {
return "", errorx.Decorate(err, "failed to execute 'helm test' for release '%s'", r.Orig.Name)
}
var buf bytes.Buffer
if err := client.GetPodLogs(&buf, rel); err != nil {
return "", errorx.Decorate(err, "failed to fetch logs for 'helm test' command")
}
return buf.String(), nil
}
func (r *Release) ParsedManifests() ([]*v1.Carp, error) {
carps, err := ParseManifests(r.Orig.Manifest)
if err != nil {
return nil, err
}
for _, carp := range carps {
if carp.Namespace == "" {
carp.Namespace = r.Orig.Namespace
}
}
return carps, err
}
func (r *Release) GetRev(revNo int) (*Release, error) {
if revNo == 0 {
revNo = r.Orig.Version
}
hist, err := r.History()
if err != nil {
return nil, errorx.Decorate(err, "failed to get history")
}
for _, rev := range hist {
if rev.Orig.Version == revNo {
return rev, nil
}
}
return nil, errorx.InternalError.New("No revision found for number %d", revNo)
}
func (r *Release) Upgrade(repoChart string, version string, justTemplate bool, values map[string]interface{}) (*release.Release, error) {
r.mx.Lock()
defer r.mx.Unlock()
// if repo chart is not passed, let's try to restore it from secret
if repoChart == "" {
var err error
repoChart, err = r.restoreChart()
if err != nil {
return nil, errorx.Decorate(err, "failed to revive chart for release")
}
}
ns := r.Settings.Namespace()
if r.Orig != nil {
ns = r.Orig.Namespace
}
hc, err := r.HelmConfig(ns)
if err != nil {
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", ns)
}
cmd := action.NewUpgrade(hc)
cmd.Namespace = r.Settings.Namespace()
cmd.Version = version
cmd.DryRun = justTemplate
cmd.ResetValues = true
chrt, err := locateChart(cmd.ChartPathOptions, repoChart, r.Settings)
if err != nil {
return nil, err
}
res, err := cmd.Run(r.Orig.Name, chrt, values)
if err != nil {
return nil, err
}
if !justTemplate {
log.Infof("Upgraded release: %s/%s#%d", res.Namespace, res.Name, res.Version)
}
return res, nil
}
func (r *Release) restoreChart() (string, error) {
if r.restoredChartPath != "" {
return r.restoredChartPath, nil
}
// we're unlikely to have the original chart, let's try the cheesy thing...
log.Infof("Attempting to restore the chart for %s", r.Orig.Name)
dir, err := os.MkdirTemp("", "khd-*")
if err != nil {
return "", errorx.Decorate(err, "failed to get temporary directory")
}
//restore Chart.yaml
cdata, err := yaml.Marshal(r.Orig.Chart.Metadata)
if err != nil {
return "", errorx.Decorate(err, "failed to restore Chart.yaml")
}
err = os.WriteFile(path.Join(dir, "Chart.yaml"), cdata, 0644)
if err != nil {
return "", errorx.Decorate(err, "failed to write file Chart.yaml")
}
//restore known values
vdata, err := yaml.Marshal(r.Orig.Chart.Values)
if err != nil {
return "", errorx.Decorate(err, "failed to restore values.yaml")
}
err = os.WriteFile(path.Join(dir, "values.yaml"), vdata, 0644)
if err != nil {
return "", errorx.Decorate(err, "failed to write file values.yaml")
}
// if possible, overwrite files with better alternatives
for _, f := range append(r.Orig.Chart.Raw, r.Orig.Chart.Templates...) {
fname := path.Join(dir, f.Name)
log.Debugf("Restoring file: %s", fname)
err := os.MkdirAll(path.Dir(fname), 0755)
if err != nil {
return "", errorx.Decorate(err, "failed to create directory for file: %s", fname)
}
err = os.WriteFile(fname, f.Data, 0644)
if err != nil {
return "", errorx.Decorate(err, "failed to write file to restore chart: %s", fname)
}
}
r.restoredChartPath = dir
return dir, nil
}
func checkIfInstallable(ch *chart.Chart) error {
switch ch.Metadata.Type {
case "", "application":
return nil
}
return errors.Errorf("%s charts are not installable", ch.Metadata.Type)
}

View File

@@ -0,0 +1,80 @@
package objects
import (
"sync"
"testing"
"gotest.tools/v3/assert"
"helm.sh/helm/v3/pkg/action"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
)
var (
fakeKubeClient *kubefake.PrintingKubeClient
fakeStorage *storage.Storage
)
func fakeHelmNSConfigGetter(ns string) (*action.Configuration, error) {
return &action.Configuration{
KubeClient: fakeKubeClient,
Releases: fakeStorage,
}, nil
}
func TestListReleases(t *testing.T) {
fakeStorage = storage.Init(driver.NewMemory())
err := fakeStorage.Create(&release.Release{
Name: "release1",
Info: &release.Info{
Status: release.StatusDeployed,
},
})
assert.NilError(t, err)
err = fakeStorage.Create(&release.Release{
Name: "release2",
Info: &release.Info{
Status: release.StatusDeployed,
},
})
assert.NilError(t, err)
err = fakeStorage.Create(&release.Release{
Name: "release3",
Info: &release.Info{
Status: release.StatusDeployed,
},
})
assert.NilError(t, err)
err = fakeStorage.Create(&release.Release{
Name: "release4",
Info: &release.Info{
Status: release.StatusDeployed,
},
})
assert.NilError(t, err)
err = fakeStorage.Create(&release.Release{
Name: "release5",
Info: &release.Info{
Status: release.StatusDeployed,
},
})
assert.NilError(t, err)
releases := &Releases{
Namespaces: []string{"testNamespace"},
HelmConfig: fakeHelmNSConfigGetter,
mx: sync.Mutex{},
}
res, err := releases.List()
assert.NilError(t, err)
assert.Equal(t, len(res), 5)
assert.Equal(t, res[0].Orig.Name, "release1")
assert.Equal(t, res[1].Orig.Name, "release2")
assert.Equal(t, res[2].Orig.Name, "release3")
assert.Equal(t, res[3].Orig.Name, "release4")
assert.Equal(t, res[4].Orig.Name, "release5")
}

View File

@@ -0,0 +1,431 @@
package objects
import (
"os"
"path/filepath"
"strings"
"sync"
"github.com/Masterminds/semver/v3"
"github.com/joomcode/errorx"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/repo"
)
const AnnRepo = "helm-dashboard/repository-name"
type Repositories struct {
Settings *cli.EnvSettings
HelmConfig *action.Configuration
mx sync.Mutex
versionConstraint *semver.Constraints
LocalCharts []string
}
func (r *Repositories) load() (*repo.File, error) {
r.mx.Lock()
defer r.mx.Unlock()
// copied from cmd/helm/repo_list.go
f, err := repo.LoadFile(r.Settings.RepositoryConfig)
if err != nil && !isNotExist(err) {
return nil, errorx.Decorate(err, "failed to load repository list")
}
return f, nil
}
func (r *Repositories) List() ([]Repository, error) {
f, err := r.load()
if err != nil {
return nil, errorx.Decorate(err, "failed to load repo information")
}
res := []Repository{}
for _, item := range f.Repositories {
res = append(res, &HelmRepo{
Settings: r.Settings,
Orig: item,
versionConstraint: r.versionConstraint,
})
}
if len(r.LocalCharts) > 0 {
lc := LocalChart{
LocalCharts: r.LocalCharts,
}
res = append(res, &lc)
}
return res, nil
}
func (r *Repositories) Add(name string, url string, username string, password string) error {
if name == "" || url == "" {
return errors.New("Name and URL are required parameters to add the repository")
}
if (username != "" && password == "") || (username == "" && password != "") {
return errors.New("Username and Password, both are required parameters to add the repository with authentication")
}
// copied from cmd/helm/repo_add.go
repoFile := r.Settings.RepositoryConfig
// Ensure the file directory exists as it is required for file locking
err := os.MkdirAll(filepath.Dir(repoFile), os.ModePerm)
if err != nil && !os.IsExist(err) {
return err
}
f, err := r.load()
if err != nil {
return errorx.Decorate(err, "Failed to load repo config")
}
r.mx.Lock()
defer r.mx.Unlock()
c := repo.Entry{
Name: name,
URL: url,
Username: username,
Password: password,
//PassCredentialsAll: o.passCredentialsAll,
//CertFile: o.certFile,
//KeyFile: o.keyFile,
//CAFile: o.caFile,
//InsecureSkipTLSverify: o.insecureSkipTLSverify,
}
// Check if the repo name is legal
if strings.Contains(c.Name, "/") {
return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", c.Name)
}
rep, err := repo.NewChartRepository(&c, getter.All(r.Settings))
if err != nil {
return err
}
if _, err := rep.DownloadIndexFile(); err != nil {
return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", url)
}
f.Update(&c)
if err := f.WriteFile(repoFile, 0644); err != nil {
return err
}
return nil
}
func (r *Repositories) Delete(name string) error {
f, err := r.load()
if err != nil {
return errorx.Decorate(err, "failed to load repo information")
}
r.mx.Lock()
defer r.mx.Unlock()
// copied from cmd/helm/repo_remove.go
if !f.Remove(name) {
return errors.Errorf("no repo named %q found", name)
}
if err := f.WriteFile(r.Settings.RepositoryConfig, 0644); err != nil {
return err
}
if err := removeRepoCache(r.Settings.RepositoryCache, name); err != nil {
return err
}
return nil
}
func (r *Repositories) Get(name string) (Repository, error) {
l, err := r.List()
if err != nil {
return nil, errorx.Decorate(err, "failed to get list of repos")
}
for _, entry := range l {
if entry.Name() == name {
return entry, nil
}
}
return nil, errorx.DataUnavailable.New("Could not find repository '%s'", name)
}
// Containing returns list of chart versions for the given chart name, across all repositories
func (r *Repositories) Containing(name string) (repo.ChartVersions, error) {
list, err := r.List()
if err != nil {
return nil, errorx.Decorate(err, "failed to get list of repos")
}
res := repo.ChartVersions{}
for _, rep := range list {
vers, err := rep.ByName(name)
if err != nil {
log.Warnf("Failed to get data from repo '%s', updating it might help", rep.Name())
log.Debugf("The error was: %v", err)
continue
}
var updatedChartVersions repo.ChartVersions
for _, v := range vers {
// just using annotations here to attach a bit of information to the object
// it has nothing to do with k8s annotations and should not get into manifests
if v.Annotations == nil {
v.Annotations = map[string]string{}
}
v.Annotations[AnnRepo] = rep.Name()
// Validate the versions against semantic version constraints and filter
version, err := semver.NewVersion(v.Version)
if err != nil {
// Ignored if version string is not parsable
log.Debugf("failed to parse version string %q: %v", v.Version, err)
continue
}
if r.versionConstraint.Check(version) {
// Add only versions that satisfy the semantic version constraint
updatedChartVersions = append(updatedChartVersions, v)
}
}
res = append(res, updatedChartVersions...)
}
return res, nil
}
func (r *Repositories) GetChartValues(chart string, ver string) (string, error) {
// comes from cmd/helm/show.go
client := action.NewShowWithConfig(action.ShowValues, r.HelmConfig)
client.Version = ver
cp, err := client.ChartPathOptions.LocateChart(chart, r.Settings)
if err != nil {
return "", err
}
out, err := client.Run(cp)
if err != nil {
return "", errorx.Decorate(err, "failed to get values for chart '%s'", chart)
}
return out, nil
}
type Repository interface {
Name() string
URL() string
Update() error
Charts() (repo.ChartVersions, error)
ByName(name string) (repo.ChartVersions, error)
}
type HelmRepo struct {
Settings *cli.EnvSettings
Orig *repo.Entry
mx sync.Mutex
versionConstraint *semver.Constraints
}
func (r *HelmRepo) Name() string {
return r.Orig.Name
}
func (r *HelmRepo) URL() string {
return r.Orig.URL
}
func (r *HelmRepo) indexFileName() string {
return filepath.Join(r.Settings.RepositoryCache, helmpath.CacheIndexFile(r.Orig.Name))
}
func (r *HelmRepo) getIndex() (*repo.IndexFile, error) {
r.mx.Lock()
defer r.mx.Unlock()
f := r.indexFileName()
ind, err := repo.LoadIndexFile(f)
if err != nil {
return nil, errorx.Decorate(err, "Repo index is corrupt or missing. Try updating repo")
}
ind.SortEntries()
return ind, nil
}
func (r *HelmRepo) Charts() (repo.ChartVersions, error) {
ind, err := r.getIndex()
if err != nil {
return nil, errorx.Decorate(err, "failed to get repo index")
}
res := repo.ChartVersions{}
for _, cv := range ind.Entries {
for _, v := range cv {
version, err := semver.NewVersion(v.Version)
if err != nil {
// Ignored if version string is not parsable
log.Debugf("failed to parse version string %q: %v", v.Version, err)
continue
}
if r.versionConstraint.Check(version) {
// Add only versions that satisfy the semantic version constraint
res = append(res, v)
// Only the highest version satisfying the constraint is required. Hence, break.
// The constraint here is (only stable versions) vs (stable + dev/prerelease).
// If dev versions are disabled and chart only has dev versions,
// chart is excluded from the result.
break
}
}
}
return res, nil
}
func (r *HelmRepo) ByName(name string) (repo.ChartVersions, error) {
ind, err := r.getIndex()
if err != nil {
return nil, errorx.Decorate(err, "failed to get repo index")
}
nx, ok := ind.Entries[name]
if ok {
return nx, nil
}
return repo.ChartVersions{}, nil
}
func (r *HelmRepo) Update() error {
r.mx.Lock()
defer r.mx.Unlock()
log.Infof("Updating repository: %s", r.Orig.Name)
// from cmd/helm/repo_update.go
// TODO: make this object to be an `Orig`?
rep, err := repo.NewChartRepository(r.Orig, getter.All(r.Settings))
if err != nil {
return errorx.Decorate(err, "could not create repository object")
}
rep.CachePath = r.Settings.RepositoryCache
_, err = rep.DownloadIndexFile()
if err != nil {
return errorx.Decorate(err, "failed to download repo index file")
}
return nil
}
// copied from cmd/helm/repo.go
func isNotExist(err error) bool {
return os.IsNotExist(errors.Cause(err))
}
// copied from cmd/helm/repo_remove.go
func removeRepoCache(root, name string) error {
idx := filepath.Join(root, helmpath.CacheChartsFile(name))
if _, err := os.Stat(idx); err == nil {
_ = os.Remove(idx)
}
idx = filepath.Join(root, helmpath.CacheIndexFile(name))
if _, err := os.Stat(idx); os.IsNotExist(err) {
return nil
} else if err != nil {
return errors.Wrapf(err, "can't remove index file %s", idx)
}
return os.Remove(idx)
}
// versionConstaint returns semantic version constraint instance that can be used to
// validate the version of repositories. The flag isDevelEnabled is used to configure
// enabling/disabling of development/prerelease versions of charts.
func versionConstaint(isDevelEnabled bool) (*semver.Constraints, error) {
// When devel flag is disabled. i.e., Only stable releases are included.
version := ">0.0.0"
if isDevelEnabled {
// When devel flag is enabled. i.e., Prereleases (alpha, beta, release candidate, etc.) are included.
version = ">0.0.0-0"
}
constraint, err := semver.NewConstraint(version)
if err != nil {
return nil, errors.Wrapf(err, "invalid version constraint format %q", version)
}
return constraint, nil
}
type LocalChart struct {
LocalCharts []string
charts map[string]repo.ChartVersions
mx sync.Mutex
}
// Update reloads the chart information from disk
func (l *LocalChart) Update() error {
l.mx.Lock()
defer l.mx.Unlock()
l.charts = map[string]repo.ChartVersions{}
for _, lc := range l.LocalCharts {
c, err := loader.Load(lc)
if err != nil {
log.Warnf("Failed to load chart from '%s': %s", lc, err)
continue
}
// we don't filter out dev versions here, because local chart implies user wants to see the chart anyway
l.charts[c.Name()] = repo.ChartVersions{&repo.ChartVersion{
URLs: []string{l.URL() + lc},
Metadata: c.Metadata,
}}
}
return nil
}
func (l *LocalChart) Name() string {
return "[local]"
}
func (l *LocalChart) URL() string {
return "file://"
}
func (l *LocalChart) Charts() (repo.ChartVersions, error) {
_ = l.Update() // always re-read, for chart devs to have quick debug loop
res := repo.ChartVersions{}
for _, c := range l.charts {
res = append(res, c...)
}
return res, nil
}
func (l *LocalChart) ByName(name string) (repo.ChartVersions, error) {
_ = l.Update() // always re-read, for chart devs to have quick debug loop
for n, c := range l.charts {
if n == name {
return c, nil
}
}
return repo.ChartVersions{}, nil
}

View File

@@ -0,0 +1,289 @@
package objects
import (
"os"
"path"
"testing"
"gotest.tools/v3/assert"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
)
const (
validRepositoryConfigPath = "./testdata/repositories.yaml"
invalidCacheFileRepositoryConfigPath = "./testdata/repositories-invalid-cache-file.yaml"
invalidMalformedManifestRepositoryConfigPath = "./testdata/repositories-malformed-manifest.yaml"
)
func initRepository(t *testing.T, filePath string, devel bool) *Repositories {
t.Helper()
settings := cli.New()
fname, err := os.CreateTemp("", "repo-*.yaml")
if err != nil {
t.Fatal(err)
}
input, err := os.ReadFile(filePath)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(fname.Name(), input, 0644)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
err := os.Remove(fname.Name())
if err != nil {
t.Fatal(err)
}
})
vc, err := versionConstaint(devel)
if err != nil {
t.Fatal(err)
}
// Sets the repository file path
settings.RepositoryConfig = fname.Name()
settings.RepositoryCache = path.Dir(filePath)
testRepository := &Repositories{
Settings: settings,
HelmConfig: &action.Configuration{}, // maybe use copy of getFakeHelmConfig from api_test.go
versionConstraint: vc,
LocalCharts: []string{"../../../charts/helm-dashboard"},
}
return testRepository
}
func TestFlow(t *testing.T) {
testRepository := initRepository(t, validRepositoryConfigPath, false)
// initial list
repos, err := testRepository.List()
assert.NilError(t, err)
assert.Equal(t, len(repos), 5)
testRepoName := "TEST"
testRepoUrl := "https://helm.github.io/examples"
// add repo
err = testRepository.Add(testRepoName, testRepoUrl, "", "")
assert.NilError(t, err)
// get repo
r, err := testRepository.Get(testRepoName)
assert.NilError(t, err)
assert.Equal(t, r.URL(), testRepoUrl)
// update repo
err = r.Update()
assert.NilError(t, err)
// list charts
c, err := r.Charts()
assert.NilError(t, err)
// contains chart
c, err = testRepository.Containing(c[0].Name)
assert.NilError(t, err)
// chart by name from repo
c, err = r.ByName(c[0].Name)
assert.NilError(t, err)
// get chart values
v, err := testRepository.GetChartValues(r.Name()+"/"+c[0].Name, c[0].Version)
assert.NilError(t, err)
assert.Assert(t, v != "")
// delete added
err = testRepository.Delete(testRepoName)
assert.NilError(t, err)
// final list
repos, err = testRepository.List()
assert.NilError(t, err)
assert.Equal(t, len(repos), 5)
}
func TestRepository_Charts_DevelDisabled(t *testing.T) {
testRepository := initRepository(t, validRepositoryConfigPath, false)
r, err := testRepository.Get("testing")
if err != nil {
t.Fatal(err)
}
charts, err := r.Charts()
if err != nil {
t.Fatal(err)
}
// Total charts in ./testdata/testing-index.yaml = 4
// Excluded charts = 2 (1 has invalid version, 1 has only dev version)
// Included charts = 2 (2 stable versions)
expectedCount := 2
if len(charts) != expectedCount {
t.Fatalf("Wrong charts count: %d, expected: %d", len(charts), expectedCount)
}
}
func TestRepository_Charts_DevelEnabled(t *testing.T) {
testRepository := initRepository(t, validRepositoryConfigPath, true)
r, err := testRepository.Get("testing")
if err != nil {
t.Fatal(err)
}
charts, err := r.Charts()
if err != nil {
t.Fatal(err)
}
// Total charts in ./testdata/testing-index.yaml = 4
// Excluded charts = 1 (1 has invalid version)
// Included charts = 3 (2 stable versions, 1 has only dev version)
expectedCount := 3
if len(charts) != expectedCount {
t.Fatalf("Wrong charts count: %d, expected: %d", len(charts), expectedCount)
}
}
func TestRepository_Charts_InvalidCacheFile(t *testing.T) {
testRepository := initRepository(t, invalidCacheFileRepositoryConfigPath, false)
r, err := testRepository.Get("non-existing")
if err != nil {
t.Fatal(err)
}
_, err = r.Charts()
if err == nil {
t.Fatalf("Expected error for invalid cache file path, got nil")
}
}
func TestRepositories_Containing_DevelDisable(t *testing.T) {
testRepository := initRepository(t, validRepositoryConfigPath, false)
chartVersions, err := testRepository.Containing("alpine")
if err != nil {
t.Fatal(err)
}
// Total versions of chart alpine in ./testdata/testing-index.yaml = 3
// Excluded charts = 1 (1 dev version)
// Included charts = 2 (2 stable versions)
expectedCount := 2
if len(chartVersions) != expectedCount {
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
}
}
func TestRepositories_Containing_DevelEnabled(t *testing.T) {
testRepository := initRepository(t, validRepositoryConfigPath, true)
chartVersions, err := testRepository.Containing("alpine")
if err != nil {
t.Fatal(err)
}
// Total versions of chart alpine in ./testdata/testing-index.yaml = 3
// Excluded charts = 0
// Included charts = 3 (2 stable versions, 1 dev version)
expectedCount := 3
if len(chartVersions) != expectedCount {
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
}
}
func TestRepositories_Containing_DevelDisable_OnlyDevVersionsOfChartAvailable(t *testing.T) {
testRepository := initRepository(t, validRepositoryConfigPath, false)
chartVersions, err := testRepository.Containing("traefik")
if err != nil {
t.Fatal(err)
}
// Total versions of chart traefik in ./testdata/testing-index.yaml = 1
// Excluded charts = 1 (1 dev version)
// Included charts = 0
expectedCount := 0
if len(chartVersions) != expectedCount {
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
}
}
func TestRepositories_Containing_DevelEnabled_OnlyDevVersionsOfChartAvailable(t *testing.T) {
testRepository := initRepository(t, validRepositoryConfigPath, true)
chartVersions, err := testRepository.Containing("traefik")
if err != nil {
t.Fatal(err)
}
// Total versions of chart traefik in ./testdata/testing-index.yaml = 1
// Excluded charts = 0
// Included charts = 1 (1 dev version)
expectedCount := 1
if len(chartVersions) != expectedCount {
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
}
}
func TestRepositories_Containing_DevelDisable_InvalidChartVersion(t *testing.T) {
testRepository := initRepository(t, validRepositoryConfigPath, false)
chartVersions, err := testRepository.Containing("rabbitmq")
if err != nil {
t.Fatal(err)
}
// Total versions of chart rabbitmq in ./testdata/testing-index.yaml = 1
// Excluded charts = 1 (1 invalid version)
// Included charts = 0
expectedCount := 0
if len(chartVersions) != expectedCount {
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
}
}
func TestRepositories_Containing_DevelEnabled_InvalidChartVersion(t *testing.T) {
testRepository := initRepository(t, validRepositoryConfigPath, true)
chartVersions, err := testRepository.Containing("rabbitmq")
if err != nil {
t.Fatal(err)
}
// Total versions of chart rabbitmq in ./testdata/testing-index.yaml = 1
// Excluded charts = 1 (1 invalid version)
// Included charts = 0
expectedCount := 0
if len(chartVersions) != expectedCount {
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
}
}
func TestRepositories_Containing_MalformedRepositoryConfigFile(t *testing.T) {
testRepository := initRepository(t, invalidMalformedManifestRepositoryConfigPath, false)
_, err := testRepository.Containing("alpine")
if err == nil {
t.Fatalf("Expected error for malformed RepositoryConfig file, got nil")
}
}

View File

@@ -0,0 +1,6 @@
apiVersion: ""
generated: "0001-01-01T00:00:00Z"
repositories:
- cache: non-existing-index.yaml
name: non-existing
url: http://example.com/charts

View File

@@ -0,0 +1,12 @@
apiVersion: ""
generated: "0001-01-01T00:00:00Z"
- repositories:
- caFile: ""
certFile: ""
insecure_skip_tls_verify: false
keyFile: ""
name: charts
pass_credentials_all: false
password: ""
url: https://charts.helm.sh/stable
username: ""

View File

@@ -0,0 +1,33 @@
apiVersion: ""
generated: "0001-01-01T00:00:00Z"
repositories:
- caFile: ""
certFile: ""
insecure_skip_tls_verify: false
keyFile: ""
name: charts
pass_credentials_all: false
password: ""
url: https://charts.helm.sh/stable
username: ""
- caFile: ""
certFile: ""
insecure_skip_tls_verify: false
keyFile: ""
name: firstexample
pass_credentials_all: false
password: ""
url: http://firstexample.com
username: ""
- caFile: ""
certFile: ""
insecure_skip_tls_verify: false
keyFile: ""
name: secondexample
pass_credentials_all: false
password: ""
url: http://secondexample.com
username: ""
- cache: testing-index.yaml
name: testing
url: http://example.com/charts

View File

@@ -0,0 +1,100 @@
apiVersion: v1
entries:
alpine:
- name: alpine
url: https://charts.helm.sh/stable/alpine-0.1.0.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
created: "2018-06-27T10:00:18.230700509Z"
deprecated: true
home: https://helm.sh/helm
sources:
- https://github.com/helm/helm
version: 0.1.0
appVersion: 1.2.3
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
icon: ""
apiVersion: v2
- name: alpine
url: https://charts.helm.sh/stable/alpine-0.2.0.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
created: "2018-07-09T11:34:37.797864902Z"
home: https://helm.sh/helm
sources:
- https://github.com/helm/helm
version: 0.2.0
appVersion: 2.3.4
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
icon: ""
apiVersion: v2
- name: alpine
url: https://charts.helm.sh/stable/alpine-0.3.0-rc.1.tgz
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
created: "2020-11-12T08:44:58.872726222Z"
home: https://helm.sh/helm
sources:
- https://github.com/helm/helm
version: 0.3.0-rc.1
appVersion: 3.0.0
description: Deploy a basic Alpine Linux pod
keywords: []
maintainers: []
icon: ""
apiVersion: v2
mariadb:
- name: mariadb
url: https://charts.helm.sh/stable/mariadb-0.3.0.tgz
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
created: "2018-04-23T08:20:27.160959131Z"
home: https://mariadb.org
sources:
- https://github.com/bitnami/bitnami-docker-mariadb
version: 0.3.0
description: Chart for MariaDB
keywords:
- mariadb
- mysql
- database
- sql
maintainers:
- name: Bitnami
email: containers@bitnami.com
icon: ""
apiVersion: v2
traefik:
- apiVersion: v1
appVersion: 1.7.26
deprecated: true
description: A Traefik based Kubernetes ingress controller with Let's
Encrypt support
home: https://traefik.io/
icon: https://docs.traefik.io/assets/img/traefik.logo.png
keywords:
- traefik
- ingress
- acme
- letsencrypt
name: traefik
sources:
- https://github.com/containous/traefik
- https://github.com/helm/charts/tree/master/stable/traefik
version: 1.87.7-rc1
rabbitmq:
- apiVersion: v1
appVersion: 3.8.2
deprecated: true
description: DEPRECATED Open source message broker software that implements the Advanced
Message Queuing Protocol (AMQP)
home: https://www.rabbitmq.com
icon: https://bitnami.com/assets/stacks/rabbitmq/img/rabbitmq-stack-220x234.png
keywords:
- rabbitmq
- message queue
- AMQP
name: rabbitmq
sources:
- https://github.com/bitnami/bitnami-docker-rabbitmq
version: invalid-version

View File

@@ -1,16 +1,51 @@
package scanners
import (
"encoding/json"
"github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
"github.com/olekukonko/tablewriter"
log "github.com/sirupsen/logrus"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"strconv"
"strings"
)
type Checkov struct {
Data *subproc.DataLayer
Data *objects.DataLayer
}
func (c *Checkov) ManifestIsScannable() bool {
return true
}
func (c *Checkov) SupportedResourceKinds() []string {
// from https://github.com/bridgecrewio/checkov//blob/master/docs/5.Policy%20Index/kubernetes.md
return []string{
"AdmissionConfiguration",
"ClusterRole",
"ClusterRoleBinding",
"ConfigMap",
"CronJob",
"DaemonSet",
"Deployment",
"DeploymentConfig",
"Ingress",
"Job",
"Pod",
"PodSecurityPolicy",
"PodTemplate",
"Policy",
"ReplicaSet",
"ReplicationController",
"Role",
"RoleBinding",
"Secret",
"Service",
"ServiceAccount",
"StatefulSet",
}
}
func (c *Checkov) Name() string {
@@ -18,6 +53,9 @@ func (c *Checkov) Name() string {
}
func (c *Checkov) Test() bool {
utils.FailLogLevel = log.DebugLevel
defer func() { utils.FailLogLevel = log.WarnLevel }()
res, err := utils.RunCommand([]string{"checkov", "--version"}, nil)
if err != nil {
return false
@@ -33,7 +71,7 @@ func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) {
}
defer fclose()
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "cli", "--file", fname}
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "json", "--file", fname}
out, err := utils.RunCommand(cmd, nil)
if err != nil {
return nil, err
@@ -41,7 +79,10 @@ func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) {
res := &subproc.ScanResults{}
res.OrigReport = out
err = json.Unmarshal([]byte(out), &res.OrigReport)
if err != nil {
return nil, err
}
return res, nil
}
@@ -50,52 +91,62 @@ func (c *Checkov) ScanResource(ns string, kind string, name string) (*subproc.Sc
carp := v1.Carp{}
carp.Kind = kind
carp.Name = name
mnf, err := c.Data.GetResourceYAML(ns, &carp)
app, err := c.Data.AppForCtx(c.Data.KubeContext)
if err != nil {
return nil, err
return nil, errorx.Decorate(err, "failed to get app for context")
}
mnf, err := app.K8s.GetResourceYAML(kind, ns, name)
if err != nil {
return nil, errorx.Decorate(err, "failed to get YAML for resource")
}
fname, fclose, err := utils.TempFile(mnf)
if err != nil {
return nil, err
return nil, errorx.Decorate(err, "failed to create temporary file")
}
defer fclose()
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "cli", "--file", fname}
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "json", "--file", fname}
out, err := utils.RunCommand(cmd, nil)
if err != nil {
return nil, err
}
res := subproc.ScanResults{}
_, out, _ = strings.Cut(out, "\n") // kubernetes scan results:
_, out, _ = strings.Cut(out, "\n") // empty line
line, out, found := strings.Cut(out, "\n") // status line
if found {
parts := strings.FieldsFunc(line, func(r rune) bool {
return r == ':' || r == ','
})
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
res.PassedCount = cnt
} else {
log.Warnf("Failed to parse Checkov output: %s", err)
}
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[3])); err == nil {
res.FailedCount = cnt
} else {
log.Warnf("Failed to parse Checkov output: %s", err)
}
} else {
log.Warnf("Failed to parse Checkov output")
cr := CheckovReport{}
err = json.Unmarshal([]byte(out), &cr)
if err != nil {
return nil, err
}
res.OrigReport = strings.TrimSpace(out)
res := &subproc.ScanResults{
PassedCount: cr.Summary.Passed,
FailedCount: cr.Summary.Failed,
OrigReport: checkovReportTable(&cr),
}
return &res, nil
return res, nil
}
type CheckovResults struct {
Summary CheckovSummary
func checkovReportTable(c *CheckovReport) string {
data := [][]string{}
for _, item := range c.Results.FailedChecks {
data = append(data, []string{item.Id, item.Name + "\n", item.Guideline})
}
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
table.SetHeader([]string{"ID", "Name", "Guideline"})
table.SetBorder(false)
table.SetColWidth(64)
table.AppendBulk(data)
table.Render()
return tableString.String()
}
type CheckovReport struct {
Summary CheckovSummary `json:"summary"`
Results CheckovResults `json:"results"`
}
type CheckovSummary struct {
@@ -105,3 +156,16 @@ type CheckovSummary struct {
// parsing errors?
// skipped ?
}
type CheckovResults struct {
FailedChecks []CheckovCheck `json:"failed_checks"`
}
type CheckovCheck struct {
Id string `json:"check_id"`
BcId string `json:"bc_check_id"`
Name string `json:"check_name"`
Resource string `json:"resource"`
Guideline string `json:"guideline"`
FileLineRange []int `json:"file_line_range"`
}

View File

@@ -1,6 +1,7 @@
package scanners
import (
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
@@ -9,7 +10,24 @@ import (
)
type Trivy struct {
Data *subproc.DataLayer
Data *objects.DataLayer
}
func (c *Trivy) ManifestIsScannable() bool {
return false
}
func (c *Trivy) SupportedResourceKinds() []string {
// from https://github.com/aquasecurity/trivy-kubernetes/blob/main/pkg/k8s/k8s.go#L190
return []string{
"ReplicaSet",
"ReplicationController",
"StatefulSet",
"Deployment",
"CronJob",
"DaemonSet",
"Job",
}
}
func (c *Trivy) Name() string {
@@ -17,6 +35,9 @@ func (c *Trivy) Name() string {
}
func (c *Trivy) Test() bool {
utils.FailLogLevel = log.DebugLevel
defer func() { utils.FailLogLevel = log.WarnLevel }()
res, err := utils.RunCommand([]string{"trivy", "--version"}, nil)
if err != nil {
return false

View File

@@ -2,67 +2,140 @@ package dashboard
import (
"context"
"github.com/gin-gonic/gin"
"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"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/registry"
"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/utils"
log "github.com/sirupsen/logrus"
)
func StartServer(version string) (string, utils.ControlChan) {
data := subproc.DataLayer{}
err := data.CheckConnectivity()
if err != nil {
log.Errorf("Failed to check that Helm is operational, cannot continue. The error was: %s", err)
os.Exit(1) // TODO: propagate error instead?
}
discoverScanners(&data)
address := os.Getenv("HD_BIND")
if address == "" {
address = "localhost"
}
if os.Getenv("HD_PORT") == "" {
address += ":8080" // TODO: better default port to clash less?
} else {
address += ":" + os.Getenv("HD_PORT")
}
abort := make(utils.ControlChan)
api := NewRouter(abort, &data, version)
done := startBackgroundServer(address, api, abort)
return "http://" + address, done
type Server struct {
Version string
Namespaces []string
Address string
Debug bool
NoTracking bool
Devel bool
LocalCharts []string
}
func startBackgroundServer(addr string, routes *gin.Engine, abort utils.ControlChan) utils.ControlChan {
done := make(utils.ControlChan)
server := &http.Server{Addr: addr, Handler: routes}
func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (string, utils.ControlChan, error) {
data, err := objects.NewDataLayer(s.Namespaces, s.Version, NewHelmConfig, s.Devel)
if err != nil {
return "", nil, errorx.Decorate(err, "Failed to create data layer")
}
go func() {
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
panic(err) // TODO: in case of "port busy", check that it's another instance of us and just open browser
data.LocalCharts = s.LocalCharts
isDevModeWithAnalytics := os.Getenv("HD_DEV_ANALYTICS") == "true"
data.StatusInfo.Analytics = (!s.NoTracking && s.Version != "0.0.0") || isDevModeWithAnalytics
err = s.detectClusterMode(data)
if err != nil {
return "", nil, errorx.Decorate(err, "Failed to detect cluster mode")
}
go checkUpgrade(data.StatusInfo)
discoverScanners(data)
go data.PeriodicTasks(ctx)
api := NewRouter(cancel, data, s.Debug)
done := s.startBackgroundServer(api, ctx)
return "http://" + s.Address, done, nil
}
func (s *Server) detectClusterMode(data *objects.DataLayer) error {
data.StatusInfo.ClusterMode = os.Getenv("HD_CLUSTER_MODE") != ""
if data.StatusInfo.ClusterMode {
return nil
}
ctxs, err := data.ListContexts()
if err != nil {
return err
}
if len(ctxs) == 0 {
log.Infof("Got no kubectl config contexts, will attempt to detect if we're inside cluster...")
app, err := data.AppForCtx("")
if err != nil {
return err
}
done <- struct{}{}
}()
ns, err := app.K8s.GetNameSpaces()
if err != nil { // no point in continuing without kubectl context and k8s connection
return errorx.InitializationFailed.Wrap(err, "No k8s cluster connection")
}
log.Debugf("Got %d namespaces listed", len(ns.Items))
data.StatusInfo.ClusterMode = true
}
return err
}
func (s *Server) startBackgroundServer(routes *gin.Engine, ctx context.Context) utils.ControlChan {
done := make(utils.ControlChan)
server := &http.Server{
Addr: s.Address,
Handler: routes,
}
go func() {
<-abort
<-ctx.Done()
err := server.Shutdown(context.Background())
if err != nil {
log.Warnf("Had problems shutting down the server: %s", err)
}
log.Infof("Web server has been shut down.")
}()
go func() {
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Warnf("Looks like port is busy for %s, checking if it's us...", s.Address)
if s.itIsUs() {
log.Infof("Yes, it's another instance of us. Just reuse it.")
} else {
panic(err)
}
}
done <- struct{}{}
}()
return done
}
func discoverScanners(data *subproc.DataLayer) {
func (s *Server) itIsUs() bool {
url := fmt.Sprintf("http://%s/status", s.Address)
var myClient = &http.Client{
Timeout: 5 * time.Second,
}
r, err := myClient.Get(url)
if err != nil {
log.Debugf("It's not us on %s: %s", s.Address, err)
return false
}
defer r.Body.Close()
return strings.HasPrefix(r.Header.Get("X-Application-Name"), "Helm Dashboard")
}
func discoverScanners(data *objects.DataLayer) {
potential := []subproc.Scanner{
&scanners.Checkov{Data: data},
&scanners.Trivy{Data: data},
@@ -75,3 +148,75 @@ func discoverScanners(data *subproc.DataLayer) {
}
}
}
func checkUpgrade(d *objects.StatusInfo) { // TODO: check it once an hour
url := "https://api.github.com/repos/komodorio/helm-dashboard/releases/latest"
type GHRelease struct {
Name string `json:"name"`
}
var myClient = &http.Client{Timeout: 5 * time.Second}
r, err := myClient.Get(url)
if err != nil {
log.Warnf("Failed to check for new version: %s", err)
return
}
defer r.Body.Close()
target := new(GHRelease)
err = json.NewDecoder(r.Body).Decode(target)
if err != nil {
log.Warnf("Failed to decode new release version: %s", err)
return
}
d.LatestVer = target.Name
v1, err := version.NewVersion(d.CurVer)
if err != nil {
log.Warnf("Failed to parse CurVer: %s", err)
v1 = &version.Version{}
}
v2, err := version.NewVersion(d.LatestVer)
if err != nil {
log.Warnf("Failed to parse RepoLatestVer: %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)
}
}
}
func NewHelmConfig(origSettings *cli.EnvSettings, ns string) (*action.Configuration, error) {
// TODO: cache it into map
// TODO: I feel there should be more elegant way to organize this code
actionConfig := new(action.Configuration)
settings := cli.New()
settings.KubeContext = origSettings.KubeContext
settings.SetNamespace(ns) // important for RESTClientGetter to have correct namespace
registryClient, err := registry.NewClient(
registry.ClientOptDebug(false),
registry.ClientOptEnableCache(true),
//registry.ClientOptWriter(out),
registry.ClientOptCredentialsFile(settings.RegistryConfig),
)
if err != nil {
return nil, errorx.Decorate(err, "failed to crete helm config object")
}
actionConfig.RegistryClient = registryClient
helmDriver := os.Getenv("HELM_DRIVER")
if err := actionConfig.Init(
settings.RESTClientGetter(),
ns,
helmDriver, log.Debugf); err != nil {
return nil, errorx.Decorate(err, "failed to init Helm action config")
}
return actionConfig, nil
}

View File

@@ -5,7 +5,7 @@ $("#btnUpgradeCheck").click(function () {
const repoName = self.data("repo")
$("#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/repositories/" + repoName).fail(function (xhr) {
reportError("Failed to update chart repo", xhr)
}).done(function () {
self.find(".spinner-border").hide()
@@ -16,26 +16,33 @@ $("#btnUpgradeCheck").click(function () {
})
})
function checkUpgradeable(name) {
$.getJSON("/api/helm/repo/search?name=" + name).fail(function (xhr) {
$.getJSON("/api/helm/repositories/latestver?name=" + name).fail(function (xhr) {
reportError("Failed to find chart in repo", xhr)
}).done(function (data) {
let elm = {name: "", version: "0"}
const btnUpgradeCheck = $("#btnUpgradeCheck");
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
btnUpgradeCheck.prop("disabled", true)
btnUpgradeCheck.text("")
$("#btnAddRepository").text("Add repository for it").data("suggestRepo", "")
} else if (data[0].isSuggestedRepo) {
btnUpgradeCheck.prop("disabled", true)
btnUpgradeCheck.text("")
$("#btnAddRepository").text("Add repository for it: " + data[0].repository).data("suggestRepo", data[0].repository).data("suggestRepoUrl", data[0].urls[0])
} else {
$("#btnAddRepository").text("")
btnUpgradeCheck.text("Check for new version")
elm = data[0]
}
$("#btnUpgrade .icon").removeClass("bi-arrow-up bi-pencil").addClass("bi-hourglass-split")
const verCur = $("#specRev").data("last-chart-ver");
const elm = data[0]
$("#btnUpgradeCheck").data("repo", elm.name.split('/').shift())
$("#btnUpgradeCheck").data("chart", elm.name.split('/').pop())
btnUpgradeCheck.data("repo", elm.repository)
btnUpgradeCheck.data("chart", elm.name)
const canUpgrade = isNewerVersion(verCur, elm.version);
$("#btnUpgradeCheck").prop("disabled", false)
btnUpgradeCheck.prop("disabled", false)
if (canUpgrade) {
$("#btnUpgrade span").text("Upgrade to " + elm.version)
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-arrow-up")
@@ -53,66 +60,94 @@ function checkUpgradeable(name) {
function popUpUpgrade(elm, ns, name, verCur, lastRev) {
$("#upgradeModal .btn-confirm").prop("disabled", true)
$('#upgradeModal').data("chart", elm.name).data("initial", !verCur)
$('#upgradeModal').data("initial", !verCur)
$('#upgradeModal').data("newManifest", "")
$("#upgradeModalLabel .name").text(elm.name)
$("#upgradeModal .rel-cluster").text(getHashParam("context"))
if (verCur) {
$("#upgradeModalLabel .type").text("Upgrade")
$("#upgradeModal .ver-old").show().find("span").text(verCur)
$("#upgradeModal .rel-name").prop("disabled", true).val(name)
$("#upgradeModal .rel-ns").prop("disabled", true).val(ns)
$.get("/api/helm/releases/" + ns + "/" + name + "/manifests").fail(function (xhr) {
reportError("Failed to get current manifest", xhr)
}).done(function (text) {
$('#upgradeModal').data("curManifest", text)
})
} else {
$("#upgradeModalLabel .type").text("Install")
$("#upgradeModal .ver-old").hide()
$("#upgradeModal .rel-name").prop("disabled", false).val(elm.name.split("/").pop())
$("#upgradeModal .rel-ns").prop("disabled", false).val("")
$("#upgradeModal .rel-ns").prop("disabled", false).val(ns)
$('#upgradeModal').data("curManifest", "")
}
$.getJSON("/api/helm/repo/search?name=" + elm.name).fail(function (xhr) {
reportError("Failed to find chart in repo", xhr)
}).done(function (vers) {
// fill versions
$('#upgradeModal select').empty()
for (let i = 0; i < vers.length; i++) {
const opt = $("<option value='" + vers[i].version + "'></option>");
if (vers[i].version === verCur) {
opt.html(vers[i].version + " &middot;")
} else {
opt.html(vers[i].version)
if (elm.name) {
$.getJSON("/api/helm/repositories/versions?name=" + elm.name).fail(function (xhr) {
reportError("Failed to find chart in repo", xhr)
}).done(function (vers) {
vers.sort((a, b) => (isNewerVersion(a.version, b.version)?1:-1))
// fill versions
$('#upgradeModal select').empty()
for (let i = 0; i < vers.length; i++) {
const opt = $("<option value='" + vers[i].version + "'></option>").data("ver", vers[i]);
const label = vers[i].repository + " @ " + vers[i].version;
if (vers[i].version === verCur) {
opt.html(label + " ✓")
} else {
opt.html(label)
}
$('#upgradeModal select').append(opt)
}
$('#upgradeModal select').append(opt)
}
$('#upgradeModal select').val(elm.version).trigger("change")
$('#upgradeModal select').val(elm.version).parent().show()
upgrPopUpCommon(verCur, ns, lastRev, name)
})
} else { // chart without repo reconfigure
$('#upgradeModal select').empty().parent().hide()
upgrPopUpCommon(verCur, ns, lastRev, name)
}
}
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
myModal.show()
function upgrPopUpCommon(verCur, ns, lastRev, name) {
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)
}
})
if (verCur) {
// fill current values
$.get("/api/helm/releases/" + ns + "/" + name + "/values?userDefined=true&revision=" + lastRev).fail(function (xhr) {
reportError("Failed to get charts values info", xhr)
}).done(function (data) {
$("#upgradeModal textarea").val(data).data("dirty", false)
$('#upgradeModal select').trigger("change")
})
} else {
$("#upgradeModal textarea").val("").data("dirty", true)
$('#upgradeModal select').trigger("change")
}
}
$("#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>')
$('#upgradeModal form .preview-mode').val("false")
$.ajax({
type: 'POST',
url: "/api/helm/charts/install" + upgradeModalQstr() + "&flag=true",
data: $("#upgradeModal textarea").data("dirty") ? $("#upgradeModal form").serialize() : null,
url: upgradeModalURL(),
data: $("#upgradeModal form").serialize(),
}).fail(function (xhr) {
reportError("Failed to upgrade the chart", xhr)
}).done(function (data) {
if (data.version) {
setHashParam("section", null)
setHashParam("namespace", $("#upgradeModal .rel-ns").val())
const ns = $("#upgradeModal .rel-ns").val();
setHashParam("namespace", ns ? ns : "default") // TODO: relaets issue #51
setHashParam("chart", $("#upgradeModal .rel-name").val())
setHashParam("revision", data.version)
window.location.reload()
@@ -130,9 +165,7 @@ function changeTimer() {
if (reconfigTimeout) {
window.clearTimeout(reconfigTimeout)
}
reconfigTimeout = window.setTimeout(function () {
requestChangeDiff()
}, 500)
reconfigTimeout = window.setTimeout(requestChangeDiff, 500)
}
$("#upgradeModal textarea").keyup(changeTimer)
@@ -141,27 +174,51 @@ $("#upgradeModal .rel-ns").keyup(changeTimer)
$('#upgradeModal select').change(function () {
const self = $(this)
const ver = self.find("option:selected").data("ver");
let chart = ""
if (ver) {
chart = ver.repository + "/" + ver.name;
// local chart case
if (ver.urls && ver.urls.length && ver.urls[0].startsWith("file://")) {
chart = ver.urls[0];
}
}
$('#upgradeModal').data("chart", chart)
$('#upgradeModal form .chart-name').val(chart)
requestChangeDiff()
// 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)
}).done(function (data) {
data = hljs.highlight(data, {language: 'yaml'}).value
$("#upgradeModal .ref-vals").html(data)
})
// TODO: if chart is empty, query different URL that will restore values without repo
if (chart) {
$.get("/api/helm/repositories/values?chart=" + chart + "&version=" + self.val()).fail(function (xhr) {
reportError("Failed to get upgrade info", xhr)
}).done(function (data) {
data = hljs.highlight(data, {language: 'yaml'}).value
$("#upgradeModal .ref-vals").html(data)
})
} else {
$("#upgradeModal .ref-vals").html("No original values information found")
}
})
$('#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>')
const form = new FormData();
form.append('manifest', $('#upgradeModal').data("newManifest"));
$.ajax({
type: "POST",
url: "/api/scanners/manifests" + upgradeModalQstr(),
data: $("#upgradeModal form").serialize(),
url: "/api/scanners/manifests",
processData: false,
contentType: false,
data: form,
}).fail(function (xhr) {
reportError("Failed to scan the manifest", xhr)
}).done(function (data) {
@@ -175,7 +232,7 @@ $('#upgradeModal .btn-scan').click(function () {
continue
}
const pre = $("<pre></pre>").text(res.OrigReport)
const pre = $("<pre></pre>").text(JSON.stringify(res.OrigReport, null, 2))
container.append("<h2>" + name + " Scan Results</h2>")
container.append(pre)
@@ -188,15 +245,14 @@ $('#upgradeModal .btn-scan').click(function () {
})
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;
$('#upgradeModal form .preview-mode').val("true")
let form = $("#upgradeModal form").serialize();
if ($("#upgradeModal textarea").data("dirty")) {
$("#upgradeModal .invalid-feedback").hide()
values = $("#upgradeModal form").serialize()
try {
jsyaml.load($("#upgradeModal textarea").val())
@@ -209,36 +265,52 @@ function requestChangeDiff() {
$.ajax({
type: "POST",
url: "/api/helm/charts/install" + upgradeModalQstr(),
data: values,
url: upgradeModalURL(),
data: form,
}).fail(function (xhr) {
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>")
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>")
}).done(function (data) {
diffBody.empty();
$("#upgradeModal .btn-confirm").prop("disabled", false)
$('#upgradeModal').data("newManifest", data.manifest)
const targetElement = document.getElementById('upgradeModalBody');
const configuration = {
inputFormat: 'diff', outputFormat: 'side-by-side',
drawFileList: false, showFiles: false, highlight: true,
};
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
diff2htmlUi.draw()
if (!data) {
diffBody.html("No changes will happen to the cluster")
}
const form = new FormData();
form.append('a', $('#upgradeModal').data("curManifest"));
form.append('b', data.manifest);
$.ajax({
type: "POST",
url: "/diff",
processData: false,
contentType: false,
data: form,
}).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)
const targetElement = document.getElementById('upgradeModalBody');
const configuration = {
inputFormat: 'diff', outputFormat: 'side-by-side',
drawFileList: false, showFiles: false, highlight: true,
};
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
diff2htmlUi.draw()
if (!data) {
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()
function upgradeModalURL() {
let ns = $("#upgradeModal .rel-ns").val();
if (!ns) {
ns = "[empty]"
}
if ($("#upgradeModal").data("initial")) {
qstr += "&initial=true"
let qstr = "/api/helm/releases/" + ns;
if (!$("#upgradeModal").data("initial")) {
qstr += "/" + $("#upgradeModal .rel-name").val()
}
return qstr
@@ -253,7 +325,7 @@ $("#btnUninstall").click(function () {
$("#confirmModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
btnConfirm.prop("disabled", true).off('click').click(function () {
btnConfirm.prop("disabled", true).append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
const url = "/api/helm/charts?namespace=" + namespace + "&name=" + chart;
const url = "/api/helm/releases/" + namespace + "/" + chart;
$.ajax({
url: url,
type: 'DELETE',
@@ -267,9 +339,7 @@ $("#btnUninstall").click(function () {
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'));
myModal.show()
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
let url = "/api/helm/charts/resources"
url += "?" + qstr
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources"
$.getJSON(url).fail(function (xhr) {
reportError("Failed to get list of resources", xhr)
}).done(function (data) {
@@ -291,10 +361,13 @@ $("#btnRollback").click(function () {
$("#confirmModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
btnConfirm.prop("disabled", true).off('click').click(function () {
btnConfirm.prop("disabled", true).append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
const url = "/api/helm/charts/rollback?namespace=" + namespace + "&name=" + chart + "&revision=" + revisionNew;
const url = "/api/helm/releases/" + namespace + "/" + chart + "/rollback";
$.ajax({
url: url,
type: 'POST',
data: {
revision: revisionNew
}
}).fail(function (xhr) {
reportError("Failed to rollback the chart", xhr)
}).done(function () {
@@ -305,8 +378,8 @@ $("#btnRollback").click(function () {
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'), {});
myModal.show()
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revisionNew + "&revisionDiff=" + revisionCur
let url = "/api/helm/charts/manifests"
let qstr = "revision=" + revisionNew + "&revisionDiff=" + revisionCur
let url = "/api/helm/releases/" + namespace + "/" + chart + "/manifests"
url += "?" + qstr
$.get(url).fail(function (xhr) {
reportError("Failed to get list of resources", xhr)
@@ -329,3 +402,34 @@ $("#btnRollback").click(function () {
})
})
$("#btnAddRepository").click(function () {
const self = $(this)
setHashParam("section", "repository")
if (self.data("suggestRepo")) {
setHashParam("suggestRepo", self.data("suggestRepo"))
setHashParam("suggestRepoUrl", self.data("suggestRepoUrl"))
}
window.location.reload()
})
$("#btnTest").click(function () {
const myModal = new bootstrap.Modal(document.getElementById('testModal'), {});
$("#testModal .test-result").empty().prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Waiting for completion...')
myModal.show()
$.ajax({
type: 'POST',
url: "/api/helm/releases/" + getHashParam("namespace") + "/" + getHashParam("chart") + "/test"
}).fail(function (xhr) {
reportError("Failed to execute test for chart", xhr)
myModal.hide()
}).done(function (data) {
var output;
if (data.length == 0 || data == null || data == "") {
output = "<div>Tests executed successfully<br><br><pre>Empty response from API<pre></div>"
} else {
output = data.replaceAll("\n", "<br>")
}
$("#testModal .test-result").empty().html(output)
myModal.show()
})
})

View File

@@ -0,0 +1,131 @@
const xhr = new XMLHttpRequest();
const TRACK_EVENT_TYPE = "track"
const IDENTIFY_EVENT_TYPE = "identify"
const BASE_ANALYTIC_MSG = {
method: "POST",
mode: "cors",
cache: "no-cache",
headers: {
"Content-Type": "application/json",
"api-key": "komodor.analytics@admin.com",
},
redirect: "follow",
referrerPolicy: "no-referrer"
}
xhr.onload = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
const status = JSON.parse(xhr.responseText);
const version = status.CurVer
if (status.Analytics) {
enableDD(version)
enableHeap(version, status.ClusterMode)
enableSegmentBackend(version, status.ClusterMode)
} else {
console.log("Analytics is disabled in this session")
}
}
}
xhr.open('GET', '/status', true);
xhr.send(null);
function enableDD(version) {
(function (h, o, u, n, d) {
h = h[d] = h[d] || {
q: [], onReady: function (c) {
h.q.push(c)
}
}
d = o.createElement(u);
d.async = true;
d.src = n
n = o.getElementsByTagName(u)[0];
n.parentNode.insertBefore(d, n)
})(window, document, 'script', 'https://www.datadoghq-browser-agent.com/datadog-rum-v4.js', 'DD_RUM')
DD_RUM.onReady(function () {
DD_RUM.init({
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
applicationId: 'e75439e5-e1b3-46ba-a9e9-a2e58579a2e2',
site: 'datadoghq.com',
service: 'helm-dashboard',
version: version,
trackInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: 'mask',
sessionReplaySampleRate: 0
})
})
}
function enableHeap(version, inCluster) {
window.heap = window.heap || [], heap.load = function (e, t) {
window.heap.appid = e, window.heap.config = t = t || {};
let r = document.createElement("script");
r.type = "text/javascript", r.async = !0, r.src = "https://cdn.heapanalytics.com/js/heap-" + e + ".js";
let a = document.getElementsByTagName("script")[0];
a.parentNode.insertBefore(r, a);
for (let n = function (e) {
return function () {
heap.push([e].concat(Array.prototype.slice.call(arguments, 0)))
}
}, p = ["addEventProperties", "addUserProperties", "clearEventProperties", "identify", "resetIdentity", "removeEventProperty", "setEventProperties", "track", "unsetEventProperty"], o = 0; o < p.length; o++) heap[p[o]] = n(p[o])
};
heap.load("4249623943");
window.heap.addEventProperties({
'version': version,
'installationMode': inCluster ? "cluster" : "local"
});
}
function sendStats(name, prop) {
if (window.heap) {
window.heap.track(name, prop);
}
}
function enableSegmentBackend(version, ClusterMode) {
sendToSegmentThroughAPI("helm dashboard loaded", {version, 'installationMode': ClusterMode ? "cluster" : "local"}, TRACK_EVENT_TYPE)
}
function sendToSegmentThroughAPI(eventName, properties, segmentCallType) {
const userId = getUserId();
try {
sendData(properties, segmentCallType, userId, eventName);
} catch (e) {
console.log("failed sending data to segment", e);
}
}
function sendData(data, eventType, userId, eventName) {
const body = createBody(eventType, userId, data, eventName);
return fetch(`https://api.komodor.com/analytics/segment/${eventType}`, {
...BASE_ANALYTIC_MSG,
body: JSON.stringify(body),
});
}
function createBody(segmentCallType, userId, params, eventName) {
const data = {userId: userId};
if (segmentCallType === IDENTIFY_EVENT_TYPE) {
data["traits"] = params;
} else if (segmentCallType === TRACK_EVENT_TYPE) {
if (!eventName) {
throw new Error("no eventName parameter on segment track call");
}
data["properties"] = params;
data["eventName"] = eventName;
}
return data;
}
const getUserId = (() => {
let userId = null;
return () => {
if (!userId) {
userId = crypto.randomUUID();
}
return userId;
};
})();

View File

@@ -0,0 +1,71 @@
<html lang="">
<head>
<link rel="icon" href="../static/logo.png"/>
<link rel="stylesheet" type="text/css"
href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.css"/>
<title>
Helm Dashboard API
</title>
</head>
<body>
<div id="swagger-ui">
<div class="center_progress">
<div class="lds-dual-ring"></div>
</div>
</div>
</body>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-bundle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.js"></script>
<script>
let swaggerUrl = "openapi.json";
function reqOas() {
const request = new XMLHttpRequest();
request.open('GET', swaggerUrl, true);
request.setRequestHeader('Accept', 'application/json');
request.onload = function () {
if (request.status >= 200 && request.status < 400) {
// Success!
const data = JSON.parse(request.responseText);
display(data);
} else {
alert("Failed to get " + swaggerUrl)
}
};
request.onerror = function () {
alert("Failed to get " + swaggerUrl)
};
request.send();
}
function display(data) {
const parent = document.querySelectorAll('#swagger-ui')[0];
parent.innerHTML = '';
let el = document.createElement('div');
el.id = "swDocs";
parent.appendChild(el);
SwaggerUIBundle({
spec: data,
dom_id: '#' + el.id,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
]
});
}
$(function () {
reqOas();
});
</script>
<script src="analytics.js"></script>
</html>

View File

@@ -1,26 +0,0 @@
(function(h,o,u,n,d) {
h=h[d]=h[d]||{q:[],onReady:function(c){h.q.push(c)}}
d=o.createElement(u);d.async=1;d.src=n
n=o.getElementsByTagName(u)[0];n.parentNode.insertBefore(d,n)
})(window,document,'script','https://www.datadoghq-browser-agent.com/datadog-rum-v4.js','DD_RUM')
DD_RUM.onReady(function() {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
const version = xhr.responseText;
if (xhr.readyState === XMLHttpRequest.DONE && version!=="dev") {
DD_RUM.init({
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
applicationId: 'e75439e5-e1b3-46ba-a9e9-a2e58579a2e2',
site: 'datadoghq.com',
service: 'helm-dashboard',
version: version,
trackInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: 'mask'
})
}
}
xhr.open('GET', '/status', true);
xhr.send(null);
})

View File

@@ -13,7 +13,11 @@ function revisionClicked(namespace, name, self) {
$("#sectionDetails .rev-tags .rev-chart").text(elm.chart)
$("#sectionDetails .rev-tags .rev-app").text(elm.app_version)
$("#sectionDetails .rev-tags .rev-ns").text(getHashParam("namespace"))
$("#sectionDetails .rev-tags .rev-cluster").text(getHashParam("context"))
if (getHashParam("context")) {
$("#sectionDetails .rev-tags .rev-cluster").text(getHashParam("context"))
} else {
$("#sectionDetails .rev-tags .rev-cluster").parent().hide() // TODO: makes UI jumpy, change to showing
}
$("#revDescr").text(elm.description).removeClass("text-danger")
if (elm.status === "failed") {
@@ -51,17 +55,17 @@ function loadContentWrapper() {
loadContent(getHashParam("tab"), getHashParam("namespace"), getHashParam("chart"), revision, revDiff, flag)
}
function loadContent(mode, namespace, name, revision, revDiff, flag) {
let qstr = "name=" + name + "&namespace=" + namespace + "&revision=" + revision
function loadContent(mode, namespace, name, revision, revDiff, userDefined) {
let qstr = "revision=" + revision
if (revDiff) {
qstr += "&revisionDiff=" + revDiff
}
if (flag) {
qstr += "&flag=" + flag
if (userDefined) {
qstr += "&userDefined=" + userDefined
}
let url = "/api/helm/charts/" + mode
let url = "/api/helm/releases/" + namespace + "/" + name + "/" + mode
url += "?" + qstr
const diffDisplay = $("#manifestText");
diffDisplay.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
@@ -143,23 +147,31 @@ $("#nav-tab [data-tab]").click(function () {
function showResources(namespace, chart, revision) {
const resBody = $("#nav-resources .body");
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
let url = "/api/helm/charts/resources"
url += "?" + qstr
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources?health=true"
$.getJSON(url).fail(function (xhr) {
reportError("Failed to get list of resources", xhr)
}).done(function (data) {
const scanners = $("body").data("scanners");
const scannableResKinds = new Set();
for (let k in scanners) {
scanners[k].SupportedResourceKinds.forEach(scannableResKinds.add, scannableResKinds)
}
resBody.empty();
data = data.sort(function (a, b) {
return interestingResources.indexOf(a.kind.toUpperCase()) - interestingResources.indexOf(b.kind.toUpperCase())
}).reverse();
for (let i = 0; i < data.length; i++) {
const res = data[i]
const resBlock = $(`
<div class="row px-3 py-2 mb-3 bg-white rounded">
<div class="col-2 res-kind text-break"></div>
<div class="col-3 res-name text-break fw-bold"></div>
<div class="col-1 res-status overflow-hidden"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span></div>
<div class="col-4 res-statusmsg"><span class="text-muted small">Getting status...</span></div>
<div class="col-2 res-actions"></div>
<div class="col-2 res-status overflow-hidden"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span></div>
<div class="col-3 res-statusmsg text-break"><span class="text-muted small">Getting status...</span></div>
<div class="col-2 res-actions"><button class='btn btn-sm ms-2 visually-hidden'>Vertical-sizer</button></div>
</div>
`)
@@ -168,25 +180,34 @@ function showResources(namespace, chart, revision) {
resBody.append(resBlock)
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
$.getJSON("/api/kube/resources/" + res.kind.toLowerCase() + "?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
//reportError("Failed to get list of resources")
}).done(function (data) {
const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase);
if (["Available", "Active", "Established"].includes(data.status.phase)) {
for (let k = 0; k < res.status.conditions.length; k++) {
if (res.status.conditions[k].type !== "hdHealth") { // it's our custom condition type
continue
}
const cond = res.status.conditions[k]
const badge = $("<span class='badge me-2 fw-normal'></span>").text(cond.reason);
if (cond.status === "Unknown") {
badge.addClass("bg-secondary text-danger")
} else if (cond.status === "Healthy") {
badge.addClass("bg-success text-dark")
} else if (["Exists"].includes(data.status.phase)) {
badge.addClass("bg-success text-dark bg-opacity-50")
} else if (["Progressing"].includes(data.status.phase)) {
} else if (cond.status === "Progressing") {
badge.addClass("bg-warning")
} else {
badge.addClass("bg-danger")
}
const statusBlock = resBlock.find(".res-status");
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 (["Exists"].includes(cond.reason)) {
badge.addClass("bg-opacity-50")
}
if (badge.text() !== "NotFound") {
const statusBlock = resBlock.find(".res-status");
statusBlock.empty().append(badge).attr("title", cond.reason)
const statusMessage = cond.message
resBlock.find(".res-statusmsg").html("<span class='text-muted small me-2'>" + (statusMessage ? statusMessage : '') + "</span>")
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
resBlock.find(".res-actions")
const btn = $("<button class=\"btn btn-sm btn-white border-secondary\">Describe</button>");
@@ -195,13 +216,19 @@ function showResources(namespace, chart, revision) {
showDescribe(ns, res.kind, res.metadata.name, badge.clone())
})
const btn2 = $("<button class='btn btn-sm btn-white border-secondary ms-2'>Scan</button>");
resBlock.find(".res-actions").append(btn2)
btn2.click(function () {
scanResource(ns, res.kind, res.metadata.name, badge.clone())
})
if (scannableResKinds.has(res.kind)) {
const btn2 = $("<button class='btn btn-sm btn-white border-secondary ms-2'>Scan</button>");
resBlock.find(".res-actions").append(btn2)
btn2.click(function () {
scanResource(ns, res.kind, res.metadata.name, badge.clone())
})
}
}
})
if (badge.hasClass("bg-danger") || badge.hasClass("bg-warning")) {
resBlock.find(".res-statusmsg").append("<a href='" + KomodorCTALink + "' class='btn btn-primary btn-sm fw-normal fs-80' target='_blank'>Troubleshoot in Komodor <i class='bi-box-arrow-up-right'></i></a>")
}
}
}
})
}
@@ -213,7 +240,7 @@ function showDescribe(ns, kind, name, badge) {
const myModal = new bootstrap.Offcanvas(document.getElementById('describeModal'));
myModal.show()
$.get("/api/kube/describe/" + kind.toLowerCase() + "?name=" + name + "&namespace=" + ns).fail(function (xhr) {
$.get("/api/k8s/" + kind.toLowerCase() + "/describe?name=" + name + "&namespace=" + ns).fail(function (xhr) {
reportError("Failed to describe resource", xhr)
}).done(function (data) {
data = hljs.highlight(data, {language: 'yaml'}).value
@@ -237,25 +264,36 @@ function scanResource(ns, kind, name, badge) {
body.append("No information from scanners. Make sure you have installed some and scanned object is supported.")
}
const tabs = $('<ul class="nav nav-tabs mt-3" role="tablist"></ul>')
const content = $('<div class="tab-content"></div>')
for (let name in data) {
const res = data[name]
if (!res.OrigReport) continue
const hdr = $("<h3>" + name + " Scan Results</h3>");
if (!res.OrigReport && !res.PassedCount) continue
const hdr = $(`<li class="nav-item" role="presentation">
<button class="nav-link" id="` + name + `-tab" data-bs-toggle="tab" data-bs-target="#` + name + `-tab-pane" type="button" role="tab">` + name + `</button>
</li>`)
if (res.FailedCount) {
hdr.append("<span class='badge bg-danger ms-3'>" + res.FailedCount + " failed</span>")
hdr.find('button').append("<span class='badge bg-danger ms-2'>" + res.FailedCount + " failed</span>")
}
if (res.PassedCount) {
hdr.append("<span class='badge bg-info ms-3'>" + res.PassedCount + " passed</span>")
hdr.find('button').append("<span class='badge bg-info ms-2'>" + res.PassedCount + " passed</span>")
}
body.append(hdr)
const hl = hljs.highlight(res.OrigReport, {language: 'yaml'}).value
const pre = $("<pre class='bg-white rounded p-3' style='font-size: inherit; overflow: unset'></pre>").html(hl)
body.append(pre)
const div = $('<div class="tab-pane fade" id="' + name + '-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0"></div>').append(pre)
tabs.append(hdr)
content.append(div)
}
body.append(tabs)
body.append(content)
tabs.find('li').first().find('button').click()
})
}

View File

@@ -5,31 +5,16 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Helm Dashboard</title>
<script src="static/datadog.js"></script>
<script src="static/analytics.js"></script>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto&family=Inter&family=Poppins:wght@600&family=Poppins:wght@500&family=Inter:wght@500&family=Roboto+Slab:wght@400&family=Roboto+Slab:wght@700&family=Roboto:wght@700&family=Roboto:wght@500"/>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/lightfair.min.css"/>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"/>
<link href="static/styles-base.css" rel="stylesheet">
<link href="static/styles.css" rel="stylesheet">
<script type="text/javascript">
window.heap = window.heap || [], heap.load = function (e, t) {
window.heap.appid = e, window.heap.config = t = t || {};
var r = document.createElement("script");
r.type = "text/javascript", r.async = !0, r.src = "https://cdn.heapanalytics.com/js/heap-" + e + ".js";
var a = document.getElementsByTagName("script")[0];
a.parentNode.insertBefore(r, a);
for (var n = function (e) {
return function () {
heap.push([e].concat(Array.prototype.slice.call(arguments, 0)))
}
}, p = ["addEventProperties", "addUserProperties", "clearEventProperties", "identify", "resetIdentity", "removeEventProperty", "setEventProperties", "track", "unsetEventProperty"], o = 0; o < p.length; o++) heap[p[o]] = n(p[o])
};
heap.load("3615793373");
</script>
</head>
<body>
@@ -37,11 +22,8 @@
<!-- TOP BAR -->
<nav class="navbar navbar-expand bg-white mb-0 p-0 b-shadow" id="topNav">
<div class="container-fluid m-0 p-0">
<div class="navbar-brand">
<a href="/"><img src="static/logo.png" alt="Logo"></a>
<div>
<h1><a href="/">Helm Dashboard</a></h1>
</div>
<div class="navbar-brand me-0">
<a href="/"><img src="static/logo-header.svg" alt="Helm Dashboard"></a>
</div>
<div class="separator-vertical mx-3"><span></span></div>
@@ -53,14 +35,49 @@
<li class="nav-item mx-2">
<a class="nav-link px-3 section-repo">Repository</a>
</li>
<li class="nav-item mx-2 dropdown">
<a class="nav-link dropdown-toggle section-help" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Help
</a>
<ul class="dropdown-menu fs-80">
<li><a class="dropdown-item"
href="https://komodorkommunity.slack.com"
target="_blank"><i class="bi-slack"></i> Support Chat</a></li>
<li><a class="dropdown-item" href="https://github.com/komodorio/helm-dashboard" target="_blank"><i
class="bi-github"></i> Project Page</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<!-- TODO: this should go under the "user menu" -->
<button class="dropdown-item" id="cacheClear"><i
class="bi-arrow-repeat"></i> Reset Cache
</button>
<li><a class="dropdown-item" href="api-docs" target="_blank"><i
class="bi-braces"></i> REST API</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item disabled" href="#">Version <span id="toolVersion"></span></a></li>
</ul>
</li>
<li class="nav-item mx-2 display-none upgrade-possible">
<a class="nav-link position-relative text-danger"
href="https://github.com/komodorio/helm-dashboard/releases" target="_blank">
Upgrade to <span id="toolVersionUpgrade"></span>
</a></li>
</ul>
<div>
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img
src="https://komodor.com/wp-content/uploads/2021/05/favicon.png" alt="komodor.io"
style="height: 1.2rem; vertical-align: text-bottom; filter: grayscale(00%);"></a>
<a class="btn me-2 text-muted" href="https://github.com/komodorio/helm-dashboard"
title="Project page on GitHub"><i class="bi-github"></i></a>
<div class="border-muted text-muted border rounded p-1 pe-2 me-3 d-flex">
<img alt="Komodor" src="https://raw.githubusercontent.com/komodorio/helm-charts/master/k8s-watcher.svg" class="me-2" style="width: 42px; height: 42px"/>
<span class="text-nowrap">
<a href="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash"
class="link text-primary fw-bold text-decoration-none" target="_blank">Upgrade your HELM experience - Free
<i class="bi-box-arrow-up-right ms-1"></i></a><br/>
Auth & RBAC, k8s events, troubleshooting and more
</span>
</div>
</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>
@@ -78,6 +95,9 @@
<button class="btn btn-sm border-secondary text-muted">
<i class="bi-plus-lg"></i> Add Repository
</button>
<div class="mt-2 p-2 small">Charts developers: you can also add local directories as chart source. Use
<span class="font-monospace text-success">--local-chart</span> CLI switch to specify it.
</div>
</div>
</div>
<div class="col-9 repo-details bg-white b-shadow pt-4 px-5 overflow-auto rounded">
@@ -88,15 +108,16 @@
<button class="btn btn-sm btn-light bg-white border border-secondary btn-remove">
<i class="bi-trash3"></i> Remove
</button>
<p class="my-3"><input class="form-control form-control-sm" type="text" placeholder="Filter..."
id="inputSearch"></p>
</div>
<div><span class="text-muted small fw-bold me-3">REPOSITORY</span></div>
<h2 class="mb-3">name-of-repo</h2>
<div class="mb-5">
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">URL: <span class="url fw-bold">http://somerepo/somepath</span></span>
</div>
<div class="py-2 mb-3 float-end">
<div class="float-end">
<!-- TODO <input class="form-control form-control-sm" type="text" placeholder="Filter..."> -->
</div>
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
style="text-transform: uppercase">
@@ -105,7 +126,7 @@
<div class="col-1">Version</div>
<div class="col-1"></div>
</div>
<ul class="list-unstyled mt-4"></ul>
<ul class="list-unstyled mt-4 charts"></ul>
</div>
</div>
@@ -114,15 +135,16 @@
<!-- FILTER BLOCK -->
<div class="p-2 ps-2 bg-white rounded-1 b-shadow" id="filters">
<form>
<h4>Clusters</h4>
<ul class="list-unstyled" id="cluster">
</ul>
<div id="clusterFilterBlock">
<h4>Clusters</h4>
<ul class="list-unstyled" id="cluster">
</ul>
</div>
<!-- TODO
<h4 class="mt-4">Namespaces</h4>
<ul class="list-unstyled" id="namespaces">
<p id="limitNamespace" class="display-none ps-3"><span class="fw-bold"></span> (forced)</p>
<ul class="list-unstyled" id="namespace">
</ul>
-->
</form>
</div>
<!-- /FILTER BLOCK -->
@@ -132,21 +154,23 @@
<!-- INSTALLED LIST -->
<div class="col ms-2" id="installedList">
<div class="col rounded rounded-1 b-shadow header">
<div class="bg-white rounded-top m-0">
<div class="bg-white rounded-top m-0 spaced-out">
<h2 class="m-0 p-1"><img class="m-2 mx-3 me-2" src="static/helm-gray.svg" alt="Installed Charts">Installed
Charts (<span></span>)</h2>
</div>
<div class="bg-secondary rounded-bottom m-0 row p-2">
<div class="col-4 hdr-name">Name</div>
<div class="col-3">Chart Status</div>
<div class="col-2">Chart</div>
<div class="col-1">Revision</div>
<div class="col-1">Namespace</div>
<div class="col-1">Updated</div>
<div class="form-outline w-25">
<input type="text" id="installedSearch" class="form-control form-control-sm"
placeholder="Filter..."/>
</div>
</div>
</div>
<div class="body"></div>
<div class="bg-white rounded shadow p-3 display-none no-charts">Looks like you don't have any charts
installed. "Repository" section may be a good place to start.
</div>
<div class="bg-white rounded shadow p-3 display-none all-filtered">There are no releases matching your
filter criteria. Reset your filters or install more charts.
</div>
</div>
<!-- /INSTALLED LIST -->
</div>
@@ -171,11 +195,19 @@
<button id="btnRollback" class="btn btn-sm btn-light bg-white border border-secondary me-2"
title="Rollback to this revision"><i class="bi-arrow-repeat"></i> <span>Rollback</span>
</button>
<button id="btnTest"
class="btn btn-sm btn-light bg-white border border-secondary me-2 display-none"
title="Run tests for this chart"><i class="bi-check-circle"></i> <span>Run tests</span>
</button>
<button id="btnUninstall" class="btn btn-sm btn-light bg-white border border-secondary"
title="Uninstall the chart"><i class="bi-trash3"></i> Uninstall
</button>
<br/>
<a class="link small" id="btnUpgradeCheck">Check for new version
<a class="link small" id="btnUpgradeCheck">
<span class="spinner-border spinner-border-sm" style="display: none" role="status"
aria-hidden="true"></span>
</a>
<a class="link small" id="btnAddRepository">
<span class="spinner-border spinner-border-sm" style="display: none" role="status"
aria-hidden="true"></span>
</a>
@@ -227,7 +259,7 @@
style="text-transform: uppercase">
<div class="col-2">Resource Type</div>
<div class="col-3">Name</div>
<div class="col-1">Status</div>
<div class="col-2">Status</div>
<div class="col-5">Status Message</div>
<div class="col-1"></div>
</div>
@@ -251,6 +283,9 @@
data-mode="diff-rev">
Diff with specific revision: <input class="form-input" size="3" id="specRev">
</label>
<label class="form-check-label" for="userDefinedVals">
<input class="form-check-input" type="checkbox" id="userDefinedVals"> User-defined only
</label>
</form>
</nav>
@@ -270,6 +305,8 @@
<hr>
<p style="white-space: pre-wrap"></p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<hr/>
<span class="small text-muted fs-80">Hint: Komodor has the same HELM capabilities, with enterprise features and support. <a href="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash" target="_blank">Sign up for free.</a></span>
</div>
<div class="offcanvas offcanvas-end rounded-start" tabindex="-1" id="describeModal"
@@ -279,8 +316,12 @@
<h5 id="describeModalLabel"></h5>
<p class="m-0 mt-4">ResourceType</p>
</div>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
<div>
<a href='https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash'
class='btn btn-primary btn-sm me-2' target='_blank'>See more details in Komodor <i
class='bi-box-arrow-up-right'></i></a>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
</div>
<div class="offcanvas-body p-2 ps-4" id="describeModalBody">
</div>
@@ -313,8 +354,26 @@
</div>
<div class="modal-body">
<form enctype="application/x-www-form-urlencoded">
<label class="form-label">Name: <input class="form-control" name="name"></label>
<label class="form-label">URL: <input class="form-control" name="url"></label>
<div class="row mb-4">
<div class="col">
<label class="form-label required">Name</label>
<input class="form-control" type="text" name="name" placeholder="Komodorio">
</div>
<div class="col">
<label class="form-label required">URL</label>
<input class="form-control" type="text" name="url" placeholder="https://helm-charts.komodor.io">
</div>
</div>
<div class="row">
<div class="col">
<label class="form-label">Username</label>
<input class="form-control" type="text" name="username">
</div>
<div class="col">
<label class="form-label">Password</label>
<input class="form-control" type="password" name="password">
</div>
</div>
</form>
</div>
<div class="modal-footer">
@@ -329,22 +388,29 @@
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="upgradeModalLabel">
Install <b class='text-success name'></b>
<span class="type"></span> <b class='text-success name'></b>
</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form class="modal-body border-bottom fs-5" enctype="multipart/form-data">
<input name="preview" type="hidden" class="preview-mode"/>
<input name="chart" type="hidden" class="chart-name"/>
<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='fw-bold text-success ver-new' name="version"></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">
Release Name: <input class="form-control rel-name" name="name">
</label>
<label class="form-label me-4 text-dark">
Namespace (optional): <input class="form-control rel-ns">
Namespace (optional):
<input type="text" class="form-control rel-ns" list="ns-datalist"/>
<datalist id="ns-datalist"></datalist>
</label>
<label class="form-label me-4 text-dark">
Cluster: <span class="form-label rel-cluster"></span>
</label>
</div>
<div class="row">
@@ -374,12 +440,48 @@
<div id="upgradeModalBody" class="small"></div>
</form>
<div class="modal-footer d-flex">
<button type="button" class="btn btn-scan bg-white border-secondary">Scan for Problems</button>
<button type="button" class="btn btn-scan bg-white border-secondary display-none">Scan for Problems
</button>
<button type="button" class="btn btn-primary btn-confirm">Confirm</button>
</div>
</div>
</div>
</div>
<div class="modal" id="testModal" 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="testModalLabel">
<span class="type">Test results</span>
</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body border-bottom fs-5">
<div class="row">
<div class="col">
<span class="test-result"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="PowerOffModal" tabindex="-1" aria-labelledby="ModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ModalLabel">Session Ended</h5>
</div>
<div class="modal-body">
The Helm Dashboard application has been shut down. You can now close the browser tab.
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
crossorigin="anonymous"></script>

View File

@@ -3,21 +3,40 @@ function loadChartsList() {
$("#sectionList").show()
const chartsCards = $("#installedList .body")
chartsCards.empty().append("<div><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span> Loading...</div>")
$.getJSON("/api/helm/charts").fail(function (xhr) {
$.getJSON("/api/helm/releases").fail(function (xhr) {
sendStats('Get releases', {'status': 'failed'});
reportError("Failed to get list of charts", xhr)
chartsCards.empty().append("<div class=\"row m-0 py-4 bg-white rounded-1 b-shadow border-4 border-start\"><div class=\"col\">Failed to get list of charts</div></div>")
}).done(function (data) {
chartsCards.empty()
$("#installedList .header h2 span").text(data.length)
chartsCards.empty().hide()
const usedNS = {}
data.forEach(function (elm) {
let card = buildChartCard(elm);
chartsCards.append(card)
usedNS[elm.namespace] = usedNS[elm.namespace] ? usedNS[elm.namespace] + 1 : 1
})
sendStats('Get releases', {'status': 'success', length: data.length});
filterInstalledList(chartsCards.find(".row"))
$("#namespace li").each(function (ix, obj) {
obj = $(obj)
const objNS = obj.find("input").val();
if (usedNS[objNS]) {
obj.find("label .text-muted").text('[' + usedNS[objNS] + ']')
obj.show()
} else {
obj.hide()
}
})
chartsCards.show()
if (!data.length) {
$("#installedList .no-charts").show()
}
})
}
function buildChartCard(elm) {
const card = $(`<div class="row m-0 py-3 bg-white rounded-1 b-shadow border-4 border-start">
<div class="col-4 rel-name"><span class="link">release-name</span><div></div></div>
const card = $(`<div class="row m-0 py-4 bg-white rounded-1 b-shadow border-4 border-start link">
<div class="col-4 rel-name"><span>release-name</span><div></div></div>
<div class="col-3 rel-status"><span></span><div></div></div>
<div class="col-2 rel-chart text-nowrap"><span></span><div>Chart Version</div></div>
<div class="col-1 rel-rev"><span>#0</span><div>Revision</div></div>
@@ -25,17 +44,32 @@ function buildChartCard(elm) {
<div class="col-1 rel-date text-nowrap"><span>today</span><div>Updated</div></div>
</div>`)
if (elm.icon) {
card.find(".rel-name").attr("style", "background-image: url(" + elm.icon + ")")
}
if (elm.description) {
card.find(".rel-name div").text(elm.description)
}
card.find(".rel-name span").text(elm.name)
card.find(".rel-rev span").text("#" + elm.revision)
card.find(".rel-ns span").text(elm.namespace)
card.find(".rel-chart span").text(elm.chart)
card.find(".rel-date span").text(getAge(elm))
card.data("namespace", elm.namespace)
card.data("name", elm.name)
card.data("chart", elm.chart)
statusStyle(elm.status, card, card.find(".rel-status span"))
card.find("a").attr("href", '#context=' + getHashParam('context') + '&namespace=' + elm.namespace + '&name=' + elm.name)
card.find(".rel-name span").data("chart", elm).click(function () {
card.data("chart", elm).click(function () {
if (window.getSelection().toString()) {
return
}
const self = $(this)
$("#sectionList").hide()
@@ -45,6 +79,66 @@ function buildChartCard(elm) {
loadChartHistory(chart.namespace, chart.name, elm.chart_name)
})
// check if upgrade is possible
$.getJSON("/api/helm/repositories/latestver?name=" + elm.chartName).fail(function (xhr) {
reportError("Failed to find chart in repo", xhr)
}).done(function (data) {
if (!data || !data.length) {
return
}
if (isNewerVersion(elm.chartVersion, data[0].version) || data[0].isSuggestedRepo) {
const icon = $("<br/><span class='fw-bold' data-bs-toggle='tooltip' data-bs-placement='bottom'></span>")
if (data[0].isSuggestedRepo) {
icon.addClass("bi-plus-circle-fill text-primary")
icon.text(" ADD REPO")
icon.attr("data-bs-title", "Add '" + data[0].repository + "' to list of known repositories")
} else {
icon.addClass("bi-arrow-up-circle-fill text-primary")
icon.text(" UPGRADE")
icon.attr("data-bs-title", "Upgrade available: " + data[0].version + " from " + data[0].repository)
}
card.find(".rel-chart div").append(icon)
const tooltipTriggerList = card.find('.rel-chart [data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
sendStats('upgradeIconShown', {'isProbable': data[0].isSuggestedRepo})
}
})
// check resource health status
$.getJSON("/api/helm/releases/" + elm.namespace + "/" + elm.name + "/resources?health=true").fail(function (xhr) {
reportError("Failed to find chart in repo", xhr)
}).done(function (data) {
for (let i = 0; i < data.length; i++) {
const res = data[i]
for (let k = 0; k < res.status.conditions.length; k++) {
if (res.status.conditions[k].type !== "hdHealth") { // it's our custom condition type
continue
}
const cond = res.status.conditions[k]
const square=$("<span class='me-1 mb-1 square rounded rounded-1' data-bs-toggle='tooltip'>&nbsp;</span>")
if (cond.status === "Healthy") {
square.addClass("bg-success")
} else if (cond.status === "Progressing") {
square.addClass("bg-warning")
} else {
square.addClass("bg-danger")
}
square.attr("data-bs-title", cond.status+" "+res.kind+" '"+res.metadata.name+"'")
card.find(".rel-status div").append(square)
}
}
const tooltipTriggerList = card.find('.rel-status [data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
})
return card;
}
$("#installedSearch").keyup(function () {
filterInstalledList($("#installedList .body .row"))
})

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="32"
height="37"
viewBox="0 0 32 37"
width="179"
height="164"
viewBox="0 0 179 164"
fill="none"
version="1.1"
id="svg4"
id="svg20"
sodipodi:docname="logo.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
@@ -13,9 +13,9 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
id="defs24" />
<sodipodi:namedview
id="namedview6"
id="namedview22"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
@@ -23,27 +23,70 @@
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="34.810811"
inkscape:cx="13.228649"
inkscape:cy="17.552019"
inkscape:zoom="3.9268293"
inkscape:cx="39.090062"
inkscape:cy="109.24845"
inkscape:window-width="3840"
inkscape:window-height="2059"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
inkscape:current-layer="svg20" />
<rect
style="opacity:0.455635;fill:#ffffff;stroke-width:18.0094;fill-opacity:1"
id="rect1031"
width="17.224331"
height="10.929513"
x="7.3572245"
y="12.500892"
ry="0" />
style="fill:#ffffff;fill-opacity:1;stroke-width:22.5327"
id="rect967"
width="113.34328"
height="68.482964"
x="32.284447"
y="47.989918" />
<path
d="M106.129 93.4776C112.242 93.4776 117.336 86.7533 117.336 78.1951C117.336 69.637 112.242 62.9127 106.129 62.9127C100.016 62.9127 94.9216 69.637 94.9216 78.1951C94.9216 86.7533 100.016 93.4776 106.129 93.4776Z"
fill="#1347FF"
id="path2" />
<path
d="M84.1221 78.1951C84.1221 86.5495 79.0279 93.4776 72.915 93.4776C66.802 93.4776 61.7078 86.7533 61.7078 78.1951C61.7078 69.8408 66.802 62.9127 72.915 62.9127C79.0279 62.9127 84.1221 69.8408 84.1221 78.1951Z"
fill="#1347FF"
id="path4" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18 5V2.29761C18 1.02617 17.1002 0 15.9958 0C14.8915 0 14 1.03576 14 2.29761V5H18ZM28.8285 11.364L30.7394 9.45308C31.6384 8.55404 31.7278 7.19222 30.9469 6.4113C30.166 5.63038 28.8032 5.73239 27.9109 6.62466L26.0001 8.53553L28.8285 11.364ZM1.62465 9.45308L3.53553 11.364L6.36396 8.53553L4.45308 6.62466C3.56081 5.73239 2.19804 5.63038 1.41712 6.4113C0.636208 7.19222 0.72561 8.55404 1.62465 9.45308ZM6 12.8164L7.78385 11H24.2163L26 12.8085V23.1915L24.2163 25H7.78372L6 23.1915V12.8164ZM8.94831 12.977L8.25128 13.6683V22.3438L8.94831 23.0351H23.0733L23.7446 22.3426V13.6694L23.0733 12.977H8.94831ZM20.1736 17.4485C20.1736 18.7679 19.4222 19.8375 18.4953 19.8375C17.5684 19.8375 16.817 18.7679 16.817 17.4485C16.817 16.1291 17.5684 15.0595 18.4953 15.0595C19.4222 15.0595 20.1736 16.1291 20.1736 17.4485ZM13.5045 19.8375C14.4314 19.8375 15.1828 18.7679 15.1828 17.4485C15.1828 16.1291 14.4314 15.0595 13.5045 15.0595C12.5776 15.0595 11.8262 16.1291 11.8262 17.4485C11.8262 18.7679 12.5776 19.8375 13.5045 19.8375ZM14.3641 34.0662V31.3638H18.3641V34.0662C18.3641 35.328 17.4726 36.3638 16.3682 36.3638C15.2638 36.3638 14.3641 35.3376 14.3641 34.0662ZM1.62465 26.9107L3.53553 24.9998L6.36396 27.8282L4.45308 29.7391C3.56081 30.6314 2.19804 30.7334 1.41712 29.9525C0.636204 29.1716 0.725608 27.8097 1.62465 26.9107ZM28.8285 24.9998L30.7394 26.9107C31.6384 27.8097 31.7278 29.1716 30.9469 29.9525C30.166 30.7334 28.8032 30.6314 27.9109 29.7391L26.0001 27.8282L28.8285 24.9998Z"
d="M22.5848 48.8529L34.607 37.2383H144.437L156.459 48.6492V114.873L144.437 126.488H34.607L22.5848 114.873V48.8529ZM42.3501 49.8717L37.6635 54.3546V109.575L42.3501 113.854H136.897L141.38 109.371V54.1508L136.897 49.668H42.3501V49.8717Z"
fill="#1347FF"
id="path2" />
id="path6" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M175.817 28.68L167.87 36.8306L155.848 25.2159L163.794 17.0653C172.353 8.09963 184.579 19.918 175.817 28.68Z"
fill="#1347FF"
id="path8" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M97.7744 9.32228V20.5294H81.0656V9.32228C81.0656 -3.10743 97.9781 -3.10743 97.7744 9.32228Z"
fill="#1347FF"
id="path10" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M81.2693 154.2V142.993H97.9781V154.2C97.9781 166.629 81.0655 166.629 81.2693 154.2Z"
fill="#1347FF"
id="path12" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.2493 17.0653L23.1961 25.2159L11.1739 36.8306L3.22708 28.68C-5.53484 19.918 6.6911 8.09963 15.2493 17.0653Z"
fill="#1347FF"
id="path14" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.0233 134.842L10.9702 126.691L22.9923 138.306L15.0455 146.457C6.48732 155.422 -5.73862 143.604 3.0233 134.842Z"
fill="#1347FF"
id="path16" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M163.591 146.457L155.644 138.306L167.666 126.691L175.613 134.842C184.375 143.604 172.149 155.422 163.591 146.457Z"
fill="#1347FF"
id="path18" />
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,754 @@
{
"openapi": "3.0.3",
"info": {
"title": "Helm Dashboard API",
"version": ""
},
"tags": [
{
"name": "Releases"
},
{
"name": "Repositories"
},
{
"name": "K8s"
},
{
"name": "Scanners"
},
{
"name": "Miscellaneous"
}
],
"paths": {
"/api/helm/releases": {
"get": {
"tags": [
"Releases"
],
"description": "Get list of installed releases",
"responses": {
"200": {
"description": "Returns list of installed releases"
}
}
}
},
"/api/helm/releases/{ns}": {
"parameters": [
{
"name": "ns",
"in": "path",
"description": "Name of kubernetes namespace, use '[emtpy]' if you want to use k8s context default"
}
],
"post": {
"tags": [
"Releases"
],
"description": "Install new release",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true
},
"chart": {
"type": "string",
"required": true
},
"version": {
"type": "string"
},
"values": {
"type": "string",
"description": "Text of values.yaml to use"
},
"preview": {
"type": "boolean"
}
}
}
}
}
},
"responses": {
"200": {
"description": "In case preview=true, the preview diff is generated",
"content": {
"text/plain": {}
}
},
"202": {
"description": "In case preview=false, the actial install is performed and resulting release object is returned",
"content": {
"application/json": {}
}
}
}
}
},
"/api/helm/releases/{ns}/{name}": {
"parameters": [
{
"name": "ns",
"in": "path",
"description": "Name of kubernetes namespace"
},
{
"name": "name",
"in": "path",
"description": "Name of Helm release"
}
],
"post": {
"tags": [
"Releases"
],
"description": "Upgrade/reconfigure existing release",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": true
},
"chart": {
"type": "string",
"required": true
},
"version": {
"type": "string"
},
"values": {
"type": "string",
"description": "Text of values.yaml to use"
},
"preview": {
"type": "boolean"
}
}
}
}
}
},
"responses": {
"200": {
"description": "In case preview=true, the preview diff is generated",
"content": {
"text/plain": {}
}
},
"202": {
"description": "In case preview=false, the actial install is performed and resulting release object is returned",
"content": {
"application/json": {}
}
}
}
}
},
"/api/helm/releases/{ns}/{name}/history": {
"parameters": [
{
"name": "ns",
"in": "path",
"description": "Name of kubernetes namespace"
},
{
"name": "name",
"in": "path",
"description": "Name of Helm release"
}
],
"get": {
"tags": [
"Releases"
],
"description": "Get revision history for release",
"responses": {
"200": {
"description": "List of release revisions"
}
}
}
},
"/api/helm/releases/{ns}/{name}/manifest": {
"parameters": [
{
"name": "ns",
"in": "path",
"description": "Name of kubernetes namespace"
},
{
"name": "name",
"in": "path",
"description": "Name of Helm release"
},
{
"name": "revision",
"in": "query",
"description": "Revision to get data from"
},
{
"name": "revisionDiff",
"in": "query",
"description": "Revision to diff against"
}
],
"get": {
"tags": [
"Releases"
],
"description": "Get manifest for release",
"responses": {
"200": {
"description": "Manifest text, or diff if revisionDiff is specified"
}
}
}
},
"/api/helm/releases/{ns}/{name}/values": {
"parameters": [
{
"name": "ns",
"in": "path",
"description": "Name of kubernetes namespace"
},
{
"name": "name",
"in": "path",
"description": "Name of Helm release"
},
{
"name": "revision",
"in": "query",
"description": "Revision to get data from"
},
{
"name": "revisionDiff",
"in": "query",
"description": "Revision to diff against"
},
{
"name": "userDefined",
"in": "query",
"description": "If set, only user-defined values will be listed"
}
],
"get": {
"tags": [
"Releases"
],
"description": "Get values for release",
"responses": {
"200": {
"description": "Values YAML text, or diff if revisionDiff is specified"
}
}
}
},
"/api/helm/releases/{ns}/{name}/notes": {
"parameters": [
{
"name": "ns",
"in": "path",
"description": "Name of kubernetes namespace"
},
{
"name": "name",
"in": "path",
"description": "Name of Helm release"
},
{
"name": "revision",
"in": "query",
"description": "Revision to get data from"
},
{
"name": "revisionDiff",
"in": "query",
"description": "Revision to diff against"
}
],
"get": {
"tags": [
"Releases"
],
"description": "Get textual notes for release",
"responses": {
"200": {
"description": "Notes text, or diff if revisionDiff is specified"
}
}
}
},
"/api/helm/releases/{ns}/{name}/resources": {
"parameters": [
{
"name": "ns",
"in": "path",
"description": "Name of kubernetes namespace"
},
{
"name": "name",
"in": "path",
"description": "Name of Helm release"
},
{
"name": "health",
"in": "query",
"description": "Flag to query k8s health status of resources"
}
],
"get": {
"tags": [
"Releases"
],
"description": "List of installed k8s resources for this release",
"responses": {
"200": {
"description": "Structured list of resources",
"content": {
"application/json": {}
}
}
}
}
},
"/api/helm/releases/{ns}/{name}/rollback": {
"parameters": [
{
"name": "ns",
"in": "path",
"description": "Name of kubernetes namespace"
},
{
"name": "name",
"in": "path",
"description": "Name of Helm release"
}
],
"post": {
"tags": [
"Releases"
],
"description": "Rollback the release to a previous revision",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"properties": {
"revision": {
"type": "integer"
}
}
}
}
}
},
"responses": {
"202": {
"description": "Rolled back successfully"
}
}
}
},
"/api/helm/releases/{ns}/{name}/test": {
"parameters": [
{
"name": "ns",
"in": "path",
"description": "Name of kubernetes namespace"
},
{
"name": "name",
"in": "path",
"description": "Name of Helm release"
}
],
"post": {
"tags": [
"Releases"
],
"description": "Run the tests on a release",
"responses": {
"200": {
"description": "Logs of a test run"
}
}
}
},
"/api/helm/repositories": {
"get": {
"tags": [
"Repositories"
],
"description": "Get list of Helm repositories",
"responses": {
"200": {
"description": "Returns list of Helm repositories"
}
}
},
"post": {
"tags": [
"Repositories"
],
"description": "Adds new repository",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string"
}
},
"required": [
"name",
"url"
]
}
}
}
},
"responses": {
"204": {
"description": "Empty response in case repository were added"
}
}
}
},
"/api/helm/repositories/{repo}": {
"parameters": [
{
"name": "repo",
"in": "path",
"description": "Name of Helm repository"
}
],
"get": {
"tags": [
"Repositories"
],
"description": "Get list of charts in repository",
"responses": {
"200": {
"description": "Returns list of charts"
}
}
},
"post": {
"tags": [
"Repositories"
],
"description": "Update repository from remote",
"responses": {
"204": {
"description": "Empty response"
}
}
},
"delete": {
"tags": [
"Repositories"
],
"description": "Remove repository",
"responses": {
"204": {
"description": "Empty response"
}
}
}
},
"/api/helm/repositories/latestver": {
"parameters": [
{
"name": "name",
"in": "query",
"description": "Name of Helm chart to search for",
"required": true
}
],
"description": "Find the latest available version of specified chart through all the repositories",
"get": {
"tags": [
"Repositories"
],
"responses": {
"200": {
"description": "The object with latest available version is returned"
},
"204": {
"description": "In case no matching repository found, the response is empty with status 204"
}
}
}
},
"/api/helm/repositories/versions": {
"parameters": [
{
"name": "name",
"in": "query",
"description": "Name of Helm chart to search for",
"required": true
}
],
"get": {
"description": "Get the list of versions for specified chart across the repositories",
"tags": [
"Repositories"
],
"responses": {
"200": {
"description": "The list if chart versions is returned"
}
}
}
},
"/api/helm/repositories/values": {
"parameters": [
{
"name": "chart",
"in": "query",
"description": "Name of Helm chart to search for, in format of <repository>/<chart-name>",
"required": true
},
{
"name": "version",
"in": "query",
"description": "Version of Helm chart to get values from",
"required": true
}
],
"get": {
"description": "Get the original values.yaml file for the chart",
"tags": [
"Repositories"
],
"responses": {
"200": {
"description": "The content of values.yaml"
}
}
}
},
"/api/k8s/contexts": {
"get": {
"tags": [
"K8s"
],
"description": "Get list of kubectl contexts configured locally",
"responses": {
"200": {
"description": "Returns list of contexts"
}
}
}
},
"/api/k8s/{kind}/get": {
"parameters": [
{
"name": "kind",
"in": "path",
"description": "Kind of kubernetes resource"
},
{
"name": "name",
"in": "query",
"description": "Name of kubernetes resource",
"required": true
},
{
"name": "namespace",
"in": "query",
"description": "Namespace of kubernetes resource",
"required": true
}
],
"get": {
"tags": [
"K8s"
],
"responses": {
"200": {
"description": "Returns resources information"
}
}
}
},
"/api/k8s/{kind}/list": {
"parameters": [
{
"name": "kind",
"in": "path",
"description": "Kind of kubernetes resource",
"schema": {
"enum": [
"namespaces"
]
}
}
],
"get": {
"tags": [
"K8s"
],
"responses": {
"200": {
"description": "Returns list of resources"
}
}
}
},
"/api/k8s/{kind}/describe": {
"parameters": [
{
"name": "kind",
"in": "path",
"description": "Kind of kubernetes resource"
},
{
"name": "name",
"in": "query",
"description": "Name of kubernetes resource",
"required": true
},
{
"name": "namespace",
"in": "query",
"description": "Namespace of kubernetes resource",
"required": true
}
],
"get": {
"tags": [
"K8s"
],
"responses": {
"200": {
"content": {
"text/plain": {}
},
"description": "Returns describe text"
}
}
}
},
"/api/scanners": {
"get": {
"tags": [
"Scanners"
],
"description": "Get list of discovered scanners",
"responses": {
"200": {
"description": "List of scanners"
}
}
}
},
"/api/scanners/manifests": {
"post": {
"tags": [
"Scanners"
],
"description": "Scan manifests using all applicable scanners",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"manifest": {
"type": "string",
"description": "Text of manifest to scan"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Map of scan results per scanner type"
}
}
}
},
"/api/scanners/resource/{kind}": {
"parameters": [
{
"in": "path",
"name": "kind"
},
{
"in": "query",
"name": "namespace",
"required": true
},
{
"in": "query",
"name": "name",
"required": true
}
],
"get": {
"tags": [
"Scanners"
],
"description": "Scan specified k8s resource in cluster",
"responses": {
"200": {
"description": "Information with scan results per scanner type"
}
}
}
},
"/": {
"delete": {
"tags": [
"Miscellaneous"
],
"description": "Shuts down the Helm Dashboard application",
"responses": {
"202": {
"description": "Shutdown command has been accepted"
}
}
}
},
"/status": {
"get": {
"tags": [
"Miscellaneous"
],
"description": "Gets application status",
"responses": {
"200": {
"description": "Returns JSON with some options",
"headers": {
"X-Application-Name": {
"description": "A string to self-identify the application"
}
}
}
}
}
}
}
}

View File

@@ -2,13 +2,22 @@ function loadRepoView() {
$("#sectionRepo .repo-details").hide()
$("#sectionRepo").show()
$.getJSON("/api/helm/repo").fail(function (xhr) {
$("#repoAddModal input[name=name]").val(getHashParam("suggestRepo"))
$("#repoAddModal input[name=url]").val(getHashParam("suggestRepoUrl"))
if (getHashParam("suggestRepo")) {
$("#sectionRepo .repo-list .btn").click()
}
$.getJSON("/api/helm/repositories").fail(function (xhr) {
reportError("Failed to get list of repositories", xhr)
sendStats('Get repo', {'status': 'fail'});
}).done(function (data) {
const items = $("#sectionRepo .repo-list ul").empty()
data.sort((a, b) => (a.name > b.name) - (a.name < b.name))
data.forEach(function (elm) {
let opt = $('<li class="mb-2"><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
let opt = $('<li class="mb-2"><label><input type="radio" name="repo" 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)
@@ -18,8 +27,9 @@ function loadRepoView() {
if (!data.length) {
items.text("No repositories found, try adding one")
}
sendStats('Get repo', {'status': 'success', length: data.length});
items.find("input").click(function () {
$("#inputSearch").val('')
const self = $(this)
const elm = self.data("item");
setHashParam("repo", elm.name)
@@ -27,8 +37,10 @@ function loadRepoView() {
$("#sectionRepo .repo-details h2").text(elm.name)
$("#sectionRepo .repo-details .url").text(elm.url)
$("#sectionRepo .btn-remove").prop("disabled", elm.url.startsWith('file://'))
$("#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) {
$.getJSON("/api/helm/repositories/" + elm.name).fail(function (xhr) {
reportError("Failed to get list of charts in repo", xhr)
}).done(function (data) {
$("#sectionRepo .repo-details ul").empty()
@@ -39,6 +51,11 @@ function loadRepoView() {
<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>`)
if (elm.icon) {
li.find("h6").prepend('<img src="' + elm.icon + '" class="me-1" style="height: 1rem"/>')
}
li.data("item", elm)
if (elm.installed_namespace) {
@@ -61,7 +78,22 @@ function loadRepoView() {
})
}
$("#inputSearch").keyup(function () {
let val = $(this).val().toLowerCase();
$(".charts li").each(function () {
let chartName = $(this.firstElementChild).text().toLowerCase()
if (chartName.indexOf(val) >= 0) {
$(this).show()
} else {
$(this).hide()
}
})
})
$("#sectionRepo .repo-list .btn").click(function () {
setHashParam("suggestRepo", null)
setHashParam("suggestRepoUrl", null)
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
myModal.show()
})
@@ -70,7 +102,7 @@ $("#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",
url: "/api/helm/repositories",
data: $("#repoAddModal form").serialize(),
}).fail(function (xhr) {
reportError("Failed to add repo", xhr)
@@ -84,7 +116,7 @@ $("#sectionRepo .btn-remove").click(function () {
if (confirm("Confirm removing repository?")) {
$.ajax({
type: 'DELETE',
url: "/api/helm/repo?name=" + $("#sectionRepo .repo-details h2").text(),
url: "/api/helm/repositories/" + $("#sectionRepo .repo-details h2").text(),
}).fail(function (xhr) {
reportError("Failed to add repo", xhr)
}).done(function () {
@@ -98,7 +130,7 @@ $("#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(),
url: "/api/helm/repositories/" + $("#sectionRepo .repo-details h2").text(),
}).fail(function (xhr) {
reportError("Failed to add repo", xhr)
}).done(function () {
@@ -115,6 +147,12 @@ function repoChartClicked() {
setHashParam("chart", elm.installed_name)
window.location.reload()
} else {
popUpUpgrade(elm)
const contexts = $("body").data("contexts")
const ctxFiltered = contexts.filter(obj => {
return obj.Name === getHashParam("context")
});
const contextNamespace = ctxFiltered.length ? ctxFiltered[0].Namespace : ""
elm.repository = $("#sectionRepo .repo-details h2").text()
popUpUpgrade(elm, contextNamespace)
}
}

View File

@@ -5,12 +5,14 @@ function loadChartHistory(namespace, name) {
$("#sectionDetails").show()
$("#sectionDetails .name").text(name)
revRow.empty().append("<li><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span></li>")
$.getJSON("/api/helm/charts/history?name=" + name + "&namespace=" + namespace).fail(function (xhr) {
$.getJSON("/api/helm/releases/" + namespace + "/" + name + "/history").fail(function (xhr) {
reportError("Failed to get chart details", xhr)
}).done(function (data) {
fillChartHistory(data, namespace, name);
checkUpgradeable(data[data.length - 1].chart_name)
checkUpgradeable(data[0].chart_name)
$("#btnTest").toggle(data[0].has_tests)
const rev = getHashParam("revision")
if (rev) {
@@ -32,7 +34,7 @@ function fillChartHistory(data, namespace, name) {
$("#specRev").val(elm.revision).data("last-rev", elm.revision).data("last-chart-ver", elm.chart_ver)
}
const rev = $(`<li class="px-2 pt-5 pb-4 mb-2 rounded border border-secondary bg-secondary position-relative">
const rev = $(`<li class="px-2 pt-5 pb-4 mb-2 rounded border border-secondary bg-secondary position-relative link">
<div class="rev-status position-absolute top-0 m-2 mb-5 start-0 fw-bold"></div>
<div class="rev-number position-absolute top-0 m-2 mb-5 end-0 fw-bold fs-6"></div>
<div class="rev-changes position-absolute bottom-0 start-0 m-2 text-muted small"></div>
@@ -48,22 +50,26 @@ function fillChartHistory(data, namespace, name) {
if (elm.description.startsWith("Rollback to ")) {
//rev.find(".rev-status").append(" <span class='small fw-normal text-lowercase'>(rollback)</span>")
rev.find(".rev-status").append(" <i class='bi-arrow-counterclockwise text-muted' title='"+elm.description+"'></i>")
rev.find(".rev-status").append(" <i class='bi-arrow-counterclockwise text-muted' title='" + elm.description + "'></i>")
}
const nxt = data[x + 1];
if (nxt && isNewerVersion(elm.chart_ver, nxt.chart_ver)) {
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='text-danger bi-arrow-down-right'></i> " + elm.chart_ver)
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='bi-arrow-down-right'></i> " + elm.chart_ver)
} else if (nxt && isNewerVersion(nxt.chart_ver, elm.chart_ver)) {
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='text-success bi-arrow-up-right'></i> " + elm.chart_ver)
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='bi-arrow-up-right'></i> " + elm.chart_ver)
}
rev.data("elm", elm)
rev.addClass("rev-" + elm.revision)
rev.click(function () {
if (window.getSelection().toString()) {
return
}
revisionClicked(namespace, name, $(this))
})
// revRow.attr("class", "link")
revRow.append(rev)
}
}

View File

@@ -1,28 +1,78 @@
$(function () {
const clusterSelect = $("#cluster");
clusterSelect.change(function () {
window.location.href = "/#context=" + clusterSelect.find("input:radio:checked").val()
window.location.reload()
})
$.getJSON("/api/kube/contexts").fail(function (xhr) {
reportError("Failed to get list of clusters", xhr)
let limNS = null
$.getJSON("/status").fail(function (xhr) { // maybe /options call in the future
reportError("Failed to get tool version", xhr)
}).done(function (data) {
const context = getHashParam("context")
fillClusterList(data, context);
$("body").data("status", data)
fillToolVersion(data)
limNS = data.LimitedToNamespace
if (limNS) {
$("#limitNamespace").show().find("span").text(limNS)
}
fillClusters(limNS)
initView(); // can only do it after loading cluster list
if (data.ClusterMode) {
$(".bi-power").hide()
$("#clusterFilterBlock").hide()
}
})
$.getJSON("/api/scanners").fail(function (xhr) {
reportError("Failed to get list of scanners", xhr)
}).done(function (data) {
if (!data.length) {
$("#upgradeModal .btn-scan").hide()
$("body").data("scanners", data)
for (let k in data) {
if (data[k].ManifestScannable) {
$("#upgradeModal .btn-scan").show() // TODO: move this to install flow
}
}
})
})
function fillClusters(limNS) {
const clusterSelect = $("#cluster");
clusterSelect.change(function () {
window.location.href = "/#context=" + clusterSelect.find("input:radio:checked").val()
window.location.reload()
})
const namespaceSelect = $("#namespace");
namespaceSelect.change(function () {
let filteredNamespaces = []
namespaceSelect.find("input:checkbox:checked").each(function () {
filteredNamespaces.push($(this).val());
})
setFilteredNamespaces(filteredNamespaces)
filterInstalledList($("#installedList .body .row"))
})
$.getJSON("/api/k8s/contexts").fail(function (xhr) {
sendStats('contexts', {'status': 'fail'});
reportError("Failed to get list of clusters", xhr)
}).done(function (data) {
$("body").data("contexts", data)
const context = getHashParam("context")
data.sort((a, b) => (getCleanClusterName(a.Name) > getCleanClusterName(b.Name)) - (getCleanClusterName(a.Name) < getCleanClusterName(b.Name)))
fillClusterList(data, context);
sendStats('contexts', {'status': 'success', length: data.length});
$.getJSON("/api/k8s/namespaces/list").fail(function (xhr) {
reportError("Failed to get namespaces", xhr)
}).done(function (res) {
const ns = res.items.map(i => i.metadata.name)
$.each(ns, function (i, item) {
$("#upgradeModal #ns-datalist").append($("<option>", {
value: item,
text: item
}))
})
if (!limNS) {
fillNamespaceList(res.items)
}
}).always(function () {
initView(); // can only do it after loading cluster and namespace lists
})
})
}
function initView() {
$(".section").hide()
@@ -44,24 +94,31 @@ function initView() {
$("#topNav ul a").click(function () {
const self = $(this)
if (self.hasClass("section-help")) {
return;
}
$("#topNav ul a").removeClass("active")
const ctx = getHashParam("context")
const filteredNamespace = getHashParam("filteredNamespace")
setHashParam(null, null)
setHashParam("context", ctx)
setHashParam("filteredNamespace", filteredNamespace)
if (self.hasClass("section-repo")) {
setHashParam("section", "repository")
} else {
} else if (self.hasClass("section-installed")) {
setHashParam("section", null)
} else {
return
}
initView()
})
const myAlert = document.getElementById('errorAlert')
myAlert.addEventListener('close.bs.alert', event => {
const errAlert = document.getElementById('errorAlert')
errAlert.addEventListener('close.bs.alert', event => {
event.preventDefault()
$("#errorAlert").hide()
})
@@ -72,6 +129,7 @@ function reportError(err, xhr) {
$("#errorAlert p").text(xhr.responseText)
}
$("#errorAlert").show()
sendStats("errorReported", {"errMessage": err})
}
@@ -116,28 +174,33 @@ function getCleanClusterName(rawClusterName) {
if (rawClusterName.indexOf('arn') === 0) {
// AWS cluster
const clusterSplit = rawClusterName.split(':')
const clusterName = clusterSplit.at(-1).split("/").at(-1)
const clusterName = clusterSplit.slice(-1)[0].replace('cluster/', '')
const region = clusterSplit.at(-3)
return region + "/" + clusterName + ' [AWS]'
}
if (rawClusterName.indexOf('gke') === 0) {
// GKE cluster
return rawClusterName.split('_').at(-2) + '/' + rawClusterName.split('_').at(-1) + ' [GKE]'
}
return rawClusterName
}
function fillClusterList(data, context) {
if (!data || !data.length) {
$("#cluster").append("No clusters listed in kubectl config, please configure some")
return
}
data.forEach(function (elm) {
let label = getCleanClusterName(elm.Name)
let opt = $('<li><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
opt.attr('title', elm.Name)
opt.find("input").val(elm.Name).text(label)
opt.find("span").text(label)
if (elm.IsCurrent && !context) {
opt.find("input").prop("checked", true)
setCurrentContext(elm.Name)
} else if (context && elm.Name === context) {
const isCurrent = elm.IsCurrent && !context;
const isSelected = context && elm.Name === context
if (isCurrent || isSelected) {
opt.find("input").prop("checked", true)
setCurrentContext(elm.Name)
}
@@ -145,6 +208,33 @@ function fillClusterList(data, context) {
})
}
function fillNamespaceList(data) {
const curContextNamespaces = $("body").data("contexts").filter(obj => {
return obj.IsCurrent
})
if (!data || !data.length) {
$("#namespace").append("default")
return
}
Array.from(data).forEach(function (elm) {
const filteredNamespace = getHashParam("filteredNamespace")
let opt = $('<li class="display-none"><label><input type="checkbox" name="namespace" class="me-2"/><span></span><span class="text-muted ms-2"></span></label></li>');
opt.attr('title', elm.metadata.name)
opt.find("input").val(elm.metadata.name).text(elm.metadata.name)
opt.find("span").text(elm.metadata.name)
if (filteredNamespace) {
if (filteredNamespace.split('+').includes(elm.metadata.name)) {
opt.find("input").prop("checked", true)
}
} else if (curContextNamespaces.length && curContextNamespaces[0].Namespace === elm.metadata.name) {
opt.find("input").prop("checked", true)
setFilteredNamespaces([elm.metadata.name])
}
$("#namespace").append(opt)
})
}
function setCurrentContext(ctx) {
setHashParam("context", ctx)
$.ajaxSetup({
@@ -181,11 +271,21 @@ $(".bi-power").click(function () {
url: "/",
type: 'DELETE',
}).done(function () {
// TODO: display explanation overlay here
$("#PowerOffModal").modal('show');
window.close();
})
})
function isNewerVersion(oldVer, newVer) {
if (oldVer && oldVer[0] === 'v') {
oldVer = oldVer.substring(1)
}
if (newVer && newVer[0] === 'v') {
newVer = newVer.substring(1)
}
const oldParts = oldVer.split('.')
const newParts = newVer.split('.')
for (let i = 0; i < newParts.length; i++) {
@@ -195,4 +295,68 @@ function isNewerVersion(oldVer, newVer) {
if (a < b) return false
}
return false
}
}
function fillToolVersion(data) {
$("#toolVersion").text(data.CurVer)
if (isNewerVersion(data.CurVer, data.LatestVer)) {
$("#toolVersionUpgrade").text(data.LatestVer)
$(".upgrade-possible").show()
}
}
$("#cacheClear").click(function () {
$.ajax({
url: "/api/cache",
type: 'DELETE',
}).done(function () {
window.location.reload()
})
})
function showHideInstalledRelease(card, filteredNamespaces, filterStr) {
let releaseNamespace = card.data("namespace")
let releaseName = card.data("name")
let chartName = card.data("chart").chart
const shownByNS = !filteredNamespaces || filteredNamespaces.split('+').includes(releaseNamespace);
const shownByStr = releaseName.indexOf(filterStr) >= 0 || chartName.indexOf(filterStr) >= 0
if (shownByNS && shownByStr) {
card.show()
return true
} else {
card.hide()
return false
}
}
function filterInstalledList(list) {
const warnMsg = $("#installedList .all-filtered").hide();
let filterStr = $("#installedSearch").val().toLowerCase();
let filteredNamespaces = getHashParam("filteredNamespace")
let anyShown = false;
let installedCount = 0;
list.each(function (ix, card) {
anyShown = showHideInstalledRelease($(card), filteredNamespaces, filterStr)
if (anyShown) {
installedCount++;
}
})
$("#installedList .header h2 span").text(installedCount)
if (list.length && !installedCount) {
warnMsg.show()
}
}
function setFilteredNamespaces(filteredNamespaces) {
if (filteredNamespaces.length === 0 && getHashParam("filteredNamespace")) {
setHashParam("filteredNamespace")
} else if (filteredNamespaces.length !== 0) {
setHashParam("filteredNamespace", filteredNamespaces.join('+'))
}
}
const KomodorCTALink="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash"

View File

@@ -0,0 +1,86 @@
.link, .nav-link {
cursor: pointer;
}
.strike {
text-decoration: line-through;
}
.text-muted {
color: #707583 !important;
}
.border-other {
border-color: #9195A1 !important;
}
.text-other {
color: #9195A1 !important;
}
.border-failed {
border-color: #FC1683 !important;
}
.text-failed {
color: #FC1683 !important;
}
.border-deployed {
border-color: #1BE99A !important;
}
.text-deployed {
color: #1FA470 !important;
}
.border-pending {
border-color: #5AB0FF !important;
}
.text-pending {
color: #5AB0FF !important;
}
.bg-tag {
background-color: #D6EFFE;
}
.bg-tag.text-dark {
color: #333333 !important;
}
.bg-danger {
background-color: #FC1683 !important;
}
.bg-success {
background-color: #A4F8D7 !important;
}
.btn {
font-weight: 500;
}
.btn-primary {
background-color: #1347FF;
}
.text-uppercase {
text-transform: uppercase;
}
.offcanvas {
width: auto !important;
max-width: 90%;
min-width: 60%;
}
.fs-80 {
font-size: 0.8rem!important;
}
.required::after {
content: " *";
color: red;
}

View File

@@ -1,81 +1,3 @@
.link, .nav-link {
cursor: pointer;
}
.strike {
text-decoration: line-through;
}
.text-muted {
color: #707583 !important;
}
.border-other {
border-color: #9195A1 !important;
}
.text-other {
color: #9195A1 !important;
}
.border-failed {
border-color: #FC1683 !important;
}
.text-failed {
color: #FC1683 !important;
}
.border-deployed {
border-color: #1BE99A !important;
}
.text-deployed {
color: #1FA470 !important;
}
.border-pending {
border-color: #5AB0FF !important;
}
.text-pending {
color: #5AB0FF !important;
}
.bg-tag {
background-color: #D6EFFE;
}
.bg-tag.text-dark {
color: #333333 !important;
}
.bg-danger {
background-color: #FC1683 !important;
}
.bg-success {
background-color: #A4F8D7 !important;
}
.btn {
font-weight: 500;
}
.btn-primary {
background-color: #1347FF;
}
.text-uppercase {
text-transform: uppercase;
}
.offcanvas {
width: auto !important;
max-width: 90%;
min-width: 60%;
}
html {
min-height: 100%;
}
@@ -104,22 +26,11 @@ body > .container-fluid {
min-height: 100% !important;
}
#topNav.navbar {
}
.navbar-brand {
font-family: Poppins, serif;
font-size: 0.6rem !important;
color: #707583;
font-weight: 500;
text-decoration: none;
}
.navbar-brand > a > img {
vertical-align: middle;
height: 2.5rem;
height: 3rem;
display: inline-block;
margin: 0.5rem 0.8rem
margin: 0.25rem 0.75rem
}
.navbar-brand > div {
@@ -156,6 +67,10 @@ body > .container-fluid {
color: #3B3D45 !important;
}
#topNav .nav-link.text-danger {
color: #FC1683 !important;
}
#topNav .nav-link.active {
background: #EBEFFF;
border-radius: 2px;
@@ -174,10 +89,11 @@ body > .container-fluid {
#filters {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 0.8rem;
line-height: 175%;
inline-size: auto;
overflow-wrap: break-word;
}
#cluster input, #cluster span {
@@ -188,6 +104,13 @@ body > .container-fluid {
margin-bottom: 0.75rem !important;
}
#installedList .header .spaced-out {
display: flex;
justify-content: space-between;
align-items: center;
padding-right: 0.5rem;
}
#installedList h2 {
font-family: Inter, serif;
font-weight: 700;
@@ -234,6 +157,10 @@ span.link {
text-decoration: underline;
}
#installedList .body .row div {
overflow: hidden;
}
#installedList .rel-name {
padding-left: 5.5rem;
background-image: url("helm-gray-50.svg");
@@ -243,6 +170,10 @@ span.link {
background-size: 3rem;
}
#installedList .rel-name div {
overflow: hidden;
}
#installedList .rel-name span {
font-family: Roboto Slab, sans-serif;
font-weight: 700;
@@ -288,23 +219,23 @@ span.link {
position: static;
}
.nav-tabs {
nav .nav-tabs {
border: none;
margin-bottom: 1rem;
}
.nav-tabs .nav-link {
nav .nav-tabs .nav-link {
padding-bottom: 0.25rem;
color: #3B3D45;
}
.nav-tabs .nav-link.active {
nav .nav-tabs .nav-link.active {
border: none;
border-bottom: 3px solid #3B3D45;
background-color: transparent;
}
#installedList .b-shadow:hover {
#installedList .body .b-shadow:hover {
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.18);
}
@@ -365,4 +296,31 @@ span.link {
#sectionRepo .repo-details ul .row:hover .btn {
visibility: visible;
}
.test-result {
font-size: 1rem;
}
.square {
width: 0.55rem;
height: 0.55rem;
display: inline-block;
border-radius: 0.1rem!important;
}
.square.bg-danger {
background-color: #ff0072!important;
}
.square.bg-warning {
background-color: #ffa800!important;
}
.square.bg-success {
background-color: #00c2ab!important;
}
.hljs {
overflow-x: inherit;
}

View File

@@ -1,481 +0,0 @@
package subproc
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/release"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
type DataLayer struct {
KubeContext string
Helm string
Kubectl string
Scanners []Scanner
}
func (d *DataLayer) runCommand(cmd ...string) (string, error) {
log.Debugf("Starting command: %s", cmd)
return utils.RunCommand(cmd, map[string]string{"HELM_KUBECONTEXT": d.KubeContext})
}
func (d *DataLayer) runCommandHelm(cmd ...string) (string, error) {
if d.Helm == "" {
d.Helm = "helm"
}
cmd = append([]string{d.Helm}, cmd...)
if d.KubeContext != "" {
cmd = append(cmd, "--kube-context", d.KubeContext)
}
return d.runCommand(cmd...)
}
func (d *DataLayer) runCommandKubectl(cmd ...string) (string, error) {
// TODO: migrate into using kubectl "k8s.io/kubectl/pkg/cmd" and kube API
if d.Kubectl == "" {
d.Kubectl = "kubectl"
}
cmd = append([]string{d.Kubectl}, cmd...)
if d.KubeContext != "" {
cmd = append(cmd, "--context", d.KubeContext)
}
return d.runCommand(cmd...)
}
func (d *DataLayer) CheckConnectivity() error {
contexts, err := d.ListContexts()
if err != nil {
return err
}
if len(contexts) < 1 {
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
if err != nil {
return err
}
return nil
}
type KubeContext struct {
IsCurrent bool
Name string
Cluster string
AuthInfo string
Namespace string
}
func (d *DataLayer) ListContexts() (res []KubeContext, err error) {
out, err := d.runCommandKubectl("config", "get-contexts")
if err != nil {
return nil, err
}
// kubectl has no JSON output for it, we'll have to do custom text parsing
lines := strings.Split(out, "\n")
// find field positions
fields := regexp.MustCompile(`(\w+\s+)`).FindAllString(lines[0], -1)
cur := len(fields[0])
name := cur + len(fields[1])
cluster := name + len(fields[2])
auth := cluster + len(fields[3])
// read items
for _, line := range lines[1:] {
if strings.TrimSpace(line) == "" {
continue
}
res = append(res, KubeContext{
IsCurrent: strings.TrimSpace(line[0:cur]) == "*",
Name: strings.TrimSpace(line[cur:name]),
Cluster: strings.TrimSpace(line[name:cluster]),
AuthInfo: strings.TrimSpace(line[cluster:auth]),
Namespace: strings.TrimSpace(line[auth:]),
})
}
return res, nil
}
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)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
return res, nil
}
func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*HistoryElement, err error) {
// TODO: there is `max` but there is no `offset`
out, err := d.runCommandHelm("history", chartName, "--namespace", namespace, "--output", "json", "--max", "18")
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
for _, elm := range res {
chartRepoName, curVer, err := utils.ChartAndVersion(elm.Chart)
if err != nil {
return nil, err
}
elm.ChartName = chartRepoName
elm.ChartVer = curVer
elm.Updated.Time = elm.Updated.Time.Round(time.Second)
}
return res, nil
}
func (d *DataLayer) ChartRepoVersions(chartName string) (res []*RepoChartElement, err error) {
search := "/" + chartName + "\v"
if strings.Contains(chartName, "/") {
search = "\v" + chartName + "\v"
}
cmd := []string{"search", "repo", "--regexp", search, "--versions", "--output", "json"}
out, err := d.runCommandHelm(cmd...)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
return res, nil
}
func (d *DataLayer) ChartRepoCharts(repoName string) (res []*RepoChartElement, err error) {
cmd := []string{"search", "repo", "--regexp", "\v" + repoName + "/", "--output", "json"}
out, err := d.runCommandHelm(cmd...)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
ins, err := d.ListInstalled()
if err != nil {
return nil, err
}
enrichRepoChartsWithInstalled(res, ins)
return res, nil
}
func enrichRepoChartsWithInstalled(charts []*RepoChartElement, installed []ReleaseElement) {
for _, chart := range charts {
for _, rel := range installed {
c, _, err := utils.ChartAndVersion(rel.Chart)
if err != nil {
log.Warnf("Failed to parse chart: %s", err)
continue
}
pieces := strings.Split(chart.Name, "/")
if pieces[1] == c {
chart.InstalledNamespace = rel.Namespace
chart.InstalledName = rel.Name
}
}
}
}
type SectionFn = func(string, string, int, bool) (string, error) // TODO: rework it into struct-based argument?
func (d *DataLayer) RevisionManifests(namespace string, chartName string, revision int, _ bool) (res string, err error) {
cmd := []string{"get", "manifest", chartName, "--namespace", namespace}
if revision > 0 {
cmd = append(cmd, "--revision", strconv.Itoa(revision))
}
out, err := d.runCommandHelm(cmd...)
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string, revision int) ([]*v1.Carp, error) {
out, err := d.RevisionManifests(namespace, chartName, revision, false)
if err != nil {
return nil, err
}
dec := yaml.NewDecoder(bytes.NewReader([]byte(out)))
res := make([]*v1.Carp, 0)
var tmp interface{}
for dec.Decode(&tmp) == nil {
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
// bug we can juggle it
jsoned, err := json.Marshal(tmp)
if err != nil {
return nil, err
}
var doc v1.Carp
err = json.Unmarshal(jsoned, &doc)
if err != nil {
return nil, err
}
if doc.Kind == "" {
log.Warnf("Manifest piece is not k8s resource: %s", jsoned)
continue
}
res = append(res, &doc)
}
return res, nil
}
func (d *DataLayer) RevisionNotes(namespace string, chartName string, revision int, _ bool) (res string, err error) {
out, err := d.runCommandHelm("get", "notes", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision))
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) RevisionValues(namespace string, chartName string, revision int, onlyUserDefined bool) (res string, err error) {
cmd := []string{"get", "values", chartName, "--namespace", namespace, "--output", "yaml"}
if revision > 0 {
cmd = append(cmd, "--revision", strconv.Itoa(revision))
}
if !onlyUserDefined {
cmd = append(cmd, "--all")
}
out, err := d.runCommandHelm(cmd...)
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) GetResource(namespace string, def *v1.Carp) (*v1.Carp, error) {
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "json")
if err != nil {
if strings.HasSuffix(strings.TrimSpace(err.Error()), " not found") {
return &v1.Carp{
Status: v1.CarpStatus{
Phase: "NotFound",
Message: err.Error(),
Reason: "not found",
},
}, nil
} else {
return nil, err
}
}
var res v1.Carp
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
sort.Slice(res.Status.Conditions, func(i, j int) bool {
// some condition types always bubble up
if res.Status.Conditions[i].Type == "Available" {
return false
}
if res.Status.Conditions[j].Type == "Available" {
return true
}
t1 := res.Status.Conditions[i].LastTransitionTime
t2 := res.Status.Conditions[j].LastTransitionTime
return t1.Time.Before(t2.Time)
})
return &res, nil
}
func (d *DataLayer) GetResourceYAML(namespace string, def *v1.Carp) (string, error) {
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "yaml")
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) DescribeResource(namespace string, kind string, name string) (string, error) {
out, err := d.runCommandKubectl("describe", strings.ToLower(kind), name, "--namespace", namespace)
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) ChartUninstall(namespace string, name string) error {
_, err := d.runCommandHelm("uninstall", name, "--namespace", namespace)
if err != nil {
return err
}
return nil
}
func (d *DataLayer) Revert(namespace string, name string, rev int) error {
_, err := d.runCommandHelm("rollback", name, strconv.Itoa(rev), "--namespace", namespace)
if err != nil {
return err
}
return nil
}
func (d *DataLayer) ChartRepoUpdate(name string) error {
cmd := []string{"repo", "update"}
if name != "" {
cmd = append(cmd, name)
}
_, err := d.runCommandHelm(cmd...)
if err != nil {
return err
}
return nil
}
func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string, version string, justTemplate bool, values string, reuseVals bool) (string, error) {
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 {
return "", err
}
cmd := []string{"upgrade", "--install", "--create-namespace", name, repoChart, "--version", version, "--namespace", namespace, "--values", valsFile, "--output", "json"}
if justTemplate {
cmd = append(cmd, "--dry-run")
}
out, err := d.runCommandHelm(cmd...)
if err != nil {
return "", err
}
res := release.Release{}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return "", err
}
if justTemplate {
out = strings.TrimSpace(res.Manifest)
}
return out, nil
}
func (d *DataLayer) ShowValues(chart string, ver string) (string, error) {
return d.runCommandHelm("show", "values", chart, "--version", ver)
}
func (d *DataLayer) ChartRepoList() (res []RepositoryElement, err error) {
out, err := d.runCommandHelm("repo", "list", "--output", "json")
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
return res, nil
}
func (d *DataLayer) ChartRepoAdd(name string, url string) (string, error) {
out, err := d.runCommandHelm("repo", "add", "--force-update", name, url)
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) ChartRepoDelete(name string) (string, error) {
out, err := d.runCommandHelm("repo", "remove", name)
if err != nil {
return "", err
}
return out, nil
}
func RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) {
if revision1 == 0 || revision2 == 0 {
log.Debugf("One of revisions is zero: %d %d", revision1, revision2)
return "", nil
}
manifest1, err := functor(namespace, name, revision1, flag)
if err != nil {
return "", err
}
manifest2, err := functor(namespace, name, revision2, flag)
if err != nil {
return "", err
}
diff := GetDiff(manifest1, manifest2, strconv.Itoa(revision1)+ext, strconv.Itoa(revision2)+ext)
return diff, nil
}
func GetDiff(text1 string, text2 string, name1 string, name2 string) string {
edits := myers.ComputeEdits(span.URIFromPath(""), text1, text2)
unified := gotextdiff.ToUnified(name1, name2, text1, edits)
diff := fmt.Sprint(unified)
log.Debugf("The diff is: %s", diff)
return diff
}

View File

@@ -1,88 +0,0 @@
package subproc
import (
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
"helm.sh/helm/v3/pkg/release"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"sync"
"testing"
)
func TestFlow(t *testing.T) {
log.SetLevel(log.DebugLevel)
var _ release.Status
data := DataLayer{}
err := data.CheckConnectivity()
if err != nil {
if err.Error() == "did not find any kubectl contexts configured" {
t.Skip()
} else {
t.Fatal(err)
}
}
ctxses, err := data.ListContexts()
if err != nil {
t.Fatal(err)
}
for _, ctx := range ctxses {
if ctx.IsCurrent {
data.KubeContext = ctx.Name
}
}
installed, err := data.ListInstalled()
if err != nil {
t.Fatal(err)
}
chart := installed[1]
history, err := data.ChartHistory(chart.Namespace, chart.Name)
if err != nil {
t.Fatal(err)
}
_ = history
chartRepoName, curVer, err := utils.ChartAndVersion(chart.Chart)
if err != nil {
t.Fatal(err)
}
_ = curVer
upgrade, err := data.ChartRepoVersions(chartRepoName)
if err != nil {
t.Fatal(err)
}
_ = upgrade
manifests, err := data.RevisionManifestsParsed(chart.Namespace, chart.Name, history[len(history)-1].Revision)
if err != nil {
t.Fatal(err)
}
_ = manifests
var wg sync.WaitGroup
res := make([]*v1.Carp, 0)
for _, m := range manifests {
wg.Add(1)
mc := m // fix the clojure
func() {
defer wg.Done()
lst, err := data.GetResource(chart.Namespace, mc)
if err != nil {
t.Fatal(err)
}
res = append(res, lst)
}()
}
wg.Wait()
diff, err := RevisionDiff(data.RevisionManifests, ".yaml", chart.Namespace, chart.Name, history[len(history)-1].Revision, history[len(history)-2].Revision, true)
if err != nil {
t.Fatal(err)
}
_ = diff
}

View File

@@ -1,44 +0,0 @@
package subproc
import (
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
)
// unpleasant copy from Helm sources, where they have it non-public
type ReleaseElement struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Revision string `json:"revision"`
Updated helmtime.Time `json:"updated"`
Status release.Status `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
}
type HistoryElement struct {
Revision int `json:"revision"`
Updated helmtime.Time `json:"updated"`
Status release.Status `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
ChartName string `json:"chart_name"`
ChartVer string `json:"chart_ver"`
}
type RepoChartElement struct {
Name string `json:"name"`
Version string `json:"version"`
AppVersion string `json:"app_version"`
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"`
}

View File

@@ -5,6 +5,8 @@ type Scanner interface {
Test() bool // test if the scanner is available
ScanManifests(mnf string) (*ScanResults, error) // run the scanner on manifests
ScanResource(ns string, kind string, name string) (*ScanResults, error) // run the scanner on k8s resource
SupportedResourceKinds() []string
ManifestIsScannable() bool
}
type ScanResults struct {

View File

@@ -3,38 +3,47 @@ package utils
import (
"bytes"
"errors"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"io/ioutil"
"os"
"os/exec"
"strconv"
"regexp"
"strings"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
var FailLogLevel = log.WarnLevel // allows to suppress error logging in some situations
type ControlChan = chan struct{}
func ChartAndVersion(x string) (string, string, error) {
lastInd := strings.LastIndex(x, "-")
if lastInd < 0 {
strs := strings.Split(x, "-")
lens := len(strs)
if lens < 2 {
return "", "", errors.New("can't parse chart version string")
} else if lens == 2 {
return strs[0], strs[1], nil
} else {
// semver2 regex , add optional v prefix
re := regexp.MustCompile(`v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?`)
match := re.FindString(x)
lastInd := strings.LastIndex(x, match)
return x[:lastInd-1], match, nil
}
return x[:lastInd], x[lastInd+1:], nil
}
func TempFile(txt string) (string, func(), error) {
file, err := ioutil.TempFile("", "helm_dahsboard_*.yaml")
file, err := os.CreateTemp("", "helm_dahsboard_*.yaml")
if err != nil {
return "", nil, err
}
err = ioutil.WriteFile(file.Name(), []byte(txt), 0600)
err = os.WriteFile(file.Name(), []byte(txt), 0600)
if err != nil {
return "", nil, err
}
return file.Name(), func() { os.Remove(file.Name()) }, nil
return file.Name(), func() { _ = os.Remove(file.Name()) }, nil
}
type CmdError struct {
@@ -49,6 +58,7 @@ func (e CmdError) Error() string {
}
func RunCommand(cmd []string, env map[string]string) (string, error) {
log.Debugf("Starting command: %s", cmd)
prog := exec.Command(cmd[0], cmd[1:]...)
prog.Env = os.Environ()
@@ -63,10 +73,10 @@ func RunCommand(cmd []string, env map[string]string) (string, error) {
prog.Stderr = &stderr
if err := prog.Run(); err != nil {
log.Warnf("Failed command: %s", cmd)
log.StandardLogger().Logf(FailLogLevel, "Failed command: %s", cmd)
serr := stderr.Bytes()
if serr != nil {
log.Warnf("STDERR:\n%s", serr)
log.StandardLogger().Logf(FailLogLevel, "STDERR:\n%s", serr)
}
if eerr, ok := err.(*exec.ExitError); ok {
return "", CmdError{
@@ -93,10 +103,9 @@ func RunCommand(cmd []string, env map[string]string) (string, error) {
type QueryProps struct {
Namespace string
Name string
Revision int
}
func GetQueryProps(c *gin.Context, revRequired bool) (*QueryProps, error) {
func GetQueryProps(c *gin.Context) (*QueryProps, error) {
qp := QueryProps{}
qp.Namespace = c.Query("namespace")
@@ -105,11 +114,5 @@ func GetQueryProps(c *gin.Context, revRequired bool) (*QueryProps, error) {
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
}

Some files were not shown because too many files have changed in this diff Show More