84 Commits

Author SHA1 Message Date
ronahk
cb535b1308 add needs 2022-12-18 15:00:52 +02:00
ronahk
c6b1586e88 fix pipeline 2022-12-18 14:40:09 +02:00
ronahk
bae6650263 fix pipeline 2022-12-18 14:27:12 +02:00
ronahk
d0836eee0e update plugin version 2022-12-18 14:25:43 +02: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
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
59 changed files with 2864 additions and 1285 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
Dockerfile
*.md
bin
.idea

View File

@@ -2,9 +2,9 @@ name: Build
on:
push:
branches: [ "main" ]
branches: main
pull_request:
branches: [ "main" ]
branches: main
jobs:
build:
@@ -40,4 +40,31 @@ jobs:
# 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@v2
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
outputs: local
build-args: VER=0.0.0-dev
helm_check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
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

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@v2
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

@@ -3,12 +3,41 @@ name: release
on:
push:
tags:
- "*"
- "v*"
env:
HELM_REP: helm-charts
GH_OWNER: komodorio
CHART_DIR: charts/helm-dashboard
jobs:
release:
pre_release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get plugin version
id: get_plugin_version
run: echo "PLUGIN_VERSION=$(cat plugin.yaml | grep "version" | cut -d '"' -f 2)" >> $GITHUB_OUTPUT
- name: Get tag name
id: get_tag_name
run: echo "TAG_NAME=$(echo ${{ github.ref_name }} | cut -d 'v' -f2)" >> $GITHUB_OUTPUT
outputs:
plugin_version: ${{ steps.get_plugin_version.outputs.PLUGIN_VERSION }}
release_tag: ${{ steps.get_tag_name.outputs.TAG_NAME }}
release:
needs: pre_release
runs-on: ubuntu-latest
steps:
- name: Plugin version/Tag name Check
if: needs.pre_release.outputs.release_tag != needs.pre_release.outputs.plugin_version
uses: actions/github-script@v3
with:
script: |
core.setFailed('Plugin version and tag name are not equivalent!')
- name: Checkout
uses: actions/checkout@v3
with:
@@ -28,3 +57,68 @@ 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@v2
- name: Docker meta
uses: docker/metadata-action@v3
id: meta
with:
images: komodorio/helm-dashboard
- name: Login to DockerHub
uses: docker/login-action@v1
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_PASS }}
- name: Build and push
uses: docker/build-push-action@v2
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 }}
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@v2
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): ${{ github.event.head_commit.message }}" #important!! don't change this commit message unless you change the condition in pipeline.yml on helm-charts repo

3
.gitignore vendored
View File

@@ -24,3 +24,6 @@ go.work
/bin
/.idea/
.DS_Store
.vscode/

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

41
Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
# Stage - builder
FROM golang as builder
ARG VER
ENV GOOS=linux
ENV GOARCH=amd64
ENV CGO_ENABLED=0
ENV VERSION=0.0.0
WORKDIR /build
COPY go.mod ./
COPY go.sum ./
COPY main.go ./
RUN go mod download
ADD . src
WORKDIR /build/src
RUN make build
# Stage - runner
FROM alpine/helm
RUN curl -o /bin/kubectl -vf -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && chmod +x /bin/kubectl && kubectl --help
# Checkov scanner
RUN apk add --update --no-cache python3
RUN python3 -m ensurepip
RUN pip3 install checkov
# Trivy
RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.18.3
COPY --from=builder /build/src/bin/dashboard /bin/helm-dashboard
ENTRYPOINT ["/bin/helm-dashboard", "--no-browser", "--bind=0.0.0.0"]
# docker build . -t komodorio/helm-dashboard:0.0.0 && kind load docker-image komodorio/helm-dashboard:0.0.0

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

View File

@@ -1,4 +1,4 @@
# <img src="pkg/dashboard/static/logo.png" height=30 style="height: 2rem"> Helm Dashboard
# ![Helm Dashboard](pkg/dashboard/static/logo-header.svg#gh-light-mode-only) ![Helm Dashboard](pkg/dashboard/static/logo-header-inverted.svg#gh-dark-mode-only)
A simplified way of working with Helm.
@@ -6,12 +6,12 @@ A simplified way of working with Helm.
## What it Does?
The _Helm Dashboard_ plugin offers a UI-driven way to view the installed Helm charts, see their revision history and
_Helm Dashboard_ 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 offical project by the [helm team](https://helm.sh/).
Some of the key capabilities of the tool:
@@ -22,9 +22,11 @@ Some of the key capabilities of the tool:
- Integration with popular problem scanners
- Easy switch between multiple clusters
## Installing
## Setup
To install it, simply run Helm command:
### Using Helm plugin manager
To install the plugin, simply run Helm command:
```shell
helm plugin install https://github.com/komodorio/helm-dashboard.git
@@ -42,9 +44,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,14 +55,31 @@ 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.
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)
### Manual Installation
Download the appropriate [release package](https://github.com/komodorio/helm-dashboard/releases) for your platform, unpack it and just run `dashboard` binary from it.
## Scanner Integrations
@@ -80,10 +97,9 @@ button at the bottom of the dialog:
## Support Channels
We have two main channels for supporting the Helm Dashboard
users: [Slack community](https://komodorkommunity.slack.com/archives/C044U1B0265) for general conversations
users: [Slack community](https://join.slack.com/t/komodorkommunity/shared_invite/zt-1dm3cnkue-ov1Yh~_95teA35QNx5yuMg) for general conversations
and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
## Local Dev Testing
Prerequisites: `helm` and `kubectl` binaries installed and operational.

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://github.com/komodorio/helm-dashboard/blob/main/pkg/dashboard/static/logo.png"
version: 0.1.0
appVersion: "0.0.0"

View File

@@ -0,0 +1,76 @@
# Helm Dashboard
## TL;DR;
```bash
helm repo add komodorio https://helm-charts.komodor.io
helm repo update
helm upgrade --install my-release 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.
## Prerequisites
- Kubernetes 1.16+
- Helm 3+
## Installing the Chart
To install the chart with the release name `my-release`:
```bash
helm install my-release .
```
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 `my-release` deployment:
```bash
helm uninstall my-release
```
The command removes all the Kubernetes components associated with the chart and deletes the release.
## 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. | `false` |
| `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) | `""` |
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`.
```bash
helm upgrade --install my-release 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,79 @@
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 }}
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 }}
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
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,48 @@
{{- 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: ""
{{- 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:
- {{ .Values.dashboard.persistence.accessMode | quote }}
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,40 @@
{{- 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: ["", "apps", "rbac.authorization.k8s.io", "rbac", "batch", "extensions", "networking.k8s.io", "storage.k8s.io"]
resources: ["*"]
verbs: ["get", "list", "watch"]
{{- if .Values.dashboard.allowWriteActions }}
- apiGroups: ["", "apps", "rbac.authorization.k8s.io", "rbac", "batch", "extensions", "networking.k8s.io", "storage.k8s.io"]
resources: ["*"]
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
{{- 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,95 @@
replicaCount: 1
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: false
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: ""
## 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
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: {}
tolerations: []
affinity: {}

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

@@ -0,0 +1,14 @@
#!/bin/bash
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
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

114
go.mod
View File

@@ -3,122 +3,68 @@ module github.com/komodorio/helm-dashboard
go 1.18
require (
github.com/aquasecurity/trivy v0.32.1
github.com/eko/gocache/v3 v3.1.1
github.com/gin-gonic/gin v1.8.1
github.com/hashicorp/go-version v1.6.0
github.com/hexops/gotextdiff v1.0.3
github.com/jessevdk/go-flags v1.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/sirupsen/logrus v1.9.0
github.com/toqueteos/webbrowser v1.2.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.9.4
k8s.io/apimachinery v0.25.0-alpha.2
helm.sh/helm/v3 v3.10.3
k8s.io/apimachinery v0.25.2
)
require (
github.com/CycloneDX/cyclonedx-go v0.6.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
github.com/Sirupsen/logrus v1.0.6 // indirect
github.com/aquasecurity/go-dep-parser v0.0.0-20220928105313-d3a51fe400e4 // indirect
github.com/aquasecurity/table v1.8.0 // indirect
github.com/aquasecurity/trivy-db v0.0.0-20220627104749-930461748b63 // indirect
github.com/aquasecurity/trivy-kubernetes v0.3.1-0.20220823151349-b90b48958b91 // indirect
github.com/caarlos0/env/v6 v6.10.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/libnetwork v0.5.6 // indirect
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-containerregistry v0.11.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/liamg/tml v0.6.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/owenrumney/go-sarif/v2 v2.1.2 // indirect
github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a // indirect
github.com/pegasus-kv/thrift v0.13.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.3 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/samber/lo v1.27.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spdx/tools-golang v0.3.0 // indirect
github.com/prometheus/client_golang v1.12.2 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.33.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.5.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.4.0 // indirect
github.com/stretchr/testify v1.8.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/vishvananda/netlink v1.1.0 // indirect
github.com/vishvananda/netns v0.0.0-20220913150850-18c4f4234207 // indirect
github.com/xlab/treeprint v1.1.0 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf // indirect
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/api v0.25.0-alpha.2 // indirect
k8s.io/cli-runtime v0.24.4 // indirect
k8s.io/client-go v0.25.0-alpha.2 // indirect
k8s.io/klog/v2 v2.70.0 // indirect
k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/kustomize/api v0.11.4 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
k8s.io/klog/v2 v2.70.1 // indirect
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
)

746
go.sum

File diff suppressed because it is too large Load Diff

100
main.go
View File

@@ -1,50 +1,110 @@
package main
import (
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard"
log "github.com/sirupsen/logrus"
"github.com/toqueteos/webbrowser"
"fmt"
"os"
"github.com/gin-gonic/gin"
"github.com/jessevdk/go-flags"
"github.com/komodorio/helm-dashboard/pkg/dashboard"
"github.com/pkg/browser"
log "github.com/sirupsen/logrus"
)
var (
version = "dev"
version = "0.0.0"
commit = "none"
date = "unknown"
)
func main() {
setupLogging()
type options struct {
Version bool `long:"version" description:"Show tool version"`
Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"`
NoBrowser bool `short:"b" long:"no-browser" description:"Do not attempt to open Web browser upon start"`
NoTracking bool `long:"no-analytics" description:"Disable user analytics (GA, DataDog etc.)"`
BindHost string `long:"bind" description:"Host binding to start server (default: localhost)"` // default should be printed but not assigned as the precedence: flag > env > default
Port uint `short:"p" long:"port" description:"Port to start server on" default:"8080"` // TODO: better default port to clash less?
Namespace string `short:"n" long:"namespace" description:"Limit operations to a specific namespace"`
}
// TODO: proper command-line parsing
if len(os.Args) > 1 { // dirty thing to allow --help to work
os.Exit(0)
func main() {
opts := parseFlags()
if opts.BindHost == "" {
host := os.Getenv("HD_BIND")
if host == "" {
host = "localhost"
}
opts.BindHost = host
}
address, webServerDone := dashboard.StartServer(version)
opts.Verbose = opts.Verbose || os.Getenv("DEBUG") != ""
setupLogging(opts.Verbose)
if os.Getenv("HD_NOBROWSER") == "" {
server := dashboard.Server{
Version: version,
Namespace: opts.Namespace,
Address: fmt.Sprintf("%s:%d", opts.BindHost, opts.Port),
Debug: opts.Verbose,
NoTracking: opts.NoTracking,
}
address, webServerDone := server.StartServer()
if !opts.NoTracking {
log.Infof("User analytics is collected to improve the quality, disable it with --no-analytics")
}
if opts.NoBrowser {
log.Infof("Access web UI at: %s", address)
} else {
log.Infof("Opening web UI: %s", address)
err := webbrowser.Open(address)
err := browser.OpenURL(address)
if err != nil {
log.Warnf("Failed to open Web browser for URL: %s", err)
}
} else {
log.Infof("Access web UI at: %s", address)
}
<-webServerDone
log.Infof("Done.")
}
func setupLogging() {
if os.Getenv("DEBUG") == "" {
log.SetLevel(log.InfoLevel)
gin.SetMode(gin.ReleaseMode)
} else {
func parseFlags() options {
ns := os.Getenv("HELM_NAMESPACE")
if ns == "default" {
ns = ""
}
opts := options{Namespace: ns}
args, err := flags.Parse(&opts)
if err != nil {
if e, ok := err.(*flags.Error); ok {
if e.Type == flags.ErrHelp {
os.Exit(0)
}
}
// we rely on default behavior to print the problem inside `flags` library
os.Exit(1)
}
if opts.Version {
fmt.Println(version)
os.Exit(0)
}
if len(args) > 0 {
panic("The program does not take argumants, see --help for usage")
}
return opts
}
func setupLogging(verbose bool) {
if verbose {
log.SetLevel(log.DebugLevel)
gin.SetMode(gin.DebugMode)
log.Debugf("Debug logging is enabled")
} else {
log.SetLevel(log.InfoLevel)
gin.SetMode(gin.ReleaseMode)
}
log.Infof("Helm Dashboard by Komodor, version %s (%s @ %s)", version, commit, date)
}

View File

@@ -2,14 +2,15 @@ package dashboard
import (
"embed"
"net/http"
"os"
"path"
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard/handlers"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
"net/http"
"os"
"path"
)
//go:embed static/*
@@ -36,17 +37,24 @@ func errorHandler(c *gin.Context) {
func contextSetter(data *subproc.DataLayer) gin.HandlerFunc {
return func(c *gin.Context) {
if context, ok := c.Request.Header["X-Kubecontext"]; ok {
log.Debugf("Setting current context to: %s", context)
data.KubeContext = context[0]
if ctx, ok := c.Request.Header["X-Kubecontext"]; ok {
log.Debugf("Setting current context to: %s", ctx)
if data.KubeContext != ctx[0] {
err := data.Cache.Clear()
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
}
data.KubeContext = ctx[0]
}
c.Next()
}
}
func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, version string) *gin.Engine {
func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, debug bool) *gin.Engine {
var api *gin.Engine
if os.Getenv("DEBUG") == "" {
if debug {
api = gin.New()
api.Use(gin.Recovery())
} else {
@@ -58,12 +66,12 @@ func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, version stri
api.Use(errorHandler)
configureStatic(api)
configureRoutes(abortWeb, data, api, version)
configureRoutes(abortWeb, data, api)
return api
}
func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *gin.Engine, version string) {
func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *gin.Engine) {
// server shutdown handler
api.DELETE("/", func(c *gin.Context) {
abortWeb <- struct{}{}
@@ -71,7 +79,21 @@ func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *g
})
api.GET("/status", func(c *gin.Context) {
c.String(http.StatusOK, version)
c.Header("X-Application-Name", "Helm Dashboard by Komodor.io") // to identify ourselves by ourselves
c.IndentedJSON(http.StatusOK, data.GetStatus())
})
api.GET("/api/cache", func(c *gin.Context) {
c.IndentedJSON(http.StatusOK, data.Cache)
})
api.DELETE("/api/cache", func(c *gin.Context) {
err := data.Cache.Clear()
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
c.Status(http.StatusAccepted)
})
configureHelms(api.Group("/api/helm"), data)
@@ -88,6 +110,7 @@ func configureHelms(api *gin.RouterGroup, data *subproc.DataLayer) {
api.GET("/charts/history", h.History)
api.GET("/charts/resources", h.Resources)
api.GET("/charts/:section", h.GetInfoSection)
api.GET("/charts/show", h.Show)
api.POST("/charts/install", h.Install)
api.POST("/charts/rollback", h.Rollback)
@@ -105,6 +128,7 @@ func configureKubectls(api *gin.RouterGroup, data *subproc.DataLayer) {
api.GET("/contexts", h.GetContexts)
api.GET("/resources/:kind", h.GetResourceInfo)
api.GET("/describe/:kind", h.Describe)
api.GET("/namespaces", h.GetNameSpaces)
}
func configureStatic(api *gin.Engine) {

View File

@@ -2,12 +2,13 @@ package handlers
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
)
type HelmHandler struct {
@@ -23,15 +24,13 @@ func (h *HelmHandler) GetCharts(c *gin.Context) {
c.IndentedJSON(http.StatusOK, res)
}
// TODO: helm show chart komodorio/k8s-watcher to get the icon URL
func (h *HelmHandler) Uninstall(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
err = h.Data.ChartUninstall(qp.Namespace, qp.Name)
err = h.Data.ReleaseUninstall(qp.Namespace, qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -46,7 +45,7 @@ func (h *HelmHandler) Rollback(c *gin.Context) {
return
}
err = h.Data.Revert(qp.Namespace, qp.Name, qp.Revision)
err = h.Data.Rollback(qp.Namespace, qp.Name, qp.Revision)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -61,7 +60,7 @@ func (h *HelmHandler) History(c *gin.Context) {
return
}
res, err := h.Data.ChartHistory(qp.Namespace, qp.Name)
res, err := h.Data.ReleaseHistory(qp.Namespace, qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
@@ -129,6 +128,22 @@ func (h *HelmHandler) RepoUpdate(c *gin.Context) {
c.Status(http.StatusNoContent)
}
func (h *HelmHandler) Show(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
res, err := h.Data.ShowChart(qp.Name)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.IndentedJSON(http.StatusOK, res)
}
func (h *HelmHandler) Install(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
@@ -248,11 +263,11 @@ func handleGetSection(data *subproc.DataLayer, section string, rDiff string, qp
return "", err
}
return res, nil
} else {
res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag)
if err != nil {
return "", err
}
return res, nil
}
res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag)
if err != nil {
return "", err
}
return res, nil
}

View File

@@ -1,12 +1,13 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
"k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"net/http"
)
type KubeHandler struct {
@@ -38,6 +39,7 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
return
}
// custom logic to provide most meaningful status for the resource
if res.Status.Phase == "Active" || res.Status.Phase == "Error" {
_ = res.Name + ""
} else if res.Status.Phase == "" && len(res.Status.Conditions) > 0 {
@@ -69,3 +71,13 @@ func (h *KubeHandler) Describe(c *gin.Context) {
c.String(http.StatusOK, res)
}
func (h *KubeHandler) GetNameSpaces(c *gin.Context) {
res, err := h.Data.GetNameSpaces()
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, res)
}

View File

@@ -12,11 +12,18 @@ type ScannersHandler struct {
}
func (h *ScannersHandler) List(c *gin.Context) {
var res []string
for _, scanner := range h.Data.Scanners {
res = append(res, scanner.Name())
type ScannerInfo struct {
SupportedResourceKinds []string
ManifestScannable bool
}
c.JSON(http.StatusOK, res)
res := map[string]ScannerInfo{}
for _, scanner := range h.Data.Scanners {
res[scanner.Name()] = ScannerInfo{
SupportedResourceKinds: scanner.SupportedResourceKinds(),
ManifestScannable: scanner.ManifestIsScannable(),
}
}
c.IndentedJSON(http.StatusOK, res)
}
func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) {

View File

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

View File

@@ -12,11 +12,31 @@ type Trivy struct {
Data *subproc.DataLayer
}
func (c *Trivy) ManifestIsScannable() bool {
return false
}
func (c *Trivy) SupportedResourceKinds() []string {
// from https://github.com/aquasecurity/trivy-kubernetes/blob/main/pkg/k8s/k8s.go#L190
return []string{
"ReplicaSet",
"ReplicationController",
"StatefulSet",
"Deployment",
"CronJob",
"DaemonSet",
"Job",
}
}
func (c *Trivy) Name() string {
return "Trivy"
}
func (c *Trivy) Test() bool {
utils.FailLogLevel = log.DebugLevel
defer func() { utils.FailLogLevel = log.WarnLevel }()
res, err := utils.RunCommand([]string{"trivy", "--version"}, nil)
if err != nil {
return false

View File

@@ -2,51 +2,73 @@ package dashboard
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/hashicorp/go-version"
"github.com/komodorio/helm-dashboard/pkg/dashboard/scanners"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
"net/http"
"os"
)
func StartServer(version string) (string, utils.ControlChan) {
data := subproc.DataLayer{}
type Server struct {
Version string
Namespace string
Address string
Debug bool
NoTracking bool
}
func (s Server) StartServer() (string, utils.ControlChan) {
data := subproc.DataLayer{
Namespace: s.Namespace,
Cache: subproc.NewCache(),
StatusInfo: &subproc.StatusInfo{
CurVer: s.Version,
Analytics: false,
LimitedToNamespace: s.Namespace,
},
}
err := data.CheckConnectivity()
if err != nil {
log.Errorf("Failed to check that Helm is operational, cannot continue. The error was: %s", err)
os.Exit(1) // TODO: propagate error instead?
}
isDevModeWithAnalytics := os.Getenv("HD_DEV_ANALYTICS") == "true"
data.StatusInfo.Analytics = (!s.NoTracking && s.Version != "0.0.0") || isDevModeWithAnalytics
go checkUpgrade(data.StatusInfo)
discoverScanners(&data)
address := os.Getenv("HD_BIND")
if address == "" {
address = "localhost"
}
if os.Getenv("HD_PORT") == "" {
address += ":8080" // TODO: better default port to clash less?
} else {
address += ":" + os.Getenv("HD_PORT")
}
abort := make(utils.ControlChan)
api := NewRouter(abort, &data, version)
done := startBackgroundServer(address, api, abort)
api := NewRouter(abort, &data, s.Debug)
done := s.startBackgroundServer(api, abort)
return "http://" + address, done
return "http://" + s.Address, done
}
func startBackgroundServer(addr string, routes *gin.Engine, abort utils.ControlChan) utils.ControlChan {
func (s Server) startBackgroundServer(routes *gin.Engine, abort utils.ControlChan) utils.ControlChan {
done := make(utils.ControlChan)
server := &http.Server{Addr: addr, Handler: routes}
server := &http.Server{
Addr: s.Address,
Handler: routes,
}
go func() {
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
panic(err) // TODO: in case of "port busy", check that it's another instance of us and just open browser
log.Warnf("Looks like port is busy for %s, checking if it's us...", s.Address)
if s.itIsUs() {
log.Infof("Yes, it's another instance of us. Just reuse it.")
} else {
panic(err)
}
}
done <- struct{}{}
}()
@@ -62,6 +84,21 @@ func startBackgroundServer(addr string, routes *gin.Engine, abort utils.ControlC
return done
}
func (s Server) itIsUs() bool {
url := fmt.Sprintf("http://%s/status", s.Address)
var myClient = &http.Client{
Timeout: 5 * time.Second,
}
r, err := myClient.Get(url)
if err != nil {
log.Debugf("It's not us on %s: %s", s.Address, err)
return false
}
defer r.Body.Close()
return strings.HasPrefix(r.Header.Get("X-Application-Name"), "Helm Dashboard")
}
func discoverScanners(data *subproc.DataLayer) {
potential := []subproc.Scanner{
&scanners.Checkov{Data: data},
@@ -75,3 +112,44 @@ func discoverScanners(data *subproc.DataLayer) {
}
}
}
func checkUpgrade(d *subproc.StatusInfo) { // 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 LatestVer: %s", err)
} else {
if v1.LessThan(v2) {
log.Warnf("Newer Helm Dashboard version is available: %s", d.LatestVer)
log.Warnf("Upgrade instructions: https://github.com/komodorio/helm-dashboard#installing")
} else {
log.Debugf("Got latest version from GH: %s", d.LatestVer)
}
}
}

View File

@@ -26,9 +26,14 @@ function checkUpgradeable(name) {
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-x-octagon")
$("#btnUpgrade").prop("disabled", true)
$("#btnUpgradeCheck").prop("disabled", true)
$("#btnAddRepository").text("Add repository for it")
$("#btnUpgradeCheck").text("")
return
}
$("#btnUpgrade .icon").removeClass("bi-x-octagon").addClass("bi-hourglass-split")
$("#btnAddRepository").text("")
$("#btnUpgradeCheck").text("Check for new version")
const verCur = $("#specRev").data("last-chart-ver");
const elm = data[0]
$("#btnUpgradeCheck").data("repo", elm.name.split('/').shift())
@@ -57,14 +62,18 @@ function popUpUpgrade(elm, ns, name, verCur, lastRev) {
$("#upgradeModalLabel .name").text(elm.name)
$("#upgradeModal .rel-cluster").text(getHashParam("context"))
if (verCur) {
$("#upgradeModalLabel .type").text("Upgrade")
$("#upgradeModal .ver-old").show().find("span").text(verCur)
$("#upgradeModal .rel-name").prop("disabled", true).val(name)
$("#upgradeModal .rel-ns").prop("disabled", true).val(ns)
} else {
$("#upgradeModalLabel .type").text("Install")
$("#upgradeModal .ver-old").hide()
$("#upgradeModal .rel-name").prop("disabled", false).val(elm.name.split("/").pop())
$("#upgradeModal .rel-ns").prop("disabled", false).val("")
$("#upgradeModal .rel-ns").prop("disabled", false).val(ns)
}
$.getJSON("/api/helm/repo/search?name=" + elm.name).fail(function (xhr) {
@@ -112,7 +121,8 @@ $("#upgradeModal .btn-confirm").click(function () {
}).done(function (data) {
if (data.version) {
setHashParam("section", null)
setHashParam("namespace", $("#upgradeModal .rel-ns").val())
const ns = $("#upgradeModal .rel-ns").val();
setHashParam("namespace", ns ? ns : "default") // TODO: relaets issue #51
setHashParam("chart", $("#upgradeModal .rel-name").val())
setHashParam("revision", data.version)
window.location.reload()
@@ -329,3 +339,7 @@ $("#btnRollback").click(function () {
})
})
$("#btnAddRepository").click(function () {
setHashParam("section", "repository")
window.location.reload()
})

View File

@@ -0,0 +1,66 @@
const xhr = new XMLHttpRequest();
xhr.onload = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
const status = JSON.parse(xhr.responseText);
const version = status.CurVer
if (status.Analytics) {
enableDD(version)
enableHeap(version)
}
}
}
xhr.open('GET', '/status', true);
xhr.send(null);
function enableDD(version) {
(function (h, o, u, n, d) {
h = h[d] = h[d] || {
q: [], onReady: function (c) {
h.q.push(c)
}
}
d = o.createElement(u);
d.async = true;
d.src = n
n = o.getElementsByTagName(u)[0];
n.parentNode.insertBefore(d, n)
})(window, document, 'script', 'https://www.datadoghq-browser-agent.com/datadog-rum-v4.js', 'DD_RUM')
DD_RUM.onReady(function () {
DD_RUM.init({
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
applicationId: 'e75439e5-e1b3-46ba-a9e9-a2e58579a2e2',
site: 'datadoghq.com',
service: 'helm-dashboard',
version: version,
trackInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: 'mask',
sessionReplaySampleRate: 0
})
})
}
function enableHeap(version) {
window.heap = window.heap || [], heap.load = function (e, t) {
window.heap.appid = e, window.heap.config = t = t || {};
let r = document.createElement("script");
r.type = "text/javascript", r.async = !0, r.src = "https://cdn.heapanalytics.com/js/heap-" + e + ".js";
let a = document.getElementsByTagName("script")[0];
a.parentNode.insertBefore(r, a);
for (let n = function (e) {
return function () {
heap.push([e].concat(Array.prototype.slice.call(arguments, 0)))
}
}, p = ["addEventProperties", "addUserProperties", "clearEventProperties", "identify", "resetIdentity", "removeEventProperty", "setEventProperties", "track", "unsetEventProperty"], o = 0; o < p.length; o++) heap[p[o]] = n(p[o])
};
heap.load("4249623943");
window.heap.addEventProperties({'version': version});
}
function sendStats(name, prop){
if (window.heap) {
window.heap.track(name, prop);
}
}

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") {
@@ -143,6 +147,7 @@ $("#nav-tab [data-tab]").click(function () {
function showResources(namespace, chart, revision) {
const resBody = $("#nav-resources .body");
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
let url = "/api/helm/charts/resources"
@@ -150,16 +155,25 @@ function showResources(namespace, chart, revision) {
$.getJSON(url).fail(function (xhr) {
reportError("Failed to get list of resources", xhr)
}).done(function (data) {
const scanners = $("body").data("scanners");
const scannableResKinds = new Set();
for (let k in scanners) {
scanners[k].SupportedResourceKinds.forEach(scannableResKinds.add, scannableResKinds)
}
resBody.empty();
data = data.sort(function (a, b) {
return interestingResources.indexOf(a.kind.toUpperCase()) - interestingResources.indexOf(b.kind.toUpperCase())
}).reverse();
for (let i = 0; i < data.length; i++) {
const res = data[i]
const resBlock = $(`
<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>
`)
@@ -172,7 +186,7 @@ function showResources(namespace, chart, revision) {
//reportError("Failed to get list of resources")
}).done(function (data) {
const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase);
if (["Available", "Active", "Established"].includes(data.status.phase)) {
if (["Available", "Active", "Established", "Bound", "Ready"].includes(data.status.phase)) {
badge.addClass("bg-success text-dark")
} else if (["Exists"].includes(data.status.phase)) {
badge.addClass("bg-success text-dark bg-opacity-50")
@@ -181,12 +195,12 @@ function showResources(namespace, chart, revision) {
} 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>")
const statusMessage = getStatusMessage(data.status)
resBlock.find(".res-statusmsg").html("<span class='text-muted small'>" + (statusMessage ? statusMessage : '') + "</span>")
if (badge.text() !== "NotFound") {
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
resBlock.find(".res-actions")
const btn = $("<button class=\"btn btn-sm btn-white border-secondary\">Describe</button>");
@@ -195,17 +209,29 @@ 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())
})
}
}
})
}
})
}
function getStatusMessage(status) {
if (!status) {
return
}
if (status.conditions) {
return status.conditions[0].message || status.conditions[0].reason
}
return status.message || status.reason
}
function showDescribe(ns, kind, name, badge) {
$("#describeModal .offcanvas-header p").text(kind)
$("#describeModalLabel").text(name).append(badge.addClass("ms-3 small fw-normal"))
@@ -237,25 +263,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,43 @@
<li class="nav-item mx-2">
<a class="nav-link px-3 section-repo">Repository</a>
</li>
<li class="nav-item mx-2 dropdown">
<a class="nav-link dropdown-toggle section-help" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Help
</a>
<ul class="dropdown-menu fs-80">
<li><a class="dropdown-item"
href="https://join.slack.com/t/komodorkommunity/shared_invite/zt-1dm3cnkue-ov1Yh~_95teA35QNx5yuMg"
target="_blank"><i class="bi-slack"></i> Support Chat</a></li>
<li><a class="dropdown-item" href="https://github.com/komodorio/helm-dashboard" target="_blank"><i
class="bi-github"></i> Project Page</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<!-- TODO: this should go under the "user menu" -->
<button class="dropdown-item" id="cacheClear"><i
class="bi-arrow-repeat"></i> Reset Cache
</button>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item disabled" href="#">Version <span id="toolVersion"></span></a></li>
</ul>
</li>
<li class="nav-item mx-2 display-none upgrade-possible">
<a class="nav-link position-relative text-danger"
href="https://github.com/komodorio/helm-dashboard#installing" target="_blank">
Upgrade to <span id="toolVersionUpgrade"></span>
</a></li>
</ul>
<div>
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img
src="https://komodor.com/wp-content/uploads/2021/05/favicon.png" alt="komodor.io"
src="static/komodor-logo.svg" alt="komodor.io"
style="height: 1.2rem; vertical-align: text-bottom; filter: grayscale(00%);"></a>
<a class="btn me-2 text-muted" href="https://github.com/komodorio/helm-dashboard"
title="Project page on GitHub"><i class="bi-github"></i></a>
</div>
<div class="separator-vertical"><span></span></div>
<i class="btn bi-power text-muted p-2 m-1 mx-2" title="Shut down the Helm Dashboard application"></i>
@@ -88,15 +99,16 @@
<button class="btn btn-sm btn-light bg-white border border-secondary btn-remove">
<i class="bi-trash3"></i> Remove
</button>
<p class="my-3"><input class="form-control form-control-sm" type="text" placeholder="Filter..."
id="inputSearch"></p>
</div>
<div><span class="text-muted small fw-bold me-3">REPOSITORY</span></div>
<h2 class="mb-3">name-of-repo</h2>
<div class="mb-5">
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">URL: <span class="url fw-bold">http://somerepo/somepath</span></span>
</div>
<div class="py-2 mb-3 float-end">
<div class="float-end">
<!-- TODO <input class="form-control form-control-sm" type="text" placeholder="Filter..."> -->
</div>
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
style="text-transform: uppercase">
@@ -105,7 +117,7 @@
<div class="col-1">Version</div>
<div class="col-1"></div>
</div>
<ul class="list-unstyled mt-4"></ul>
<ul class="list-unstyled mt-4 charts"></ul>
</div>
</div>
@@ -114,15 +126,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,9 +145,13 @@
<!-- INSTALLED LIST -->
<div class="col ms-2" id="installedList">
<div class="col rounded rounded-1 b-shadow header">
<div class="bg-white rounded-top m-0">
<div class="bg-white rounded-top m-0 spaced-out">
<h2 class="m-0 p-1"><img class="m-2 mx-3 me-2" src="static/helm-gray.svg" alt="Installed Charts">Installed
Charts (<span></span>)</h2>
<div class="form-outline w-25">
<input type="text" id="installedSearch" class="form-control form-control-sm"
placeholder="Filter..."/>
</div>
</div>
<div class="bg-secondary rounded-bottom m-0 row p-2">
<div class="col-4 hdr-name">Name</div>
@@ -147,6 +164,12 @@
</div>
<div class="body"></div>
<div class="bg-white rounded shadow p-3 display-none no-charts">Looks like you don't have any charts
installed. "Repository" section may be a good place to start.
</div>
<div class="bg-white rounded shadow p-3 display-none all-filtered">There are no releases matching your
filter criteria. Reset your filters or install more charts.
</div>
</div>
<!-- /INSTALLED LIST -->
</div>
@@ -175,7 +198,11 @@
title="Uninstall the chart"><i class="bi-trash3"></i> Uninstall
</button>
<br/>
<a class="link small" id="btnUpgradeCheck">Check for new version
<a class="link small" id="btnUpgradeCheck">
<span class="spinner-border spinner-border-sm" style="display: none" role="status"
aria-hidden="true"></span>
</a>
<a class="link small" id="btnAddRepository">
<span class="spinner-border spinner-border-sm" style="display: none" role="status"
aria-hidden="true"></span>
</a>
@@ -227,7 +254,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 +278,9 @@
data-mode="diff-rev">
Diff with specific revision: <input class="form-input" size="3" id="specRev">
</label>
<label class="form-check-label" for="userDefinedVals">
<input class="form-check-input" type="checkbox" id="userDefinedVals"> User-defined only
</label>
</form>
</nav>
@@ -329,7 +359,7 @@
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="upgradeModalLabel">
Install <b class='text-success name'></b>
<span class="type"></span> <b class='text-success name'></b>
</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
@@ -344,7 +374,12 @@
Release Name: <input class="form-control rel-name">
</label>
<label class="form-label me-4 text-dark">
Namespace (optional): <input class="form-control rel-ns">
Namespace (optional):
<input type="text" class="form-control rel-ns" list="ns-datalist"/>
<datalist id="ns-datalist"></datalist>
</label>
<label class="form-label me-4 text-dark">
Cluster: <span class="form-label rel-cluster"></span>
</label>
</div>
<div class="row">
@@ -374,12 +409,28 @@
<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>
<!-- 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

@@ -4,20 +4,39 @@ function loadChartsList() {
const chartsCards = $("#installedList .body")
chartsCards.empty().append("<div><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span> Loading...</div>")
$.getJSON("/api/helm/charts").fail(function (xhr) {
sendStats('Get releases', {'status': 'failed'});
reportError("Failed to get list of charts", xhr)
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,56 @@ function buildChartCard(elm) {
<div class="col-1 rel-date text-nowrap"><span>today</span><div>Updated</div></div>
</div>`)
let chartName = elm.chart
// semver2 regex , add optional v prefix
const chartNameRegex = 'v?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?'
const match = elm.chart.match(chartNameRegex);
if (match) {
chartName = elm.chart.substring(0, match.index - 1)
} else {
// fall back to simple substr
chartName = elm.chart.substring(0, elm.chart.lastIndexOf("-"))
}
$.getJSON("/api/helm/repo/search?name=" + chartName).fail(function (xhr) {
// we're ok if we can't show icon and description
console.log("Failed to get repo name for charts", xhr)
}).done(function (data) {
if (data.length > 0) {
$.getJSON("/api/helm/charts/show?name=" + data[0].name).fail(function (xhr) {
console.log("Failed to get chart", xhr)
}).done(function (data) {
if (data) {
const res = data[0];
if (res.icon) {
card.find(".rel-name").attr("style", "background-image: url(" + res.icon + ")")
}
if (res.description) {
card.find(".rel-name div").text(res.description)
}
}
})
}
})
card.find(".rel-name span").text(elm.name)
card.find(".rel-rev span").text("#" + elm.revision)
card.find(".rel-ns span").text(elm.namespace)
card.find(".rel-chart span").text(elm.chart)
card.find(".rel-date span").text(getAge(elm))
card.data("namespace", elm.namespace)
card.data("name", elm.name)
card.data("chart", elm.chart)
statusStyle(elm.status, card, card.find(".rel-status span"))
card.find("a").attr("href", '#context=' + getHashParam('context') + '&namespace=' + elm.namespace + '&name=' + elm.name)
card.find(".rel-name span").data("chart", elm).click(function () {
card.data("chart", elm).click(function () {
if (window.getSelection().toString()) {
return
}
const self = $(this)
$("#sectionList").hide()
@@ -48,3 +106,6 @@ function buildChartCard(elm) {
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

@@ -4,9 +4,11 @@ function loadRepoView() {
$.getJSON("/api/helm/repo").fail(function (xhr) {
reportError("Failed to get list of repositories", xhr)
sendStats('Get repo', {'status': 'fail'});
}).done(function (data) {
const items = $("#sectionRepo .repo-list ul").empty()
data.sort((a, b) => (a.name > b.name) - (a.name < b.name))
data.forEach(function (elm) {
let opt = $('<li class="mb-2"><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
opt.attr('title', elm.url)
@@ -18,8 +20,9 @@ function loadRepoView() {
if (!data.length) {
items.text("No repositories found, try adding one")
}
sendStats('Get repo', {'status': 'success', length:data.length});
items.find("input").click(function () {
$("#inputSearch").val('')
const self = $(this)
const elm = self.data("item");
setHashParam("repo", elm.name)
@@ -61,6 +64,19 @@ function loadRepoView() {
})
}
$("#inputSearch").keyup(function () {
let val = $(this).val().toLowerCase();
$(".charts li").each(function () {
let chartName = $(this.firstElementChild).text().toLowerCase()
if (chartName.indexOf(val) >= 0) {
$(this).show()
} else {
$(this).hide()
}
})
})
$("#sectionRepo .repo-list .btn").click(function () {
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
myModal.show()
@@ -115,6 +131,9 @@ 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:""
popUpUpgrade(elm, contextNamespace)
}
}

View File

@@ -32,7 +32,7 @@ function fillChartHistory(data, namespace, name) {
$("#specRev").val(elm.revision).data("last-rev", elm.revision).data("last-chart-ver", elm.chart_ver)
}
const rev = $(`<li class="px-2 pt-5 pb-4 mb-2 rounded border border-secondary bg-secondary position-relative">
const rev = $(`<li class="px-2 pt-5 pb-4 mb-2 rounded border border-secondary bg-secondary position-relative link">
<div class="rev-status position-absolute top-0 m-2 mb-5 start-0 fw-bold"></div>
<div class="rev-number position-absolute top-0 m-2 mb-5 end-0 fw-bold fs-6"></div>
<div class="rev-changes position-absolute bottom-0 start-0 m-2 text-muted small"></div>
@@ -48,22 +48,26 @@ function fillChartHistory(data, namespace, name) {
if (elm.description.startsWith("Rollback to ")) {
//rev.find(".rev-status").append(" <span class='small fw-normal text-lowercase'>(rollback)</span>")
rev.find(".rev-status").append(" <i class='bi-arrow-counterclockwise text-muted' title='"+elm.description+"'></i>")
rev.find(".rev-status").append(" <i class='bi-arrow-counterclockwise text-muted' title='" + elm.description + "'></i>")
}
const nxt = data[x + 1];
if (nxt && isNewerVersion(elm.chart_ver, nxt.chart_ver)) {
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='text-danger bi-arrow-down-right'></i> " + elm.chart_ver)
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='bi-arrow-down-right'></i> " + elm.chart_ver)
} else if (nxt && isNewerVersion(nxt.chart_ver, elm.chart_ver)) {
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='text-success bi-arrow-up-right'></i> " + elm.chart_ver)
rev.find(".rev-changes").html("<span class='strike'>" + nxt.chart_ver + "</span> <i class='bi-arrow-up-right'></i> " + elm.chart_ver)
}
rev.data("elm", elm)
rev.addClass("rev-" + elm.revision)
rev.click(function () {
if (window.getSelection().toString()) {
return
}
revisionClicked(namespace, name, $(this))
})
// revRow.attr("class", "link")
revRow.append(rev)
}
}

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/kube/contexts").fail(function (xhr) {
sendStats('contexts', {'status': 'fail'});
reportError("Failed to get list of clusters", xhr)
}).done(function (data) {
$("body").data("contexts", data)
const context = getHashParam("context")
data.sort((a, b) => (getCleanClusterName(a.Name) > getCleanClusterName(b.Name)) - (getCleanClusterName(a.Name) < getCleanClusterName(b.Name)))
fillClusterList(data, context);
sendStats('contexts', {'status': 'success', length: data.length});
$.getJSON("/api/kube/namespaces").fail(function (xhr) {
reportError("Failed to get namespaces", xhr)
}).done(function (res) {
const ns = res.items.map(i => i.metadata.name)
$.each(ns, function (i, item) {
$("#upgradeModal #ns-datalist").append($("<option>", {
value: item,
text: item
}))
})
if (!limNS) {
fillNamespaceList(res.items)
}
}).always(function () {
initView(); // can only do it after loading cluster and namespace lists
})
})
}
function initView() {
$(".section").hide()
@@ -44,17 +94,24 @@ function initView() {
$("#topNav ul a").click(function () {
const self = $(this)
if (self.hasClass("section-help")) {
return;
}
$("#topNav ul a").removeClass("active")
const ctx = getHashParam("context")
const filteredNamespace = getHashParam("filteredNamespace")
setHashParam(null, null)
setHashParam("context", ctx)
setHashParam("filteredNamespace", filteredNamespace)
if (self.hasClass("section-repo")) {
setHashParam("section", "repository")
} else {
} else if (self.hasClass("section-installed")) {
setHashParam("section", null)
} else {
return
}
initView()
@@ -116,28 +173,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 +207,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 +270,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 +294,66 @@ 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('+'))
}
}

View File

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

View File

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

View File

@@ -0,0 +1,100 @@
package subproc
import (
"context"
"errors"
"github.com/eko/gocache/v3/marshaler"
"github.com/eko/gocache/v3/store"
gocache "github.com/patrickmn/go-cache"
log "github.com/sirupsen/logrus"
"time"
)
type CacheKey = string
const CacheKeyRelList CacheKey = "installed-releases-list"
const CacheKeyShowChart CacheKey = "show-chart"
const CacheKeyRelHistory CacheKey = "release-history"
const CacheKeyRevManifests CacheKey = "rev-manifests"
const CacheKeyRevNotes CacheKey = "rev-notes"
const CacheKeyRevValues CacheKey = "rev-values"
const CacheKeyRepoChartValues CacheKey = "chart-values"
const CacheKeyAllRepos CacheKey = "all-repos"
type Cache struct {
Marshaler *marshaler.Marshaler `json:"-"`
HitCount int
MissCount int
}
func NewCache() *Cache {
gocacheClient := gocache.New(60*time.Minute, 10*time.Minute)
gocacheStore := store.NewGoCache(gocacheClient)
// TODO: use tiered cache with some disk backend, allow configuring that static cache folder
// Initializes marshaler
marshal := marshaler.New(gocacheStore)
return &Cache{
Marshaler: marshal,
}
}
func (c *Cache) String(key CacheKey, tags []string, callback func() (string, error)) (string, error) {
if tags == nil {
tags = make([]string, 0)
}
tags = append(tags, key)
ctx := context.Background()
out := ""
_, err := c.Marshaler.Get(ctx, key, &out)
if err == nil {
log.Debugf("Using cached value for %s", key)
c.HitCount++
return out, nil
} else if !errors.Is(err, store.NotFound{}) {
return "", err
}
c.MissCount++
out, err = callback()
if err != nil {
return "", err
}
err = c.Marshaler.Set(ctx, key, out, store.WithTags(tags))
if err != nil {
return "", err
}
return out, nil
}
func (c *Cache) Invalidate(tags ...CacheKey) {
log.Debugf("Invalidating tags %v", tags)
err := c.Marshaler.Invalidate(context.Background(), store.WithInvalidateTags(tags))
if err != nil {
log.Warnf("Failed to invalidate tags %v: %s", tags, err)
}
}
func (c *Cache) Clear() error {
c.HitCount = 0
c.MissCount = 0
return c.Marshaler.Clear(context.Background())
}
func cacheTagRelease(namespace string, name string) CacheKey {
return "release" + "\v" + namespace + "\v" + name
}
func cacheTagRepoVers(chartName string) CacheKey {
return "repo-versions" + "\v" + chartName
}
func cacheTagRepoCharts(name string) CacheKey {
return "repo-charts" + "\v" + name
}
func cacheTagRepoName(name string) CacheKey {
return "repo-name" + "\v" + name
}

View File

@@ -5,6 +5,10 @@ import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
@@ -13,11 +17,6 @@ import (
"gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/release"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
type DataLayer struct {
@@ -25,10 +24,27 @@ type DataLayer struct {
Helm string
Kubectl string
Scanners []Scanner
StatusInfo *StatusInfo
Namespace string
Cache *Cache
}
type StatusInfo struct {
CurVer string
LatestVer string
Analytics bool
LimitedToNamespace string
CacheHitRatio float64
ClusterMode bool
}
func (d *DataLayer) runCommand(cmd ...string) (string, error) {
log.Debugf("Starting command: %s", cmd)
for i, c := range cmd {
// TODO: remove namespace parameter if it's empty
if c == "--namespace" && i < len(cmd) { // TODO: in case it's not found - add it?
d.forceNamespace(&cmd[i+1])
}
}
return utils.RunCommand(cmd, map[string]string{"HELM_KUBECONTEXT": d.KubeContext})
}
@@ -46,19 +62,10 @@ func (d *DataLayer) runCommandHelm(cmd ...string) (string, error) {
return d.runCommand(cmd...)
}
func (d *DataLayer) runCommandKubectl(cmd ...string) (string, error) {
// TODO: migrate into using kubectl "k8s.io/kubectl/pkg/cmd" and kube API
if d.Kubectl == "" {
d.Kubectl = "kubectl"
func (d *DataLayer) forceNamespace(s *string) {
if d.Namespace != "" {
*s = d.Namespace
}
cmd = append([]string{d.Kubectl}, cmd...)
if d.KubeContext != "" {
cmd = append(cmd, "--context", d.KubeContext)
}
return d.runCommand(cmd...)
}
func (d *DataLayer) CheckConnectivity() error {
@@ -68,10 +75,17 @@ func (d *DataLayer) CheckConnectivity() error {
}
if len(contexts) < 1 {
return errors.New("did not find any kubectl contexts configured")
log.Debugf("Did not find any contexts, will try checking k8s")
_, err := d.runCommandKubectl("get", "pods")
if err != nil {
log.Debugf("The error were: %s", err)
return errors.New("did not find any kubectl contexts configured")
}
log.Infof("Assuming k8s environment")
d.StatusInfo.ClusterMode = true
}
_, err = d.runCommandHelm("--help") // no point in doing is, since the default context may be invalid
_, err = d.runCommandHelm("--help")
if err != nil {
return err
}
@@ -79,51 +93,29 @@ func (d *DataLayer) CheckConnectivity() error {
return nil
}
type KubeContext struct {
IsCurrent bool
Name string
Cluster string
AuthInfo string
Namespace string
}
func (d *DataLayer) ListContexts() (res []KubeContext, err error) {
out, err := d.runCommandKubectl("config", "get-contexts")
if err != nil {
return nil, err
func (d *DataLayer) GetStatus() *StatusInfo {
sum := float64(d.Cache.HitCount + d.Cache.MissCount)
if sum > 0 {
d.StatusInfo.CacheHitRatio = float64(d.Cache.HitCount) / sum
} else {
d.StatusInfo.CacheHitRatio = 0
}
// kubectl has no JSON output for it, we'll have to do custom text parsing
lines := strings.Split(out, "\n")
// find field positions
fields := regexp.MustCompile(`(\w+\s+)`).FindAllString(lines[0], -1)
cur := len(fields[0])
name := cur + len(fields[1])
cluster := name + len(fields[2])
auth := cluster + len(fields[3])
// read items
for _, line := range lines[1:] {
if strings.TrimSpace(line) == "" {
continue
}
res = append(res, KubeContext{
IsCurrent: strings.TrimSpace(line[0:cur]) == "*",
Name: strings.TrimSpace(line[cur:name]),
Cluster: strings.TrimSpace(line[name:cluster]),
AuthInfo: strings.TrimSpace(line[cluster:auth]),
Namespace: strings.TrimSpace(line[auth:]),
})
}
return res, nil
return d.StatusInfo
}
func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) {
cmd := []string{"ls", "--all", "--output", "json", "--time-format", time.RFC3339}
// TODO: filter by namespace
out, err := d.runCommandHelm("ls", "--all", "--all-namespaces", "--output", "json", "--time-format", time.RFC3339)
if d.Namespace == "" {
cmd = append(cmd, "--all-namespaces")
} else {
cmd = append(cmd, "--namespace", d.Namespace)
}
out, err := d.Cache.String(CacheKeyRelList, nil, func() (string, error) {
return d.runCommandHelm(cmd...)
})
if err != nil {
return nil, err
}
@@ -135,9 +127,13 @@ func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) {
return res, nil
}
func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*HistoryElement, err error) {
func (d *DataLayer) ReleaseHistory(namespace string, releaseName string) (res []*HistoryElement, err error) {
// TODO: there is `max` but there is no `offset`
out, err := d.runCommandHelm("history", chartName, "--namespace", namespace, "--output", "json", "--max", "18")
ct := cacheTagRelease(namespace, releaseName)
out, err := d.Cache.String(CacheKeyRelHistory+ct, []string{ct}, func() (string, error) {
return d.runCommandHelm("history", releaseName, "--namespace", namespace, "--output", "json")
})
if err != nil {
return nil, err
}
@@ -152,7 +148,7 @@ func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*His
if err != nil {
return nil, err
}
elm.ChartName = chartRepoName
elm.ChartName = chartRepoName // TODO: move it to frontend?
elm.ChartVer = curVer
elm.Updated.Time = elm.Updated.Time.Round(time.Second)
}
@@ -160,78 +156,15 @@ func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*His
return res, nil
}
func (d *DataLayer) ChartRepoVersions(chartName string) (res []*RepoChartElement, err error) {
search := "/" + chartName + "\v"
if strings.Contains(chartName, "/") {
search = "\v" + chartName + "\v"
}
cmd := []string{"search", "repo", "--regexp", search, "--versions", "--output", "json"}
out, err := d.runCommandHelm(cmd...)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
return res, nil
}
func (d *DataLayer) ChartRepoCharts(repoName string) (res []*RepoChartElement, err error) {
cmd := []string{"search", "repo", "--regexp", "\v" + repoName + "/", "--output", "json"}
out, err := d.runCommandHelm(cmd...)
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
ins, err := d.ListInstalled()
if err != nil {
return nil, err
}
enrichRepoChartsWithInstalled(res, ins)
return res, nil
}
func enrichRepoChartsWithInstalled(charts []*RepoChartElement, installed []ReleaseElement) {
for _, chart := range charts {
for _, rel := range installed {
c, _, err := utils.ChartAndVersion(rel.Chart)
if err != nil {
log.Warnf("Failed to parse chart: %s", err)
continue
}
pieces := strings.Split(chart.Name, "/")
if pieces[1] == c {
chart.InstalledNamespace = rel.Namespace
chart.InstalledName = rel.Name
}
}
}
}
type SectionFn = func(string, string, int, bool) (string, error) // TODO: rework it into struct-based argument?
func (d *DataLayer) RevisionManifests(namespace string, chartName string, revision int, _ bool) (res string, err error) {
cmd := []string{"get", "manifest", chartName, "--namespace", namespace}
if revision > 0 {
cmd = append(cmd, "--revision", strconv.Itoa(revision))
}
cmd := []string{"get", "manifest", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision)}
out, err := d.runCommandHelm(cmd...)
if err != nil {
return "", err
}
return out, nil
key := CacheKeyRevManifests + "\v" + namespace + "\v" + chartName + "\v" + strconv.Itoa(revision)
return d.Cache.String(key, nil, func() (string, error) {
return d.runCommandHelm(cmd...)
})
}
func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string, revision int) ([]*v1.Carp, error) {
@@ -246,7 +179,7 @@ func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string,
var tmp interface{}
for dec.Decode(&tmp) == nil {
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
// bug we can juggle it
// we can juggle it
jsoned, err := json.Marshal(tmp)
if err != nil {
return nil, err
@@ -270,115 +203,38 @@ func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string,
}
func (d *DataLayer) RevisionNotes(namespace string, chartName string, revision int, _ bool) (res string, err error) {
out, err := d.runCommandHelm("get", "notes", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision))
if err != nil {
return "", err
}
return out, nil
cmd := []string{"get", "notes", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision)}
key := CacheKeyRevNotes + "\v" + namespace + "\v" + chartName + "\v" + strconv.Itoa(revision)
return d.Cache.String(key, nil, func() (string, error) {
return d.runCommandHelm(cmd...)
})
}
func (d *DataLayer) RevisionValues(namespace string, chartName string, revision int, onlyUserDefined bool) (res string, err error) {
cmd := []string{"get", "values", chartName, "--namespace", namespace, "--output", "yaml"}
if revision > 0 {
cmd = append(cmd, "--revision", strconv.Itoa(revision))
}
cmd := []string{"get", "values", chartName, "--namespace", namespace, "--output", "yaml", "--revision", strconv.Itoa(revision)}
if !onlyUserDefined {
cmd = append(cmd, "--all")
}
out, err := d.runCommandHelm(cmd...)
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) GetResource(namespace string, def *v1.Carp) (*v1.Carp, error) {
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "json")
if err != nil {
if strings.HasSuffix(strings.TrimSpace(err.Error()), " not found") {
return &v1.Carp{
Status: v1.CarpStatus{
Phase: "NotFound",
Message: err.Error(),
Reason: "not found",
},
}, nil
} else {
return nil, err
}
}
var res v1.Carp
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
sort.Slice(res.Status.Conditions, func(i, j int) bool {
// some condition types always bubble up
if res.Status.Conditions[i].Type == "Available" {
return false
}
if res.Status.Conditions[j].Type == "Available" {
return true
}
t1 := res.Status.Conditions[i].LastTransitionTime
t2 := res.Status.Conditions[j].LastTransitionTime
return t1.Time.Before(t2.Time)
key := CacheKeyRevValues + "\v" + namespace + "\v" + chartName + "\v" + strconv.Itoa(revision) + "\v" + fmt.Sprintf("%v", onlyUserDefined)
return d.Cache.String(key, nil, func() (string, error) {
return d.runCommandHelm(cmd...)
})
return &res, nil
}
func (d *DataLayer) GetResourceYAML(namespace string, def *v1.Carp) (string, error) {
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "yaml")
if err != nil {
return "", err
}
func (d *DataLayer) ReleaseUninstall(namespace string, name string) error {
d.Cache.Invalidate(CacheKeyRelList, cacheTagRelease(namespace, name))
return out, nil
}
func (d *DataLayer) DescribeResource(namespace string, kind string, name string) (string, error) {
out, err := d.runCommandKubectl("describe", strings.ToLower(kind), name, "--namespace", namespace)
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) ChartUninstall(namespace string, name string) error {
_, err := d.runCommandHelm("uninstall", name, "--namespace", namespace)
if err != nil {
return err
}
return nil
return err
}
func (d *DataLayer) Revert(namespace string, name string, rev int) error {
func (d *DataLayer) Rollback(namespace string, name string, rev int) error {
d.Cache.Invalidate(CacheKeyRelList, cacheTagRelease(namespace, name))
_, err := d.runCommandHelm("rollback", name, strconv.Itoa(rev), "--namespace", namespace)
if err != nil {
return err
}
return nil
}
func (d *DataLayer) ChartRepoUpdate(name string) error {
cmd := []string{"repo", "update"}
if name != "" {
cmd = append(cmd, name)
}
_, err := d.runCommandHelm(cmd...)
if err != nil {
return err
}
return nil
return err
}
func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string, version string, justTemplate bool, values string, reuseVals bool) (string, error) {
@@ -405,6 +261,11 @@ func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string
if err != nil {
return "", err
}
if !justTemplate {
d.Cache.Invalidate(CacheKeyRelList, cacheTagRelease(namespace, name))
}
res := release.Release{}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
@@ -417,41 +278,6 @@ func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string
return out, nil
}
func (d *DataLayer) ShowValues(chart string, ver string) (string, error) {
return d.runCommandHelm("show", "values", chart, "--version", ver)
}
func (d *DataLayer) ChartRepoList() (res []RepositoryElement, err error) {
out, err := d.runCommandHelm("repo", "list", "--output", "json")
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
return res, nil
}
func (d *DataLayer) ChartRepoAdd(name string, url string) (string, error) {
out, err := d.runCommandHelm("repo", "add", "--force-update", name, url)
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) ChartRepoDelete(name string) (string, error) {
out, err := d.runCommandHelm("repo", "remove", name)
if err != nil {
return "", err
}
return out, nil
}
func RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) {
if revision1 == 0 || revision2 == 0 {
log.Debugf("One of revisions is zero: %d %d", revision1, revision2)

View File

@@ -13,7 +13,9 @@ func TestFlow(t *testing.T) {
log.SetLevel(log.DebugLevel)
var _ release.Status
data := DataLayer{}
data := DataLayer{
Cache: NewCache(),
}
err := data.CheckConnectivity()
if err != nil {
if err.Error() == "did not find any kubectl contexts configured" {
@@ -40,7 +42,7 @@ func TestFlow(t *testing.T) {
}
chart := installed[1]
history, err := data.ChartHistory(chart.Namespace, chart.Name)
history, err := data.ReleaseHistory(chart.Namespace, chart.Name)
if err != nil {
t.Fatal(err)
}

View File

@@ -24,8 +24,9 @@ type HistoryElement struct {
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
ChartName string `json:"chart_name"`
ChartVer string `json:"chart_ver"`
ChartName string `json:"chart_name"` // custom addition on top of Helm
ChartVer string `json:"chart_ver"` // custom addition on top of Helm
}
type RepoChartElement struct {

View File

@@ -0,0 +1,151 @@
package subproc
import (
"encoding/json"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"os"
"regexp"
"sort"
"strings"
)
func (d *DataLayer) runCommandKubectl(cmd ...string) (string, error) {
if d.Kubectl == "" {
d.Kubectl = "kubectl"
}
cmd = append([]string{d.Kubectl}, cmd...)
if d.KubeContext != "" {
cmd = append(cmd, "--context", d.KubeContext)
}
return d.runCommand(cmd...)
}
type KubeContext struct {
IsCurrent bool
Name string
Cluster string
AuthInfo string
Namespace string
}
func (d *DataLayer) ListContexts() (res []KubeContext, err error) {
res = []KubeContext{}
if os.Getenv("HD_CLUSTER_MODE") != "" {
return res, nil
}
out, err := d.runCommandKubectl("config", "get-contexts")
if err != nil {
return nil, err
}
// kubectl has no JSON output for it, we'll have to do custom text parsing
lines := strings.Split(out, "\n")
// find field positions
fields := regexp.MustCompile(`(\w+\s+)`).FindAllString(lines[0], -1)
cur := len(fields[0])
name := cur + len(fields[1])
cluster := name + len(fields[2])
auth := cluster + len(fields[3])
// read items
for _, line := range lines[1:] {
if strings.TrimSpace(line) == "" {
continue
}
res = append(res, KubeContext{
IsCurrent: strings.TrimSpace(line[0:cur]) == "*",
Name: strings.TrimSpace(line[cur:name]),
Cluster: strings.TrimSpace(line[name:cluster]),
AuthInfo: strings.TrimSpace(line[cluster:auth]),
Namespace: strings.TrimSpace(line[auth:]),
})
}
return res, nil
}
type NamespaceElement struct {
Items []struct {
Metadata struct {
Name string `json:"name"`
} `json:"metadata"`
} `json:"items"`
}
func (d *DataLayer) GetNameSpaces() (res *NamespaceElement, err error) {
out, err := d.runCommandKubectl("get", "namespaces", "-o", "json")
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
return res, nil
}
func (d *DataLayer) GetResource(namespace string, def *v1.Carp) (*v1.Carp, error) {
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "json")
if err != nil {
if strings.HasSuffix(strings.TrimSpace(err.Error()), " not found") {
return &v1.Carp{
Status: v1.CarpStatus{
Phase: "NotFound",
Message: err.Error(),
Reason: "not found",
},
}, nil
} else {
return nil, err
}
}
var res v1.Carp
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
sort.Slice(res.Status.Conditions, func(i, j int) bool {
// some condition types always bubble up
if res.Status.Conditions[i].Type == "Available" {
return false
}
if res.Status.Conditions[j].Type == "Available" {
return true
}
t1 := res.Status.Conditions[i].LastTransitionTime
t2 := res.Status.Conditions[j].LastTransitionTime
return t1.Time.Before(t2.Time)
})
return &res, nil
}
func (d *DataLayer) GetResourceYAML(namespace string, def *v1.Carp) (string, error) {
out, err := d.runCommandKubectl("get", strings.ToLower(def.Kind), def.Name, "--namespace", namespace, "--output", "yaml")
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) DescribeResource(namespace string, kind string, name string) (string, error) {
out, err := d.runCommandKubectl("describe", strings.ToLower(kind), name, "--namespace", namespace)
if err != nil {
return "", err
}
return out, nil
}

View File

@@ -0,0 +1,164 @@
package subproc
import (
"bytes"
"encoding/json"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/chart"
"strings"
)
func (d *DataLayer) ChartRepoList() (res []RepositoryElement, err error) {
out, err := d.Cache.String(CacheKeyAllRepos, nil, func() (string, error) {
// TODO: do a bg check, if the state is changed - do reset some caches
return d.runCommandHelm("repo", "list", "--output", "json")
})
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
return res, nil
}
func (d *DataLayer) ChartRepoAdd(name string, url string) (string, error) {
d.Cache.Invalidate(CacheKeyAllRepos)
out, err := d.runCommandHelm("repo", "add", "--force-update", name, url)
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) ChartRepoDelete(name string) (string, error) {
d.Cache.Invalidate(CacheKeyAllRepos)
out, err := d.runCommandHelm("repo", "remove", name)
if err != nil {
return "", err
}
return out, nil
}
func (d *DataLayer) ChartRepoUpdate(name string) error {
d.Cache.Invalidate(cacheTagRepoName(name), CacheKeyAllRepos)
cmd := []string{"repo", "update"}
if name != "" {
cmd = append(cmd, name)
}
_, err := d.runCommandHelm(cmd...)
return err
}
func (d *DataLayer) ChartRepoVersions(chartName string) (res []*RepoChartElement, err error) {
search := "/" + chartName + "\v"
if strings.Contains(chartName, "/") {
search = "\v" + chartName + "\v"
}
cmd := []string{"search", "repo", "--regexp", search, "--versions", "--output", "json"}
out, err := d.Cache.String(cacheTagRepoVers(chartName), []string{CacheKeyAllRepos}, func() (string, error) {
return d.runCommandHelm(cmd...)
})
if err != nil {
if strings.Contains(err.Error(), "no repositories configured") {
out = "[]"
} else {
return nil, err
}
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
return res, nil
}
func (d *DataLayer) ChartRepoCharts(repoName string) (res []*RepoChartElement, err error) {
cmd := []string{"search", "repo", "--regexp", "\v" + repoName + "/", "--output", "json"}
out, err := d.Cache.String(cacheTagRepoCharts(repoName), []string{CacheKeyAllRepos}, func() (string, error) {
return d.runCommandHelm(cmd...)
})
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
ins, err := d.ListInstalled()
if err != nil {
return nil, err
}
enrichRepoChartsWithInstalled(res, ins)
return res, nil
}
func enrichRepoChartsWithInstalled(charts []*RepoChartElement, installed []ReleaseElement) {
for _, rchart := range charts {
for _, rel := range installed {
c, _, err := utils.ChartAndVersion(rel.Chart)
if err != nil {
log.Warnf("Failed to parse chart: %s", err)
continue
}
pieces := strings.Split(rchart.Name, "/")
if pieces[1] == c {
// TODO: there can be more than one
rchart.InstalledNamespace = rel.Namespace
rchart.InstalledName = rel.Name
}
}
}
}
// ShowValues get values from repo chart, not from installed release
func (d *DataLayer) ShowValues(chart string, ver string) (string, error) {
return d.Cache.String(CacheKeyRepoChartValues+"\v"+chart+"\v"+ver, nil, func() (string, error) {
return d.runCommandHelm("show", "values", chart, "--version", ver)
})
}
func (d *DataLayer) ShowChart(chartName string) ([]*chart.Metadata, error) { // TODO: add version parameter to method
out, err := d.Cache.String(CacheKeyShowChart+"\v"+chartName, []string{"chart\v" + chartName}, func() (string, error) {
return d.runCommandHelm("show", "chart", chartName)
})
if err != nil {
return nil, err
}
deccoder := yaml.NewDecoder(bytes.NewReader([]byte(out)))
res := make([]*chart.Metadata, 0)
var tmp interface{}
for deccoder.Decode(&tmp) == nil {
jsoned, err := json.Marshal(tmp)
if err != nil {
return nil, err
}
var resjson chart.Metadata
err = json.Unmarshal(jsoned, &resjson)
if err != nil {
return nil, err
}
res = append(res, &resjson)
}
return res, nil
}

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

View File

@@ -0,0 +1,118 @@
package utils
import (
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestGetQueryProps(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name string
endpoint string
revRequired bool
wantErr bool
}{
{
name: "Get query props - all set with revRequired true",
wantErr: false,
revRequired: true,
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing&revision=1",
},
{
name: "Get query props - no revision with revRequired true",
wantErr: true,
revRequired: true,
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing",
},
{
name: "Get query props - no namespace with revRequired true",
wantErr: false,
revRequired: true,
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&revision=1",
},
{
name: "Get query props - no name with revRequired true",
wantErr: true,
revRequired: true,
endpoint: "/api/v1/namespaces/komodorio/charts?namespace=testing&revision=1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", tt.endpoint, nil)
_, err := GetQueryProps(c, tt.revRequired)
if (err != nil) != tt.wantErr {
t.Errorf("GetQueryProps() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestChartAndVersion(t *testing.T) {
tests := []struct {
name string
params string
wantChart string
wantVer string
wantError bool
}{
{
name: "Chart and version - successfully parsing chart and version",
params: "chart-1.0.0",
wantChart: "chart",
wantVer: "1.0.0",
wantError: false,
},
{
name: "Chart and version - successfully parsing chart and version",
params: "chart-v1.0.0",
wantChart: "chart",
wantVer: "v1.0.0",
wantError: false,
},
{
name: "Chart and version - successfully parsing chart and version",
params: "chart-v1.0.0-alpha",
wantChart: "chart",
wantVer: "v1.0.0-alpha",
wantError: false,
},
{
name: "Chart and version - successfully parsing chart and version",
params: "chart-1.0.0-alpha",
wantChart: "chart",
wantVer: "1.0.0-alpha",
wantError: false,
},
{
name: "Chart and version - parsing chart without version",
params: "chart",
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a, b, err := ChartAndVersion(tt.params)
if (err != nil) != tt.wantError {
t.Errorf("ChartAndVersion() error = %v, wantErr %v", err, tt.wantError)
return
}
if a != tt.wantChart {
t.Errorf("ChartAndVersion() got = %v, want %v", a, tt.wantChart)
}
if b != tt.wantVer {
t.Errorf("ChartAndVersion() got1 = %v, want %v", b, tt.wantVer)
}
})
}
}

View File

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

View File

@@ -11,6 +11,7 @@ if [ -n "${HELM_PUSH_PLUGIN_NO_INSTALL_HOOK}" ]; then
fi
version="$(cat plugin.yaml | grep "version" | cut -d '"' -f 2)"
# TODO: if no version provided, get it from https://api.github.com/repos/komodorio/helm-dashboard/releases/latest
echo "Downloading and installing ${name} v${version} ..."
url=""
@@ -62,3 +63,8 @@ fi
tar xzf "releases/v${version}.tar.gz" -C "releases/v${version}"
mv "releases/v${version}/${name}" "bin/${name}" || \
mv "releases/v${version}/${name}.exe" "bin/${name}"
echo
echo "Helm Dashboard is installed, to start it, run in your terminal:"
echo " helm dashboard"
echo