Compare commits
199 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37e1d44bf1 | ||
|
|
362cb09e6d | ||
|
|
209f5b5e44 | ||
|
|
a0680a4820 | ||
|
|
d95cac94d5 | ||
|
|
bbb425bfea | ||
|
|
679d31e4ab | ||
|
|
3119d17738 | ||
|
|
778e58360c | ||
|
|
a7c7ba80fe | ||
|
|
d86c46aabf | ||
|
|
c79259275a | ||
|
|
4a4760d5b8 | ||
|
|
244e35bb6b | ||
|
|
709c3c600b | ||
|
|
3060b92f8e | ||
|
|
f49f52efe4 | ||
|
|
6a4ca793c9 | ||
|
|
61b67f8bed | ||
|
|
ac690b6332 | ||
|
|
b613e4e9dc | ||
|
|
a9939d5067 | ||
|
|
7a25335028 | ||
|
|
8befc1d017 | ||
|
|
aaf6ae80c5 | ||
|
|
8faa5dabd5 | ||
|
|
0b891e7c28 | ||
|
|
2736777b8a | ||
|
|
96c789a012 | ||
|
|
e13aa2fde6 | ||
|
|
6ffcdf2b8e | ||
|
|
e6d6ff41a9 | ||
|
|
0e45e89a1e | ||
|
|
2df533ab5b | ||
|
|
7e32008bfe | ||
|
|
23cfd2d61b | ||
|
|
0e6231dfbd | ||
|
|
4ebf67095a | ||
|
|
5902bc010e | ||
|
|
36217401c8 | ||
|
|
e6381f96e9 | ||
|
|
efe394c8c4 | ||
|
|
a91287c2ff | ||
|
|
28a4b37bb5 | ||
|
|
2e1f2e481b | ||
|
|
714c0f9e02 | ||
|
|
35421ede58 | ||
|
|
26e0b1db32 | ||
|
|
50947e585d | ||
|
|
40ae829186 | ||
|
|
87ee388bfb | ||
|
|
b76fbb130a | ||
|
|
f3c66ecf03 | ||
|
|
83e4348ace | ||
|
|
af1c09ae02 | ||
|
|
6e5bce26e8 | ||
|
|
d0d2ed3ef1 | ||
|
|
450190a1aa | ||
|
|
74aab13e3e | ||
|
|
c5cd12b6b2 | ||
|
|
fd650f10b6 | ||
|
|
3e1e4be4b3 | ||
|
|
d0c9de9718 | ||
|
|
1a1b28d09f | ||
|
|
6c4ee06d27 | ||
|
|
7c5ffe9c07 | ||
|
|
196644683c | ||
|
|
20ee6e9695 | ||
|
|
e9abedbadb | ||
|
|
5d0a148fea | ||
|
|
c0cf6237e6 | ||
|
|
9876b6a12e | ||
|
|
90815f2271 | ||
|
|
e83ddbb15d | ||
|
|
78f458112b | ||
|
|
f6d3e519e2 | ||
|
|
9bac7306a4 | ||
|
|
030708e7cc | ||
|
|
0b8a258f7f | ||
|
|
2c25193adf | ||
|
|
2c1883c835 | ||
|
|
960c268224 | ||
|
|
c4e5094ce5 | ||
|
|
74f6236ba6 | ||
|
|
9b8edb6a39 | ||
|
|
56e9430155 | ||
|
|
a89ccbdab7 | ||
|
|
fa4819b353 | ||
|
|
8de7941063 | ||
|
|
34158a7a9c | ||
|
|
3384db7193 | ||
|
|
7de7c85426 | ||
|
|
8e65c555e0 | ||
|
|
2557e6b73d | ||
|
|
4f75ee06a0 | ||
|
|
717adc9e9c | ||
|
|
15adeb7cfa | ||
|
|
0b06036a39 | ||
|
|
f7d4dcbff4 | ||
|
|
8334f2b0b2 | ||
|
|
5cccb1caa0 | ||
|
|
db9cdeb1c9 | ||
|
|
3abae8e49e | ||
|
|
bedb356b02 | ||
|
|
34a7dc57b2 | ||
|
|
d0dbb42492 | ||
|
|
1393b117cf | ||
|
|
cf407c63a2 | ||
|
|
3f00e8ef6d | ||
|
|
758b03de36 | ||
|
|
76d55f8e44 | ||
|
|
b9392ab4c9 | ||
|
|
dadf2d1bde | ||
|
|
74c2a3d6e7 | ||
|
|
2454fcf47c | ||
|
|
96a7a429e1 | ||
|
|
f29800ed5b | ||
|
|
15ce9170f3 | ||
|
|
9a144c1c6f | ||
|
|
f897c0f197 | ||
|
|
671fa949df | ||
|
|
612352d69f | ||
|
|
ef31263797 | ||
|
|
f64fbd4a2e | ||
|
|
dffb8a726b | ||
|
|
14d4886e61 | ||
|
|
0012b0a797 | ||
|
|
f6b2a8c66d | ||
|
|
c0a1d31c8d | ||
|
|
329ae055ee | ||
|
|
7ab0f33201 | ||
|
|
2e8ba39b8f | ||
|
|
c5f9f71e45 | ||
|
|
9bb597f366 | ||
|
|
786bddc478 | ||
|
|
d9edcf2f48 | ||
|
|
2262445b75 | ||
|
|
0c486e76c0 | ||
|
|
7d50f4e620 | ||
|
|
b2ec371709 | ||
|
|
549cdd9bfb | ||
|
|
44787b31cf | ||
|
|
e75e653c58 | ||
|
|
3eae013286 | ||
|
|
2221fb22a0 | ||
|
|
9dc3e6a12d | ||
|
|
de0024cd03 | ||
|
|
ed4e970194 | ||
|
|
b0067e31ba | ||
|
|
7e8ba4709e | ||
|
|
be7b2642fc | ||
|
|
896d9e3f72 | ||
|
|
be6666373b | ||
|
|
91df9392c0 | ||
|
|
bd058ee912 | ||
|
|
997f951d0c | ||
|
|
09886ad933 | ||
|
|
0de0b5d0cb | ||
|
|
0141eecef1 | ||
|
|
65ecc20c90 | ||
|
|
f86a4a93a7 | ||
|
|
5cae4b5adf | ||
|
|
d8afa3861d | ||
|
|
3c4d73665e | ||
|
|
8b5f8e1031 | ||
|
|
44461bf5ab | ||
|
|
061bd12f2f | ||
|
|
86c9f89acc | ||
|
|
890994d70d | ||
|
|
35097fed45 | ||
|
|
11912e7b51 | ||
|
|
8e90c9f8d0 | ||
|
|
1e3a706698 | ||
|
|
1b6dc4159a | ||
|
|
69609b1ee2 | ||
|
|
bdd5b9b32e | ||
|
|
870a1196f0 | ||
|
|
c1732c86a5 | ||
|
|
388c330390 | ||
|
|
cb7c29de90 | ||
|
|
fa6a38c50f | ||
|
|
a4f4ddacb7 | ||
|
|
8fa2bcb87b | ||
|
|
7d9863ebed | ||
|
|
89be257ded | ||
|
|
7fd5fcc5b2 | ||
|
|
ea6e4d55b0 | ||
|
|
ccb2836791 | ||
|
|
f4b753b19f | ||
|
|
927d507fd1 | ||
|
|
d662849424 | ||
|
|
6b8d959491 | ||
|
|
269895ae31 | ||
|
|
47929785e7 | ||
|
|
ab17544c96 | ||
|
|
5ea54f9257 | ||
|
|
fa48cf5435 | ||
|
|
91fd3793c7 | ||
|
|
7b6e9f1748 |
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
Dockerfile
|
||||
*.md
|
||||
bin
|
||||
.idea
|
||||
59
.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: 🐛 Bug
|
||||
|
||||
description: Report an issue to help improve the project.
|
||||
|
||||
labels: ["🛠 goal: fix"]
|
||||
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
|
||||
id: description
|
||||
|
||||
attributes:
|
||||
|
||||
label: Description
|
||||
|
||||
description: A brief description of the question or issue, also include what you tried and what didn't work
|
||||
|
||||
validations:
|
||||
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
|
||||
id: screenshots
|
||||
|
||||
attributes:
|
||||
|
||||
label: Screenshots
|
||||
|
||||
description: Please add screenshots if applicable
|
||||
|
||||
validations:
|
||||
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
|
||||
id: extrainfo
|
||||
|
||||
attributes:
|
||||
|
||||
label: Additional information
|
||||
|
||||
description: Is there anything else we should know about this bug?
|
||||
|
||||
validations:
|
||||
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
|
||||
attributes:
|
||||
|
||||
value: |
|
||||
|
||||
You can also join our slack community [here](https://komodorkommunity.slack.com)
|
||||
|
||||
Feel free to check out other cool repositories of the [komodorio](https://github.com/komodorio)
|
||||
68
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
# This configures label matching for PR's.
|
||||
#
|
||||
# The keys are labels, and the values are lists of minimatch patterns
|
||||
# to which those labels apply.
|
||||
#
|
||||
# NOTE: This can only add labels, not remove them.
|
||||
# NOTE: Due to YAML syntax limitations, patterns or labels which start
|
||||
# with a character that is part of the standard YAML syntax must be
|
||||
# quoted.
|
||||
#
|
||||
# Please keep the labels sorted and deduplicated.
|
||||
|
||||
api:
|
||||
- pkg/dashboard/api.go
|
||||
|
||||
app:
|
||||
- main.go
|
||||
- pkg/dashboard/server.go
|
||||
- pkg/dashboard/subproc/*
|
||||
- pkg/dashboard/utils/*
|
||||
|
||||
backend:
|
||||
- pkg/dashboard/handlers/*
|
||||
- pkg/dashboard/scanners/*
|
||||
|
||||
ci:
|
||||
- .github/workflow/build.yml
|
||||
- ci/*
|
||||
- Makefile
|
||||
- scripts/*
|
||||
|
||||
docs:
|
||||
- CODE_OF_CONDUCT.md
|
||||
- CONTRIBUTING.md
|
||||
- LICENSE
|
||||
- README.md
|
||||
- screenshot*.png
|
||||
- screenshot*.svg
|
||||
|
||||
docker:
|
||||
- .dockerignore
|
||||
- Dockerfile
|
||||
|
||||
helm-charts:
|
||||
- charts/*
|
||||
|
||||
github-actions:
|
||||
- .github/ISSUE_TEMPLATE/*
|
||||
- .github/labeler.yml
|
||||
- .github/pull_request_template
|
||||
- .github/workflow/pull-request-labeler.yaml
|
||||
|
||||
release:
|
||||
- .github/workflows/publish-chart.yaml
|
||||
- .github/workflows/release.yaml
|
||||
- .goreleaser.yml
|
||||
- artifacthub-repo.yml
|
||||
- plugin.yaml
|
||||
|
||||
scanners:
|
||||
- pkg/dashboard/scanners/*
|
||||
|
||||
tests:
|
||||
- pkg/dashboard/**/*_test.go
|
||||
- pkg/dashboard/objects/testdata/*
|
||||
|
||||
frontend:
|
||||
- pkg/dashboard/static/*
|
||||
21
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
## Changes Proposed
|
||||
|
||||
<!-- Describe the proposed changes and any additional information -->
|
||||
|
||||
<!-- Add all the screenshots which illustrate your changes -->
|
||||
|
||||
## Check List
|
||||
|
||||
<!-- Mark all the applicable boxes. To mark the box as done follow the following conventions -->
|
||||
<!--
|
||||
[x] - Correct; marked as done
|
||||
[X] - Correct; marked as done
|
||||
|
||||
[ ] - Not correct; marked as **not** done
|
||||
-->
|
||||
|
||||
- [ ] The title of my pull request is a short description of the changes
|
||||
- [ ] This PR relates to some issue: <!-- use "Closes #999" to auto-close related issue -->
|
||||
- [ ] I have documented the changes made (if applicable)
|
||||
- [ ] I have covered the changes with unit tests
|
||||
|
||||
44
.github/workflows/build.yml
vendored
@@ -2,9 +2,9 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches: main
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
go-version: 1.18
|
||||
- name: Unit tests
|
||||
run: |
|
||||
go test -v -race ./... # Run all the tests with the race detector enabled
|
||||
go test -v -race ./... -covermode=atomic # Run all the tests with the race detector enabled
|
||||
- name: Static analysis
|
||||
run: |
|
||||
go vet ./... # go vet is the official Go static analyzer
|
||||
@@ -34,4 +34,40 @@ jobs:
|
||||
- name: Test Binary is Runnable
|
||||
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v3.3.1
|
||||
with:
|
||||
# version: latest
|
||||
# skip-go-installation: true
|
||||
skip-pkg-cache: true
|
||||
skip-build-cache: true
|
||||
# args: --timeout=15m
|
||||
|
||||
image:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- 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@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Helm Template Check For Sanity
|
||||
uses: igabaydulin/helm-check-action@0.1.4
|
||||
env:
|
||||
CHART_LOCATION: ./charts/helm-dashboard
|
||||
CHART_VALUES: ./charts/helm-dashboard/values.yaml
|
||||
- name: Test Helm plugin install script is runnable
|
||||
run: |
|
||||
scripts/install_plugin.sh
|
||||
|
||||
43
.github/workflows/publish-chart.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: publish helm chart
|
||||
|
||||
# for manual running in case we need to update the chart without releasing the dashboard app
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
HELM_REP: helm-charts
|
||||
GH_OWNER: komodorio
|
||||
CHART_DIR: charts/helm-dashboard
|
||||
|
||||
jobs:
|
||||
publish_chart:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Bump versions
|
||||
run: |
|
||||
git config user.email komi@komodor.io
|
||||
git config user.name komodor-bot
|
||||
git fetch --tags
|
||||
git checkout main
|
||||
sh ./ci/bump-versions.sh
|
||||
git add charts/helm-dashboard/Chart.yaml
|
||||
git commit -m "Increment chart versions [skip ci]" || echo "Already up-to-date"
|
||||
git push -f || echo "Nothing to push!"
|
||||
env:
|
||||
APP_VERSION: ${{ needs.pre_release.outputs.release_tag }}
|
||||
- name: Push folder to helm-charts repository
|
||||
uses: crykn/copy_folder_to_another_repo_action@v1.0.6
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.KOMI_WORKFLOW_TOKEN }}
|
||||
with:
|
||||
source_folder: "charts/helm-dashboard"
|
||||
destination_repo: "komodorio/helm-charts"
|
||||
destination_folder: "charts/helm-dashboard"
|
||||
user_email: "komi@komodor.io"
|
||||
user_name: "komodor-bot"
|
||||
destination_branch: "master"
|
||||
commit_msg: "feat(helm-dashboard): update chart" #important!! don't change this commit message unless you change the condition in pipeline.yml on helm-charts repo
|
||||
14
.github/workflows/pull-request-labeler.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
87
.github/workflows/release.yaml
vendored
@@ -3,10 +3,29 @@ name: release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
HELM_REP: helm-charts
|
||||
GH_OWNER: komodorio
|
||||
CHART_DIR: charts/helm-dashboard
|
||||
|
||||
jobs:
|
||||
pre_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Get tag name
|
||||
id: get_tag_name
|
||||
run: echo "TAG_NAME=$(echo ${{ github.ref_name }} | cut -d 'v' -f2)" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
release_tag: ${{ steps.get_tag_name.outputs.TAG_NAME }}
|
||||
|
||||
release:
|
||||
needs: pre_release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -28,3 +47,69 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Test Binary Versions
|
||||
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
|
||||
|
||||
image:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [release, pre_release]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
uses: docker/metadata-action@v3
|
||||
id: meta
|
||||
with:
|
||||
images: komodorio/helm-dashboard
|
||||
|
||||
- name: 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@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Bump versions
|
||||
run: |
|
||||
git config user.email komi@komodor.io
|
||||
git config user.name komodor-bot
|
||||
git fetch --tags
|
||||
git checkout main
|
||||
sh ./ci/bump-versions.sh
|
||||
git add charts/helm-dashboard/Chart.yaml
|
||||
git add plugin.yaml
|
||||
git commit -m "Increment chart versions [skip ci]" || echo "Already up-to-date"
|
||||
git push -f || echo "Nothing to push!"
|
||||
env:
|
||||
APP_VERSION: ${{ needs.pre_release.outputs.release_tag }}
|
||||
- name: Push folder to helm-charts repository
|
||||
uses: crykn/copy_folder_to_another_repo_action@v1.0.6
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.KOMI_WORKFLOW_TOKEN }}
|
||||
with:
|
||||
source_folder: "charts/helm-dashboard"
|
||||
destination_repo: "komodorio/helm-charts"
|
||||
destination_folder: "charts/helm-dashboard"
|
||||
user_email: "komi@komodor.io"
|
||||
user_name: "komodor-bot"
|
||||
destination_branch: "master"
|
||||
commit_msg: "feat(helm-dashboard): ${{ github.event.head_commit.message }}" #important!! don't change this commit message unless you change the condition in pipeline.yml on helm-charts repo
|
||||
|
||||
5
.gitignore
vendored
@@ -15,6 +15,7 @@
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
*.cov
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
@@ -24,3 +25,7 @@ go.work
|
||||
|
||||
/bin
|
||||
/.idea/
|
||||
|
||||
.DS_Store
|
||||
.vscode/
|
||||
/pkg/dashboard/objects/testdata/hello-world-0.1.0.tgz
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
builds:
|
||||
- main: ./main.go
|
||||
binary: helm-dashboard
|
||||
ldflags: -s -w -X main.version={{.Version}} -X main.version={{.Version}} -X main.version={{.Version}} -X main.date={{.Date}}
|
||||
ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
|
||||
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
itiel@komodor.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
55
CONTRIBUTING.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Contributing to Helm Dashboard
|
||||
|
||||
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
|
||||
|
||||
- Reporting a bug
|
||||
- Discussing the current state of the code
|
||||
- Submitting a fix
|
||||
- Proposing new features
|
||||
|
||||
## We Develop with GitHub
|
||||
|
||||
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
|
||||
## First-time contributors
|
||||
|
||||
We've tagged some issues to make it easy to get started :smile:
|
||||
[Good first issues](https://github.com/komodorio/helm-dashboard/labels/good%20first%20issue)
|
||||
|
||||
Add a comment on the issue and wait for the issue to be assigned before you start working on it. This helps to avoid multiple people working on similar issues.
|
||||
|
||||
## All Code Changes Happen Through Pull Requests
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:
|
||||
|
||||
1. Fork the repo and create your branch from `main`.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. Ensure the test suite passes.
|
||||
4. Make sure your code lints.
|
||||
5. Issue that pull request!
|
||||
|
||||
## Any contributions you make will be under the Apache License 2.0
|
||||
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) that covers the project.
|
||||
|
||||
## Report bugs using GitHub's [issues](https://github.com/komodorio/helm-dashboard/issues)
|
||||
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/komodorio/helm-dashboard/issues/new) and labeling it with the `bug` label. It's that easy!
|
||||
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- Be specific!
|
||||
- Give sample code if you can.
|
||||
- What you expected would happen
|
||||
- What actually happens
|
||||
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its Apache License 2.0.
|
||||
|
||||
## Questions?
|
||||
|
||||
Contact us on [Slack](https://komodorkommunity.slack.com).
|
||||
43
Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# Stage - builder
|
||||
FROM golang as builder
|
||||
|
||||
|
||||
ENV GOOS=linux
|
||||
ENV GOARCH=amd64
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY go.mod ./
|
||||
COPY go.sum ./
|
||||
COPY main.go ./
|
||||
RUN go mod download
|
||||
|
||||
ARG VER=0.0.0
|
||||
ENV VERSION=${VER}
|
||||
|
||||
ADD . src
|
||||
|
||||
WORKDIR /build/src
|
||||
|
||||
RUN make build
|
||||
|
||||
# Stage - runner
|
||||
FROM alpine
|
||||
EXPOSE 8080
|
||||
|
||||
# Python
|
||||
RUN apk add --update --no-cache python3 curl && python3 -m ensurepip && pip3 install --upgrade pip setuptools
|
||||
|
||||
# Trivy
|
||||
RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.18.3
|
||||
RUN trivy --version
|
||||
|
||||
# Checkov scanner
|
||||
RUN pip3 install checkov packaging==21.3 && checkov --version
|
||||
|
||||
COPY --from=builder /build/src/bin/dashboard /bin/helm-dashboard
|
||||
|
||||
ENTRYPOINT ["/bin/helm-dashboard", "--no-browser", "--bind=0.0.0.0", "--port=8080"]
|
||||
|
||||
# docker build . -t komodorio/helm-dashboard:0.0.0 && kind load docker-image komodorio/helm-dashboard:0.0.0
|
||||
24
Makefile
Normal file
@@ -0,0 +1,24 @@
|
||||
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")
|
||||
|
||||
.PHONY: test
|
||||
test: ; $(info $(M) start unit testing...) @
|
||||
@go test $$(go list ./... | grep -v /mocks/) --race -v -short -coverpkg=./... -coverprofile=profile.cov
|
||||
@echo "\n*****************************"
|
||||
@echo "** TOTAL COVERAGE: $$(go tool cover -func profile.cov | grep total | grep -Eo '[0-9]+\.[0-9]+')% **"
|
||||
@echo "*****************************\n"
|
||||
|
||||
.PHONY: pull
|
||||
pull: ; $(info $(M) Pulling source...) @
|
||||
@git pull
|
||||
|
||||
.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
|
||||
206
README.md
@@ -1,72 +1,180 @@
|
||||
# <img src="pkg/dashboard/static/logo.png" height=30 style="height: 2rem"> Helm Dashboard
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="pkg/dashboard/static/logo-header-inverted.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="pkg/dashboard/static/logo-header.svg#gh-light-mode-only">
|
||||
<img alt="Helm Dashboard" src="pkg/dashboard/static/logo-header.svg#gh-light-mode-only">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
A simplified way of working with Helm.
|
||||
<p align="center">A simplified way of working with Helm.</p>
|
||||
|
||||
[<img src="screenshot.png" style="width: 100%; border: 1px solid silver">](screenshot.png)
|
||||
|
||||
## Local Testing
|
||||
 [](https://github.com/komodorio/helm-dashboard/issues)      [](https://github.com/komodorio/helm-dashboard)
|
||||
|
||||
<kbd>[<img src="images/screenshot.png" style="width: 100%; border: 1px solid silver;" border="1" alt="Screenshot">](images/screenshot.png)</kbd>
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
_Helm Dashboard_ is an **open-source project** which offers a UI-driven way to view the installed Helm charts, see their revision history and
|
||||
corresponding k8s resources. Also, you can perform simple actions like roll back to a revision or upgrade to newer
|
||||
version.
|
||||
This project is part of [Komodor's](https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash-gh) vision of
|
||||
helping Kubernetes users to navigate and troubleshoot their clusters, the project is **NOT** an offical project by the [helm team](https://helm.sh/).
|
||||
|
||||
Key capabilities of the tool:
|
||||
|
||||
- See all installed charts and their revision history
|
||||
- See manifest diff of the past revisions
|
||||
- Browse k8s resources resulting from the chart
|
||||
- Easy rollback or upgrade version with a clear and easy manifest diff
|
||||
- Integration with popular problem scanners
|
||||
- Easy switch between multiple clusters
|
||||
- Can be used locally, or installed into Kubernetes cluster
|
||||
- Does not require Helm or Kubectl installed
|
||||
|
||||
## Setup
|
||||
|
||||
### Standalone Binary
|
||||
|
||||
Since version 1.0, the recommended install method is to just use standalone binary. It does not require Helm or kubectl to be installed.
|
||||
|
||||
Download the appropriate [release package](https://github.com/komodorio/helm-dashboard/releases) for your platform, unpack it and just run `dashboard` binary from it.
|
||||
|
||||
### Using Helm plugin manager
|
||||
|
||||
To install dashboard as Helm plugin, simply run Helm command:
|
||||
|
||||
```shell
|
||||
helm plugin install https://github.com/komodorio/helm-dashboard.git
|
||||
```
|
||||
|
||||
To update the plugin to the latest version, run:
|
||||
|
||||
```shell
|
||||
helm plugin update dashboard
|
||||
```
|
||||
|
||||
To uninstall, run:
|
||||
|
||||
```shell
|
||||
helm plugin uninstall dashboard
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```shell
|
||||
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`.
|
||||
|
||||
> Precedence order: flag `--bind=<host>` > env `HD_BIND=<host>` > default value `localhost`
|
||||
|
||||
If your port 8080 is busy, you can specify a different port to use via `--port <number>` command-line flag.
|
||||
|
||||
If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line. You can specify multiple namespaces, separated by commas.
|
||||
|
||||
If you don't want browser tab to automatically open, add `--no-browser` flag in your command line.
|
||||
|
||||
If you want to increase the logging verbosity and see all the debug info, use the `--verbose` flag.
|
||||
|
||||
> Disclaimer: For the sake of improving the project quality, there is user analytics collected by the tool. You can disable this collecting with `--no-analytics` option. The collection is done via DataDog RUM and Heap Analytics. Only the anonymous data is collected, no sensitive information is used.
|
||||
|
||||
### Deploying Helm Dashboard on Kubernetes
|
||||
|
||||
The official helm chart is [available here](https://github.com/komodorio/helm-charts/blob/master/charts/helm-dashboard)
|
||||
|
||||
## Selected Features
|
||||
|
||||
### Support for Local Charts
|
||||
|
||||
Local Helm chart is a directory with specially named files and a `Chart.yaml` file, which you can install via Helm, without the need to publish the chart into Helm repository. Chart developers might want to experiment with the chart locally, before uploading into public repository. Also, a proprietary application might only use non-published chart as an approach to deploy the software.
|
||||
|
||||
For all the above use-cases, you may use Helm Dashboard UI, spcifying location of your local chart folders via special `--local-chart` command-line parameter. The parameter might be specified multiple times, for example:
|
||||
|
||||
```shell
|
||||
helm-dashboard --local-chart=/opt/charts/my-private-app --local-chart=/home/dev/sources/app/chart
|
||||
```
|
||||
|
||||
When _valid_ local chart sources specified, the repository list would contain a surrogate `[local]` entry, with those charts listed inside. All the chart operations are normal: installing, reconfiguring and upgrading.
|
||||
|
||||

|
||||
|
||||
### Execute Helm tests
|
||||
|
||||
For all the release(s) (installed helm charts), you can execute helm tests for that release. For the tests to execute successfully, you need to have existing tests for that helm chart.
|
||||
|
||||
You can execute `helm test` for the specific release as below:
|
||||

|
||||
|
||||
The result of executed `helm test` for the release will be disapled as below:
|
||||

|
||||
|
||||
### Scanner Integrations
|
||||
|
||||
Upon startup, Helm Dashboard detects the presence of [Trivy](https://github.com/aquasecurity/trivy)
|
||||
and [Checkov](https://github.com/bridgecrewio/checkov) scanners. When available, these scanners are offered on k8s
|
||||
resources page, as well as install/upgrade preview page.
|
||||
|
||||
You can request scanning of the specific k8s resource in your cluster:
|
||||

|
||||
|
||||
If you want to validate the k8s manifest prior to installing/reconfiguring a Helm chart, look for "Scan for Problems"
|
||||
button at the bottom of the dialog:
|
||||

|
||||
|
||||
## Support Channels
|
||||
|
||||
We have two main channels for supporting the Helm Dashboard
|
||||
users: [Slack community](https://komodorkommunity.slack.com) for general conversations
|
||||
and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
|
||||
|
||||
## Contributing
|
||||
|
||||
Kindly read our [Contributing Guide](CONTRIBUTING.md) to learn and understand about our development process, how to propose bug fixes and improvements, and how to build and test your changes to Helm Dashboard. <br>
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/komodorio/helm-dashboard/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=komodorio/helm-dashboard" />
|
||||
</a>
|
||||
|
||||
## Local Dev Testing
|
||||
|
||||
Prerequisites: `helm` and `kubectl` binaries installed and operational.
|
||||
|
||||
Until we make our repo public, we have to use a custom way to install the plugin.
|
||||
|
||||
There is a need to build binary for plugin to function, run:
|
||||
|
||||
```shell
|
||||
go build -o bin/dashboard .
|
||||
```
|
||||
|
||||
You can just run the `bin/dashboard` binary directly, it will just work.
|
||||
|
||||
To install, checkout the source code and run from source dir:
|
||||
|
||||
```shell
|
||||
helm plugin install .
|
||||
```
|
||||
|
||||
Local install of plugin just creates a symlink, so making the changes and rebuilding the binary would not require to reinstall a plugin.
|
||||
Local installation of plugin just creates a symlink, so making the changes and rebuilding the binary would not require
|
||||
to
|
||||
reinstall a plugin.
|
||||
|
||||
To use the plugin, run in your terminal:
|
||||
|
||||
```shell
|
||||
helm dashboard
|
||||
```
|
||||
|
||||
Then, use the web UI.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
To uninstall, run:
|
||||
```shell
|
||||
helm plugin uninstall dashboard
|
||||
```
|
||||
|
||||
## Support Channels
|
||||
|
||||
We have two main channels for supporting the tool users: [Slack community](#TODO) for general conversations and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Internal Milestone 1
|
||||
- Helm Plugin Packaging
|
||||
- CLI launcher
|
||||
- Web Server with REST API
|
||||
|
||||
|
||||
### First Public Version
|
||||
Listing the installed applications
|
||||
View k8s resources created by the application (describe, status)
|
||||
Viewing revision history for application
|
||||
View manifest diffs between revisions, also changelogs etc
|
||||
Analytics reporting (telemetry)
|
||||
|
||||
### Further Ideas
|
||||
Setting parameter values and installing
|
||||
Installing new app from repo
|
||||
Uninstalling the app completely
|
||||
Reconfiguring the application
|
||||
Rollback a revision
|
||||
|
||||
Validate manifests before deploy and get better errors
|
||||
Switch clusters (?)
|
||||
Browsing repositories
|
||||
Adding new repository
|
||||
|
||||
Recognise & show ArgoCD-originating charts/objects
|
||||
Have cleaner idea on the web API structure
|
||||
See if we can build in Chechov or Validkube validation
|
||||
|
||||
5
artifacthub-repo.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
# Artifact Hub repository metadata file
|
||||
repositoryID: 9ed6d12d-b3d5-4efd-836e-3ac9fa9dd3d1
|
||||
owners:
|
||||
- name: komodor-bot
|
||||
email: komi@komodor.io
|
||||
23
charts/helm-dashboard/.helmignore
Normal 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/
|
||||
9
charts/helm-dashboard/Chart.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
|
||||
name: helm-dashboard
|
||||
description: A GUI Dashboard for Helm by Komodor
|
||||
icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/main/pkg/dashboard/static/logo.svg"
|
||||
|
||||
version: 0.1.5
|
||||
appVersion: "1.1.1"
|
||||
85
charts/helm-dashboard/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Helm Dashboard
|
||||
|
||||
## TL;DR;
|
||||
|
||||
```bash
|
||||
helm repo add komodorio https://helm-charts.komodor.io
|
||||
helm repo update
|
||||
helm upgrade --install helm-dashboard komodorio/helm-dashboard
|
||||
```
|
||||
|
||||
## Introduction
|
||||
|
||||
This chart bootstraps a Helm Dashboard deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
|
||||
|
||||
While installed inside cluster, Helm Dashboard will run some additional backgroud actions, for example, will automatically update Helm repositories. To enable that behavior locally, set `HD_CLUSTER_MODE` env variable.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.16+
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
To install the chart with the release name `helm-dashboard`:
|
||||
|
||||
```bash
|
||||
helm install helm-dashboard .
|
||||
```
|
||||
|
||||
The command deploys Helm Dashboard on the Kubernetes cluster in the default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation.
|
||||
|
||||
> **Tip**: List all releases using `helm list`
|
||||
|
||||
## Uninstalling the Chart
|
||||
|
||||
To uninstall/delete the `helm-dashboard` deployment:
|
||||
|
||||
```bash
|
||||
helm uninstall helm-dashboard
|
||||
```
|
||||
|
||||
The command removes all the Kubernetes components associated with the chart and deletes the release.
|
||||
|
||||
## Adding Authentication
|
||||
|
||||
The task of authentication and user control is out of scope for Helm Dashboard. Luckily, there are third-party solutions which are dedicated to provide that functionality.
|
||||
|
||||
For instance, you can place authentication proxy in front of Helm Dashboard, like this one: https://github.com/oauth2-proxy/oauth2-proxy
|
||||
|
||||
## Parameters
|
||||
|
||||
The following table lists the configurable parameters of the chart and their default values.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| ------------------------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `image.repository` | Image registry/name | `docker.io/komodorio/helm-dashboard` |
|
||||
| `image.tag` | Image tag | |
|
||||
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
|
||||
| `replicaCount` | Number of dashboard Pods to run | `1` |
|
||||
| `dashboard.allowWriteActions` | Enables write actions. Allow modifying, deleting and creating charts and kubernetes resources. | `true` |
|
||||
| `resources.requests.cpu` | CPU resource requests | `200m` |
|
||||
| `resources.limits.cpu` | CPU resource limits | `1` |
|
||||
| `resources.requests.memory` | Memory resource requests | `256Mi` |
|
||||
| `resources.limits.memory` | Memory resource limits | `1Gi` |
|
||||
| `service.type ` | Kubernetes service type | `ClusterIP` |
|
||||
| `service.port ` | Kubernetes service port | `8080` |
|
||||
| `serviceAccount.create` | Creates a service account | `true` |
|
||||
| `serviceAccount.name` | Optional name for the service account | `{RELEASE_FULLNAME}` |
|
||||
| `nodeSelector` | Node labels for pod assignment | |
|
||||
| `affinity` | Affinity settings for pod assignment | |
|
||||
| `tolerations` | Tolerations for pod assignment | |
|
||||
| `dashboard.persistence.enabled` | Enable helm data persistene using PVC | `true` |
|
||||
| `dashboard.persistence.accessModes` | Persistent Volume access modes | `["ReadWriteOnce"]` |
|
||||
| `dashboard.persistence.storageClass` | Persistent Volume storage class | `""` |
|
||||
| `dashboard.persistence.size` | Persistent Volume size | `100M` |
|
||||
| `dashboard.persistence.hostPath` | Set path in case you want to use local host path volumes (not recommended in production) | `""`
|
||||
| `updateStrategy.type` | Set up update strategy for helm-dashboard installation. | `RollingUpdate` |
|
||||
| `extraArgs` | Set the arguments to be supplied to the helm-dashboard binary | `[--no-browser, --bind=0.0.0.0]`
|
||||
|
||||
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`.
|
||||
|
||||
```bash
|
||||
helm upgrade --install helm-dashboard komodorio/helm-dashboard --set dashboard.allowWriteActions=true --set service.port=9090
|
||||
```
|
||||
|
||||
> **Tip**: You can use the default [values.yaml](values.yaml)
|
||||
16
charts/helm-dashboard/templates/NOTES.txt
Normal 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
|
||||
|
||||
62
charts/helm-dashboard/templates/_helpers.tpl
Normal 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 }}
|
||||
93
charts/helm-dashboard/templates/deployment.yaml
Normal file
@@ -0,0 +1,93 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.fullname" . }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "helm-dashboard.selectorLabels" . | nindent 6 }}
|
||||
strategy: {{- toYaml .Values.updateStrategy | nindent 4 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
command:
|
||||
- /bin/helm-dashboard
|
||||
args:
|
||||
{{- with .Values.extraArgs }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: HELM_CACHE_HOME
|
||||
value: /opt/dashboard/helm/cache
|
||||
- name: HELM_CONFIG_HOME
|
||||
value: /opt/dashboard/helm/config
|
||||
- name: HELM_DATA_HOME
|
||||
value: /opt/dashboard/helm/data
|
||||
- name: DEBUG
|
||||
value: {{- ternary " 1" "" .Values.debug }}
|
||||
{{- if .Values.dashboard.namespace }}
|
||||
- name: HELM_NAMESPACE
|
||||
value: {{ .Values.dashboard.namespace }}
|
||||
{{end}}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /status
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /status
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /opt/dashboard/helm
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: data
|
||||
{{- if .Values.dashboard.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "helm-dashboard.fullname" . }}
|
||||
{{- else }}
|
||||
emptyDir: { }
|
||||
{{- end }}
|
||||
|
||||
61
charts/helm-dashboard/templates/ingress.yaml
Normal 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 }}
|
||||
54
charts/helm-dashboard/templates/pvc.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
{{- if .Values.dashboard.persistence.enabled -}}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.fullname" . }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.labels" . | nindent 4 }}
|
||||
{{- with .Values.dashboard.persistence.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.dashboard.persistence.hostPath }}
|
||||
storageClassName: ""
|
||||
{{- else }}
|
||||
storageClassName: "{{ .Values.dashboard.persistence.storageClass }}"
|
||||
{{- end }}
|
||||
accessModes:
|
||||
{{- if not (empty .Values.dashboard.persistence.accessModes) }}
|
||||
{{- range .Values.dashboard.persistence.accessModes }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.dashboard.persistence.size | quote }}
|
||||
{{- end }}
|
||||
|
||||
---
|
||||
{{- if and .Values.dashboard.persistence.enabled .Values.dashboard.persistence.hostPath -}}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.fullname" . }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.labels" . | nindent 4 }}
|
||||
{{- with .Values.dashboard.persistence.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{- if not (empty .Values.dashboard.persistence.accessModes) }}
|
||||
{{- range .Values.dashboard.persistence.accessModes }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
capacity:
|
||||
storage: {{ .Values.dashboard.persistence.size | quote }}
|
||||
hostPath:
|
||||
path: {{ .Values.dashboard.persistence.hostPath | quote }}
|
||||
{{- end -}}
|
||||
15
charts/helm-dashboard/templates/service.yaml
Normal 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 }}
|
||||
39
charts/helm-dashboard/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
---
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
rules:
|
||||
- apiGroups: ["*"]
|
||||
resources: ["*"]
|
||||
{{- if .Values.dashboard.allowWriteActions }}
|
||||
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
|
||||
{{- else }}
|
||||
verbs: ["get", "list", "watch"]
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
namespace: {{ .Release.Namespace }}
|
||||
name: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
15
charts/helm-dashboard/templates/tests/test-connection.yaml
Normal 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
|
||||
121
charts/helm-dashboard/values.yaml
Normal file
@@ -0,0 +1,121 @@
|
||||
replicaCount: 1
|
||||
|
||||
# Flag for setting environment to debug mode
|
||||
debug: false
|
||||
|
||||
image:
|
||||
repository: komodorio/helm-dashboard
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 1Gi
|
||||
|
||||
dashboard:
|
||||
allowWriteActions: true
|
||||
|
||||
# default namespace for Helm operations
|
||||
namespace: ""
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
|
||||
## If defined, storageClassName: <storageClass>
|
||||
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||
## GKE, AWS & OpenStack)
|
||||
##
|
||||
storageClass: ""
|
||||
|
||||
## Helm Dashboard Persistent Volume access modes
|
||||
## Must match those of existing PV or dynamic provisioner
|
||||
## Ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
|
||||
##
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
||||
## Helm Dashboard Persistent Volume labels
|
||||
##
|
||||
labels: {}
|
||||
|
||||
## Helm Dashboard Persistent Volume annotations
|
||||
##
|
||||
annotations: {}
|
||||
|
||||
## Set path in case you want to use local host path volumes (not recommended in production)
|
||||
##
|
||||
hostPath: ""
|
||||
|
||||
## Helm Dashboard data Persistent Volume size
|
||||
##
|
||||
size: 100M
|
||||
|
||||
## @param.updateStrategy.type Set up update strategy for helm-dashboard installation.
|
||||
## Set to Recreate if you use persistent volume that cannot be mounted by more than one pods to make sure the pods is destroyed first.
|
||||
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
|
||||
## Example:
|
||||
## updateStrategy:
|
||||
## type: RollingUpdate
|
||||
## rollingUpdate:
|
||||
## maxSurge: 25%
|
||||
## maxUnavailable: 25%
|
||||
##
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
|
||||
securityContext: {}
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
extraArgs:
|
||||
- --no-browser
|
||||
- --bind=0.0.0.0
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
15
ci/bump-versions.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
WORKING_DIRECTORY="$PWD"
|
||||
|
||||
[ -z "$HELM_CHARTS_SOURCE" ] && HELM_CHARTS_SOURCE="$WORKING_DIRECTORY/charts/helm-dashboard"
|
||||
|
||||
[ -z "$APP_VERSION" ] && {
|
||||
APP_VERSION=$(cat ${HELM_CHARTS_SOURCE}/Chart.yaml | grep 'appVersion:' | awk -F'"' '{print $2}')
|
||||
}
|
||||
|
||||
sed -i -e "s/appVersion.*/appVersion: \"${APP_VERSION}\" /g" ${HELM_CHARTS_SOURCE}/Chart.yaml
|
||||
sed -i -e "s/version.*/version: \"${APP_VERSION}\" /g" plugin.yaml
|
||||
CURRENT_VERSION=$(cat ${HELM_CHARTS_SOURCE}/Chart.yaml | grep 'version:' | awk '{print $2}')
|
||||
NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g')
|
||||
sed -i -e "s/${CURRENT_VERSION}/${NEW_VERSION}/g" ${HELM_CHARTS_SOURCE}/Chart.yaml
|
||||
155
go.mod
@@ -3,104 +3,163 @@ module github.com/komodorio/helm-dashboard
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.1.1
|
||||
github.com/Masterminds/semver/v3 v3.2.0
|
||||
github.com/eko/gocache/v3 v3.1.2
|
||||
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/sirupsen/logrus v1.8.1
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
helm.sh/helm/v3 v3.9.4
|
||||
k8s.io/kubectl v0.24.2
|
||||
github.com/jessevdk/go-flags v1.5.0
|
||||
github.com/joomcode/errorx v1.1.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/rogpeppe/go-internal v1.8.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.4.0
|
||||
helm.sh/helm/v3 v3.11.1
|
||||
k8s.io/api v0.26.0
|
||||
k8s.io/apimachinery v0.26.0
|
||||
k8s.io/cli-runtime v0.26.0
|
||||
k8s.io/client-go v0.26.0
|
||||
k8s.io/kubectl v0.26.0
|
||||
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.3 // indirect
|
||||
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/containerd/containerd v1.6.18 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/cli v20.10.21+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/docker/docker v20.10.21+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
github.com/fatih/camelcase v1.0.0 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.1 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.0.2 // 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.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/swag v0.19.14 // 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/gobwas/glob v0.2.3 // 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-cmp v0.5.6 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.2.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lib/pq v1.10.7 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/lithammer/dedent v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2 // 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/russross/blackfriday v1.5.2 // indirect
|
||||
github.com/spf13/cobra v1.4.0 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rubenv/sql-migrate v1.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.6.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.8.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xlab/treeprint v1.1.0 // indirect
|
||||
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
|
||||
golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
|
||||
golang.org/x/crypto v0.5.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20221110155412-d0897a79cd37 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/term v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
|
||||
google.golang.org/grpc v1.49.0 // 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
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.24.2 // indirect
|
||||
k8s.io/apimachinery v0.24.2 // indirect
|
||||
k8s.io/cli-runtime v0.24.2 // indirect
|
||||
k8s.io/client-go v0.24.2 // indirect
|
||||
k8s.io/component-base v0.24.2 // indirect
|
||||
k8s.io/component-helpers v0.24.2 // indirect
|
||||
k8s.io/klog/v2 v2.60.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect
|
||||
k8s.io/metrics v0.24.2 // indirect
|
||||
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.11.4 // indirect
|
||||
sigs.k8s.io/kustomize/kustomize/v4 v4.5.4 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.26.0 // indirect
|
||||
k8s.io/apiserver v0.26.0 // indirect
|
||||
k8s.io/component-base v0.26.0 // indirect
|
||||
k8s.io/klog/v2 v2.80.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
|
||||
oras.land/oras-go v1.2.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.12.1 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
BIN
images/screenshot.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
images/screenshot_local_charts.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
images/screenshot_run_test.png
Executable file
|
After Width: | Height: | Size: 48 KiB |
BIN
images/screenshot_run_test_result.png
Executable file
|
After Width: | Height: | Size: 60 KiB |
BIN
images/screenshot_scan_manifest.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
images/screenshot_scan_resource.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
128
main.go
@@ -1,50 +1,138 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard"
|
||||
"github.com/pkg/browser"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
version = "0.0.0"
|
||||
commit = "none"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
setupLogging()
|
||||
type options struct {
|
||||
Version bool `long:"version" description:"Show tool version"`
|
||||
Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"`
|
||||
NoBrowser bool `short:"b" long:"no-browser" description:"Do not attempt to open Web browser upon start"`
|
||||
NoTracking bool `long:"no-analytics" description:"Disable user analytics (Heap, DataDog etc.)"`
|
||||
BindHost string `long:"bind" description:"Host binding to start server (default: localhost)"` // default should be printed but not assigned as the precedence: flag > env > default
|
||||
Port uint `short:"p" long:"port" description:"Port to start server on" default:"8080"`
|
||||
Namespace string `short:"n" long:"namespace" description:"Namespace for HELM operations"`
|
||||
Devel bool `long:"devel" description:"Include development versions of charts"`
|
||||
LocalChart []string `long:"local-chart" description:"Specify location of local chart to include into UI"`
|
||||
}
|
||||
|
||||
// TODO: proper command-line parsing
|
||||
if len(os.Args) > 1 { // dirty thing to allow --help to work
|
||||
os.Exit(0)
|
||||
func main() {
|
||||
err := os.Setenv("HD_VERSION", version) // for anyone willing to access it
|
||||
if err != nil {
|
||||
fmt.Println("Failed to remember app version because of error: " + err.Error())
|
||||
}
|
||||
|
||||
address, webServerDone := dashboard.StartServer()
|
||||
opts := parseFlags()
|
||||
if opts.BindHost == "" {
|
||||
host := os.Getenv("HD_BIND")
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
opts.BindHost = host
|
||||
}
|
||||
|
||||
if os.Getenv("HD_NOBROWSER") == "" {
|
||||
opts.Verbose = opts.Verbose || os.Getenv("DEBUG") != ""
|
||||
setupLogging(opts.Verbose)
|
||||
|
||||
server := dashboard.Server{
|
||||
Version: version,
|
||||
Namespaces: strings.Split(opts.Namespace, ","),
|
||||
Address: fmt.Sprintf("%s:%d", opts.BindHost, opts.Port),
|
||||
Debug: opts.Verbose,
|
||||
NoTracking: opts.NoTracking,
|
||||
Devel: opts.Devel,
|
||||
LocalCharts: opts.LocalChart,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
osSignal := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
oscall := <-osSignal
|
||||
log.Warnf("Stopping on signal: %s\n", oscall)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
address, webServerDone, err := server.StartServer(ctx, cancel)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to start Helm Dashboard: %+v", err)
|
||||
}
|
||||
|
||||
if !opts.NoTracking {
|
||||
log.Infof("User analytics is collected to improve the quality, disable it with --no-analytics")
|
||||
}
|
||||
|
||||
if opts.NoBrowser {
|
||||
log.Infof("Access web UI at: %s", address)
|
||||
} else {
|
||||
log.Infof("Opening web UI: %s", address)
|
||||
err := webbrowser.Open(address)
|
||||
err := browser.OpenURL(address)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to open Web browser for URL: %s", err)
|
||||
}
|
||||
} else {
|
||||
log.Infof("Access web UI at: %s", address)
|
||||
}
|
||||
|
||||
<-webServerDone
|
||||
log.Infof("Done.")
|
||||
}
|
||||
|
||||
func setupLogging() {
|
||||
if os.Getenv("DEBUG") == "" {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
} else {
|
||||
func parseFlags() options {
|
||||
ns := os.Getenv("HELM_NAMESPACE")
|
||||
if ns == "default" { // it's how Helm passes to plugin the empty NS, we have to reset it back
|
||||
ns = ""
|
||||
}
|
||||
|
||||
opts := options{Namespace: ns}
|
||||
args, err := flags.Parse(&opts)
|
||||
if err != nil {
|
||||
if e, ok := err.(*flags.Error); ok {
|
||||
if e.Type == flags.ErrHelp {
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// we rely on default behavior to print the problem inside `flags` library
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if opts.Version {
|
||||
fmt.Println(version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
fmt.Println("The program does not take arguments, see --help for usage")
|
||||
os.Exit(1)
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func setupLogging(verbose bool) {
|
||||
if verbose {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
gin.SetMode(gin.DebugMode)
|
||||
log.Debugf("Debug logging is enabled")
|
||||
} else {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
log.Infof("Helm Dashboard by Komodor, version %s (%s @ %s)", version, commit, date)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/handlers"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"html"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var staticFS embed.FS
|
||||
|
||||
func noCache(c *gin.Context) {
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
if c.GetHeader("Cache-Control") == "" { // default policy is not to cache
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
|
||||
@@ -26,185 +28,139 @@ func errorHandler(c *gin.Context) {
|
||||
|
||||
errs := ""
|
||||
for _, err := range c.Errors {
|
||||
log.Debugf("Error: %s", err)
|
||||
log.Debugf("Error: %+v", err)
|
||||
errs += err.Error() + "\n"
|
||||
}
|
||||
|
||||
if errs != "" {
|
||||
c.String(http.StatusInternalServerError, errs)
|
||||
c.String(http.StatusInternalServerError, html.EscapeString(errs))
|
||||
}
|
||||
}
|
||||
|
||||
func NewRouter(abortWeb ControlChan, data *DataLayer) *gin.Engine {
|
||||
func contextSetter(data *objects.DataLayer) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctxName := ""
|
||||
if ctx, ok := c.Request.Header["X-Kubecontext"]; ok {
|
||||
ctxName = ctx[0]
|
||||
if err := data.SetContext(ctxName); err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
app, err := data.AppForCtx(ctxName)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(handlers.APP, app)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func NewRouter(abortWeb context.CancelFunc, data *objects.DataLayer, debug bool) *gin.Engine {
|
||||
var api *gin.Engine
|
||||
if os.Getenv("DEBUG") == "" {
|
||||
if debug {
|
||||
api = gin.New()
|
||||
api.Use(gin.Recovery())
|
||||
} else {
|
||||
api = gin.Default()
|
||||
}
|
||||
|
||||
api.Use(noCache)
|
||||
api.Use(contextSetter(data))
|
||||
api.Use(noCache)
|
||||
api.Use(errorHandler)
|
||||
configureStatic(api)
|
||||
|
||||
configureStatic(api)
|
||||
configureRoutes(abortWeb, data, api)
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
func configureRoutes(abortWeb ControlChan, data *DataLayer, api *gin.Engine) {
|
||||
func configureRoutes(abortWeb context.CancelFunc, data *objects.DataLayer, api *gin.Engine) {
|
||||
// server shutdown handler
|
||||
api.DELETE("/", func(c *gin.Context) {
|
||||
abortWeb <- struct{}{}
|
||||
abortWeb()
|
||||
c.Status(http.StatusAccepted)
|
||||
})
|
||||
|
||||
api.GET("/api/helm/charts", func(c *gin.Context) {
|
||||
res, err := data.ListInstalled()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
api.GET("/status", func(c *gin.Context) {
|
||||
c.Header("X-Application-Name", "Helm Dashboard by Komodor.io") // to identify ourselves by ourselves
|
||||
c.IndentedJSON(http.StatusOK, data.GetStatus())
|
||||
})
|
||||
|
||||
api.GET("/api/helm/charts/history", func(c *gin.Context) {
|
||||
cName := c.Query("chart")
|
||||
cNamespace := c.Query("namespace")
|
||||
if cName == "" {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
|
||||
return
|
||||
}
|
||||
|
||||
res, err := data.ChartHistory(cNamespace, cName)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
api.GET("/api/cache", func(c *gin.Context) { // TODO: included into OpenAPI or not?
|
||||
c.IndentedJSON(http.StatusOK, data.Cache)
|
||||
})
|
||||
|
||||
api.GET("/api/helm/charts/resources", func(c *gin.Context) {
|
||||
cName := c.Query("chart")
|
||||
cNamespace := c.Query("namespace")
|
||||
if cName == "" {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
|
||||
return
|
||||
}
|
||||
cRev, err := strconv.Atoi(c.Query("revision"))
|
||||
api.DELETE("/api/cache", func(c *gin.Context) { // TODO: included into OpenAPI or not?
|
||||
err := data.Cache.Clear()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := data.RevisionManifestsParsed(cNamespace, cName, cRev)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
c.Status(http.StatusAccepted)
|
||||
})
|
||||
|
||||
configureKubectls(api, data)
|
||||
api.POST("/diff", func(c *gin.Context) { // TODO: included into OpenAPI or not?
|
||||
a := c.PostForm("a")
|
||||
b := c.PostForm("b")
|
||||
|
||||
sections := map[string]SectionFn{
|
||||
"manifests": data.RevisionManifests,
|
||||
"values": data.RevisionValues,
|
||||
"notes": data.RevisionNotes,
|
||||
}
|
||||
|
||||
api.GET("/api/helm/charts/:section", func(c *gin.Context) {
|
||||
functor, found := sections[c.Param("section")]
|
||||
if !found {
|
||||
_ = c.AbortWithError(http.StatusNotFound, errors.New("unsupported section: "+c.Param("section")))
|
||||
return
|
||||
}
|
||||
|
||||
cName := c.Query("chart")
|
||||
cNamespace := c.Query("namespace")
|
||||
if cName == "" {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: chart"))
|
||||
return
|
||||
}
|
||||
|
||||
cRev, err := strconv.Atoi(c.Query("revision"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
flag := c.Query("flag") == "true"
|
||||
rDiff := c.Query("revisionDiff")
|
||||
if rDiff != "" {
|
||||
cRevDiff, err := strconv.Atoi(rDiff)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ext := ".yaml"
|
||||
if c.Param("section") == "notes" {
|
||||
ext = ".txt"
|
||||
}
|
||||
|
||||
res, err := RevisionDiff(functor, ext, cNamespace, cName, cRevDiff, cRev, flag)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, res)
|
||||
} else {
|
||||
res, err := functor(cNamespace, cName, cRev, flag)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, res)
|
||||
}
|
||||
out := handlers.GetDiff(a, b, "current.yaml", "upgraded.yaml")
|
||||
c.Header("Content-Type", "text/plain")
|
||||
c.String(http.StatusOK, out)
|
||||
})
|
||||
|
||||
api.GET("/api-docs", func(c *gin.Context) { // https://github.com/OAI/OpenAPI-Specification/search?q=api-docs
|
||||
c.Redirect(http.StatusFound, "static/api-docs.html")
|
||||
})
|
||||
|
||||
configureHelms(api.Group("/api/helm"), data)
|
||||
configureKubectls(api.Group("/api/k8s"), data)
|
||||
configureScanners(api.Group("/api/scanners"), data)
|
||||
}
|
||||
|
||||
func configureKubectls(api *gin.Engine, data *DataLayer) {
|
||||
api.GET("/api/kube/contexts", func(c *gin.Context) {
|
||||
res, err := data.ListContexts()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
})
|
||||
func configureHelms(api *gin.RouterGroup, data *objects.DataLayer) {
|
||||
h := handlers.HelmHandler{
|
||||
Contexted: &handlers.Contexted{
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
|
||||
api.GET("/api/kube/resources/:kind", func(c *gin.Context) {
|
||||
cName := c.Query("name")
|
||||
cNamespace := c.Query("namespace")
|
||||
if cName == "" {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, errors.New("missing required query string parameter: name"))
|
||||
return
|
||||
}
|
||||
rels := api.Group("/releases")
|
||||
rels.GET("", h.GetReleases)
|
||||
rels.POST(":ns", h.Install)
|
||||
rels.POST(":ns/:name", h.Upgrade)
|
||||
rels.DELETE(":ns/:name", h.Uninstall)
|
||||
rels.GET(":ns/:name/history", h.History)
|
||||
rels.GET(":ns/:name/:section", h.GetInfoSection)
|
||||
rels.GET(":ns/:name/resources", h.Resources)
|
||||
rels.POST(":ns/:name/rollback", h.Rollback)
|
||||
rels.POST(":ns/:name/test", h.RunTests)
|
||||
|
||||
res, err := data.GetResource(cNamespace, &GenericResource{
|
||||
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
|
||||
ObjectMeta: v1.ObjectMeta{Name: cName},
|
||||
})
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
repos := api.Group("/repositories")
|
||||
repos.GET("", h.RepoList)
|
||||
repos.POST("", h.RepoAdd)
|
||||
repos.GET("/:name", h.RepoCharts)
|
||||
repos.POST("/:name", h.RepoUpdate)
|
||||
repos.DELETE("/:name", h.RepoDelete)
|
||||
repos.GET("/latestver", h.RepoLatestVer) // TODO: use /versions in client insted and remove this?
|
||||
repos.GET("/versions", h.RepoVersions)
|
||||
repos.GET("/values", h.RepoValues)
|
||||
}
|
||||
|
||||
if res.Status.Phase == "Active" || res.Status.Phase == "Error" {
|
||||
_ = res.Name + ""
|
||||
} else if res.Status.Phase == "" && len(res.Status.Conditions) > 0 {
|
||||
res.Status.Phase = v12.CarpPhase(res.Status.Conditions[len(res.Status.Conditions)-1].Type)
|
||||
res.Status.Message = res.Status.Conditions[len(res.Status.Conditions)-1].Message
|
||||
res.Status.Reason = res.Status.Conditions[len(res.Status.Conditions)-1].Reason
|
||||
if res.Status.Conditions[len(res.Status.Conditions)-1].Status == "False" {
|
||||
res.Status.Phase = "Not" + res.Status.Phase
|
||||
}
|
||||
} else if res.Status.Phase == "" {
|
||||
res.Status.Phase = "Exists"
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
})
|
||||
func configureKubectls(api *gin.RouterGroup, data *objects.DataLayer) {
|
||||
h := handlers.KubeHandler{
|
||||
Contexted: &handlers.Contexted{
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
api.GET("/contexts", h.GetContexts)
|
||||
api.GET("/:kind/get", h.GetResourceInfo)
|
||||
api.GET("/:kind/describe", h.Describe)
|
||||
api.GET("/:kind/list", h.GetNameSpaces)
|
||||
}
|
||||
|
||||
func configureStatic(api *gin.Engine) {
|
||||
@@ -237,12 +193,13 @@ func configureStatic(api *gin.Engine) {
|
||||
}
|
||||
}
|
||||
|
||||
func contextSetter(data *DataLayer) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if context, ok := c.Request.Header["X-Kubecontext"]; ok {
|
||||
log.Debugf("Setting current context to: %s", context)
|
||||
data.KubeContext = context[0]
|
||||
}
|
||||
c.Next()
|
||||
func configureScanners(api *gin.RouterGroup, data *objects.DataLayer) {
|
||||
h := handlers.ScannersHandler{
|
||||
Contexted: &handlers.Contexted{
|
||||
Data: data,
|
||||
},
|
||||
}
|
||||
api.GET("", h.List)
|
||||
api.POST("/manifests", h.ScanManifest)
|
||||
api.GET("/resource/:kind", h.ScanResource)
|
||||
}
|
||||
|
||||
413
pkg/dashboard/api_test.go
Normal file
@@ -0,0 +1,413 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/handlers"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gotest.tools/v3/assert"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
kubefake "helm.sh/helm/v3/pkg/kube/fake"
|
||||
"helm.sh/helm/v3/pkg/registry"
|
||||
"helm.sh/helm/v3/pkg/storage"
|
||||
"helm.sh/helm/v3/pkg/storage/driver"
|
||||
)
|
||||
|
||||
var inMemStorage *storage.Storage
|
||||
var repoFile string
|
||||
|
||||
func TestMain(m *testing.M) { // fixture to set logging level via env variable
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.Debugf("Set logging level")
|
||||
}
|
||||
|
||||
inMemStorage = storage.Init(driver.NewMemory())
|
||||
d, err := ioutil.TempDir("", "helm")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
repoFile = filepath.Join(d, "repositories.yaml")
|
||||
|
||||
m.Run()
|
||||
inMemStorage = nil
|
||||
repoFile = ""
|
||||
}
|
||||
|
||||
func GetTestGinContext(w *httptest.ResponseRecorder) *gin.Context {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Request = &http.Request{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestNoCacheMiddleware(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
con := GetTestGinContext(w)
|
||||
noCache(con)
|
||||
assert.Equal(t, w.Header().Get("Cache-Control"), "no-cache")
|
||||
}
|
||||
|
||||
func TestEnableCacheControl(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
con := GetTestGinContext(w)
|
||||
|
||||
// Sets deafault policy to `no-cache`
|
||||
noCache(con)
|
||||
|
||||
h := handlers.HelmHandler{
|
||||
Contexted: &handlers.Contexted{
|
||||
Data: &objects.DataLayer{},
|
||||
},
|
||||
}
|
||||
h.EnableClientCache(con)
|
||||
assert.Equal(t, w.Header().Get("Cache-Control"), "max-age=43200")
|
||||
}
|
||||
|
||||
func TestConfigureStatic(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create an API Engine
|
||||
api := gin.Default()
|
||||
|
||||
// Configure static routes
|
||||
configureStatic(api)
|
||||
|
||||
// Start the server
|
||||
api.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestConfigureRoutes(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
req, err := http.NewRequest("GET", "/status", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a API Engine
|
||||
api := gin.Default()
|
||||
|
||||
// Required arguements for route configuration
|
||||
abortWeb := func() {}
|
||||
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Configure routes to API engine
|
||||
configureRoutes(abortWeb, data, api)
|
||||
|
||||
// Start the server
|
||||
api.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestContextSetter(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
con := GetTestGinContext(w)
|
||||
|
||||
// Required arguements
|
||||
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set the context
|
||||
ctxHandler := contextSetter(data)
|
||||
ctxHandler(con)
|
||||
|
||||
appName, exists := con.Get("app")
|
||||
|
||||
if !exists {
|
||||
t.Fatal("Value app doesn't exist in context")
|
||||
}
|
||||
|
||||
tmp := handlers.Contexted{Data: data}
|
||||
|
||||
assert.Equal(t, appName, tmp.GetApp(con))
|
||||
}
|
||||
|
||||
func TestNewRouter(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/status", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Required arguemnets
|
||||
abortWeb := func() {}
|
||||
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a new router with the function
|
||||
newRouter := NewRouter(abortWeb, data, false)
|
||||
|
||||
newRouter.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestConfigureScanners(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/api/scanners", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Required arguemnets
|
||||
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
apiEngine := gin.Default()
|
||||
|
||||
configureScanners(apiEngine.Group("/api/scanners"), data)
|
||||
|
||||
apiEngine.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestConfigureKubectls(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/api/kube/contexts", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Required arguemnets
|
||||
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
apiEngine := gin.Default()
|
||||
|
||||
// Required middleware for kubectl api configuration
|
||||
apiEngine.Use(contextSetter(data))
|
||||
|
||||
configureKubectls(apiEngine.Group("/api/kube"), data)
|
||||
|
||||
apiEngine.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestE2E(t *testing.T) {
|
||||
// Initialize data layer
|
||||
data, err := objects.NewDataLayer([]string{""}, "0.0.0-test", getFakeHelmConfig, false)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Create a new router with the function
|
||||
abortWeb := func() {}
|
||||
newRouter := NewRouter(abortWeb, data, false)
|
||||
|
||||
// initially, we don't have any releases
|
||||
w := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/api/helm/releases", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "[]")
|
||||
|
||||
// initially, we don't have any repositories
|
||||
w = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/api/helm/repositories", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "[]")
|
||||
|
||||
// then we add one repository
|
||||
w = httptest.NewRecorder()
|
||||
form := url.Values{}
|
||||
form.Add("name", "komodorio")
|
||||
form.Add("url", "https://helm-charts.komodor.io")
|
||||
req, err = http.NewRequest("POST", "/api/helm/repositories", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusNoContent)
|
||||
assert.Equal(t, w.Body.String(), "")
|
||||
|
||||
// now, we have one repo
|
||||
w = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/api/helm/repositories", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), `[
|
||||
{
|
||||
"name": "komodorio",
|
||||
"url": "https://helm-charts.komodor.io"
|
||||
}
|
||||
]`)
|
||||
|
||||
// what's the latest version of that chart
|
||||
w = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/api/helm/repositories/latestver?name=helm-dashboard", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
|
||||
// generate template for potential release
|
||||
w = httptest.NewRecorder()
|
||||
form = url.Values{}
|
||||
form.Add("preview", "true")
|
||||
form.Add("name", "release1")
|
||||
form.Add("chart", "komodorio/helm-dashboard")
|
||||
req, err = http.NewRequest("POST", "/api/helm/releases/test1", strings.NewReader(form.Encode()))
|
||||
assert.NilError(t, err)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
|
||||
// install the release
|
||||
w = httptest.NewRecorder()
|
||||
form = url.Values{}
|
||||
form.Add("name", "release1")
|
||||
form.Add("chart", "komodorio/helm-dashboard")
|
||||
req, err = http.NewRequest("POST", "/api/helm/releases/test1", strings.NewReader(form.Encode()))
|
||||
assert.NilError(t, err)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusAccepted)
|
||||
|
||||
// get list of releases
|
||||
w = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/api/helm/releases", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
t.Logf("Release: %s", w.Body.String())
|
||||
//assert.Equal(t, w.Body.String(), "[]")
|
||||
|
||||
// upgrade/reconfigure release
|
||||
w = httptest.NewRecorder()
|
||||
form = url.Values{}
|
||||
form.Add("chart", "komodorio/helm-dashboard")
|
||||
form.Add("values", "dashboard:\n allowWriteActions: true\n")
|
||||
req, err = http.NewRequest("POST", "/api/helm/releases/test1/release1", strings.NewReader(form.Encode()))
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
assert.Equal(t, w.Code, http.StatusAccepted)
|
||||
|
||||
// get history of revisions for release
|
||||
w = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/api/helm/releases/test1/release1/history", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
t.Logf("Revs: %s", w.Body.String())
|
||||
//assert.Equal(t, w.Body.String(), "[]")
|
||||
|
||||
// get values for revision
|
||||
w = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/api/helm/releases/test1/release1/values?revision=2&userDefined=true", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
//assert.Equal(t, w.Body.String(), "[]")
|
||||
|
||||
// rollback
|
||||
w = httptest.NewRecorder()
|
||||
form = url.Values{}
|
||||
form.Add("revision", "1")
|
||||
req, err = http.NewRequest("POST", "/api/helm/releases/test1/release1/rollback", strings.NewReader(form.Encode()))
|
||||
assert.NilError(t, err)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusAccepted)
|
||||
|
||||
// get manifest diff for release
|
||||
w = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/api/helm/releases/test1/release1/manifests?revision=1&revisionDiff=2", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
//assert.Equal(t, w.Body.String(), "[]")
|
||||
|
||||
// delete repo
|
||||
w = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("DELETE", "/api/helm/repositories/komodorio", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusNoContent)
|
||||
|
||||
// reconfigure release without repo connection
|
||||
w = httptest.NewRecorder()
|
||||
form = url.Values{}
|
||||
form.Add("chart", "komodorio/helm-dashboard")
|
||||
form.Add("values", "dashboard:\n allowWriteActions: false\n")
|
||||
req, err = http.NewRequest("POST", "/api/helm/releases/test1/release1", strings.NewReader(form.Encode()))
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
assert.Equal(t, w.Code, http.StatusAccepted)
|
||||
t.Logf("Upgraded: %s", w.Body.String())
|
||||
|
||||
// uninstall
|
||||
w = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("DELETE", "/api/helm/releases/test1/release1", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusAccepted)
|
||||
|
||||
// check we don't have releases again
|
||||
w = httptest.NewRecorder()
|
||||
req, err = http.NewRequest("GET", "/api/helm/releases", nil)
|
||||
assert.NilError(t, err)
|
||||
newRouter.ServeHTTP(w, req)
|
||||
assert.Equal(t, w.Code, http.StatusOK)
|
||||
assert.Equal(t, w.Body.String(), "[]")
|
||||
}
|
||||
|
||||
func getFakeHelmConfig(settings *cli.EnvSettings, _ string) (*action.Configuration, error) {
|
||||
settings.RepositoryConfig = repoFile
|
||||
|
||||
registryClient, err := registry.NewClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &action.Configuration{
|
||||
Releases: inMemStorage,
|
||||
KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: os.Stderr}},
|
||||
Capabilities: chartutil.DefaultCapabilities,
|
||||
RegistryClient: registryClient,
|
||||
Log: log.Infof,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/hexops/gotextdiff"
|
||||
"github.com/hexops/gotextdiff/myers"
|
||||
"github.com/hexops/gotextdiff/span"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DataLayer struct {
|
||||
KubeContext string
|
||||
Helm string
|
||||
Kubectl string
|
||||
}
|
||||
|
||||
func (d *DataLayer) runCommand(cmd ...string) (string, error) {
|
||||
log.Debugf("Starting command: %s", cmd)
|
||||
prog := exec.Command(cmd[0], cmd[1:]...)
|
||||
prog.Env = os.Environ()
|
||||
prog.Env = append(prog.Env, "HELM_KUBECONTEXT="+d.KubeContext)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
prog.Stdout = &stdout
|
||||
|
||||
var stderr bytes.Buffer
|
||||
prog.Stderr = &stderr
|
||||
|
||||
if err := prog.Run(); err != nil {
|
||||
log.Warnf("Failed command: %s", cmd)
|
||||
serr := stderr.Bytes()
|
||||
if serr != nil {
|
||||
log.Warnf("STDERR:\n%s", serr)
|
||||
}
|
||||
if eerr, ok := err.(*exec.ExitError); ok {
|
||||
return "", fmt.Errorf("failed to run command %s:\nError: %s\nSTDERR:%s", cmd, eerr, serr)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
sout := stdout.Bytes()
|
||||
serr := stderr.Bytes()
|
||||
log.Debugf("Command STDOUT:\n%s", sout)
|
||||
log.Debugf("Command STDERR:\n%s", serr)
|
||||
return string(sout), nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) runCommandHelm(cmd ...string) (string, error) {
|
||||
if d.Helm == "" {
|
||||
d.Helm = "helm"
|
||||
}
|
||||
|
||||
cmd = append([]string{d.Helm}, cmd...)
|
||||
if d.KubeContext != "" {
|
||||
cmd = append(cmd, "--kube-context", d.KubeContext)
|
||||
}
|
||||
|
||||
return d.runCommand(cmd...)
|
||||
}
|
||||
|
||||
func (d *DataLayer) runCommandKubectl(cmd ...string) (string, error) {
|
||||
// TODO: migrate into using kubectl "k8s.io/kubectl/pkg/cmd" and kube API
|
||||
if d.Kubectl == "" {
|
||||
d.Kubectl = "kubectl"
|
||||
}
|
||||
|
||||
cmd = append([]string{d.Kubectl}, cmd...)
|
||||
|
||||
if d.KubeContext != "" {
|
||||
cmd = append(cmd, "--context", d.KubeContext)
|
||||
}
|
||||
|
||||
return d.runCommand(cmd...)
|
||||
}
|
||||
|
||||
func (d *DataLayer) CheckConnectivity() error {
|
||||
contexts, err := d.ListContexts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(contexts) < 1 {
|
||||
return errors.New("did not find any kubectl contexts configured")
|
||||
}
|
||||
|
||||
/*
|
||||
_, err = d.runCommandHelm("env") // no point in doing is, since the default context may be invalid
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*/
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type KubeContext struct {
|
||||
IsCurrent bool
|
||||
Name string
|
||||
Cluster string
|
||||
AuthInfo string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
func (d *DataLayer) ListContexts() (res []KubeContext, err error) {
|
||||
out, err := d.runCommandKubectl("config", "get-contexts")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// kubectl has no JSON output for it, we'll have to do custom text parsing
|
||||
lines := strings.Split(out, "\n")
|
||||
|
||||
// find field positions
|
||||
fields := regexp.MustCompile(`(\w+\s+)`).FindAllString(lines[0], -1)
|
||||
cur := len(fields[0])
|
||||
name := cur + len(fields[1])
|
||||
cluster := name + len(fields[2])
|
||||
auth := cluster + len(fields[3])
|
||||
|
||||
// read items
|
||||
for _, line := range lines[1:] {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
res = append(res, KubeContext{
|
||||
IsCurrent: strings.TrimSpace(line[0:cur]) == "*",
|
||||
Name: strings.TrimSpace(line[cur:name]),
|
||||
Cluster: strings.TrimSpace(line[name:cluster]),
|
||||
AuthInfo: strings.TrimSpace(line[cluster:auth]),
|
||||
Namespace: strings.TrimSpace(line[auth:]),
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ListInstalled() (res []releaseElement, err error) {
|
||||
out, err := d.runCommandHelm("ls", "--all", "--all-namespaces", "--output", "json", "--time-format", time.RFC3339)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*historyElement, err error) {
|
||||
// TODO: there is `max` but there is no `offset`
|
||||
out, err := d.runCommandHelm("history", chartName, "--namespace", namespace, "--output", "json", "--max", "18")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var aprev *semver.Version
|
||||
var cprev *semver.Version
|
||||
for _, elm := range res {
|
||||
chartRepoName, curVer, err := chartAndVersion(elm.Chart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
elm.ChartName = chartRepoName
|
||||
elm.ChartVer = curVer
|
||||
elm.Action = ""
|
||||
elm.Updated.Time = elm.Updated.Time.Round(time.Second)
|
||||
|
||||
cver, err1 := semver.NewVersion(elm.ChartVer)
|
||||
aver, err2 := semver.NewVersion(elm.AppVersion)
|
||||
if err1 == nil && err2 == nil {
|
||||
if aprev != nil && cprev != nil {
|
||||
switch {
|
||||
case aprev.LessThan(aver):
|
||||
elm.Action = "app_upgrade"
|
||||
case aprev.GreaterThan(aver):
|
||||
elm.Action = "app_downgrade"
|
||||
case cprev.LessThan(cver):
|
||||
elm.Action = "chart_upgrade"
|
||||
case cprev.GreaterThan(cver):
|
||||
elm.Action = "chart_downgrade"
|
||||
default:
|
||||
elm.Action = "reconfigure"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Semver parsing errors: %s=%s, %s=%s", elm.ChartVer, err1, elm.AppVersion, err2)
|
||||
}
|
||||
|
||||
aprev = aver
|
||||
cprev = cver
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ChartRepoVersions(chartName string) (res []repoChartElement, err error) {
|
||||
out, err := d.runCommandHelm("search", "repo", "--regexp", "/"+chartName+"\v", "--versions", "--output", "json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
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) {
|
||||
out, err := d.runCommandHelm("get", "manifest", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string, revision int) ([]*GenericResource, error) {
|
||||
out, err := d.RevisionManifests(namespace, chartName, revision, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dec := yaml.NewDecoder(bytes.NewReader([]byte(out)))
|
||||
|
||||
res := make([]*GenericResource, 0)
|
||||
var tmp interface{}
|
||||
for dec.Decode(&tmp) == nil {
|
||||
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
|
||||
// bug we can juggle it
|
||||
jsoned, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var doc GenericResource
|
||||
err = json.Unmarshal(jsoned, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = append(res, &doc)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) RevisionNotes(namespace string, chartName string, revision int, _ bool) (res string, err error) {
|
||||
out, err := d.runCommandHelm("get", "notes", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) RevisionValues(namespace string, chartName string, revision int, onlyUserDefined bool) (res string, err error) {
|
||||
cmd := []string{"get", "values", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision), "--output", "yaml"}
|
||||
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 *GenericResource) (*GenericResource, 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 &GenericResource{
|
||||
Status: v1.CarpStatus{
|
||||
Phase: "NotFound",
|
||||
Message: err.Error(),
|
||||
Reason: "not found",
|
||||
},
|
||||
}, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var res GenericResource
|
||||
err = json.Unmarshal([]byte(out), &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(res.Status.Conditions, func(i, j int) bool {
|
||||
t1 := res.Status.Conditions[i].LastTransitionTime
|
||||
t2 := res.Status.Conditions[j].LastTransitionTime
|
||||
return t1.Time.Before(t2.Time)
|
||||
})
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) {
|
||||
if revision1 == 0 || revision2 == 0 {
|
||||
log.Debugf("One of revisions is zero: %d %d", revision1, revision2)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
manifest1, err := functor(namespace, name, revision1, flag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
manifest2, err := functor(namespace, name, revision2, flag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
edits := myers.ComputeEdits(span.URIFromPath(""), manifest1, manifest2)
|
||||
unified := gotextdiff.ToUnified(strconv.Itoa(revision1)+ext, strconv.Itoa(revision2)+ext, manifest1, edits)
|
||||
diff := fmt.Sprint(unified)
|
||||
log.Debugf("The diff is: %s", diff)
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
type GenericResource = v1.Carp
|
||||
@@ -1,86 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFlow(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
var _ release.Status
|
||||
data := DataLayer{}
|
||||
err := data.CheckConnectivity()
|
||||
if err != nil {
|
||||
if err.Error() == "did not find any kubectl contexts configured" {
|
||||
t.Skip()
|
||||
} else {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
ctxses, err := data.ListContexts()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, ctx := range ctxses {
|
||||
if ctx.IsCurrent {
|
||||
data.KubeContext = ctx.Name
|
||||
}
|
||||
}
|
||||
|
||||
installed, err := data.ListInstalled()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
chart := installed[1]
|
||||
history, err := data.ChartHistory(chart.Namespace, chart.Name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = history
|
||||
|
||||
chartRepoName, curVer, err := chartAndVersion(chart.Chart)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = curVer
|
||||
|
||||
upgrade, err := data.ChartRepoVersions(chartRepoName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = upgrade
|
||||
|
||||
manifests, err := data.RevisionManifestsParsed(chart.Namespace, chart.Name, history[len(history)-1].Revision)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = manifests
|
||||
|
||||
var wg sync.WaitGroup
|
||||
res := make([]*GenericResource, 0)
|
||||
for _, m := range manifests {
|
||||
wg.Add(1)
|
||||
mc := m // fix the clojure
|
||||
func() {
|
||||
defer wg.Done()
|
||||
lst, err := data.GetResource(chart.Namespace, mc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res = append(res, lst)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
diff, err := RevisionDiff(data.RevisionManifests, ".yaml", chart.Namespace, chart.Name, history[len(history)-1].Revision, history[len(history)-2].Revision, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = diff
|
||||
}
|
||||
31
pkg/dashboard/handlers/common.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const APP = "app"
|
||||
|
||||
type Contexted struct {
|
||||
Data *objects.DataLayer
|
||||
}
|
||||
|
||||
func (h *Contexted) GetApp(c *gin.Context) *objects.Application {
|
||||
var app *objects.Application
|
||||
if a, ok := c.Get(APP); ok {
|
||||
app = a.(*objects.Application)
|
||||
} else {
|
||||
err := errorx.IllegalState.New("No application context found")
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func (h *Contexted) EnableClientCache(c *gin.Context) {
|
||||
c.Header("Cache-Control", "max-age=43200")
|
||||
}
|
||||
737
pkg/dashboard/handlers/helmHandlers.go
Normal file
@@ -0,0 +1,737 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hexops/gotextdiff"
|
||||
"github.com/hexops/gotextdiff/myers"
|
||||
"github.com/hexops/gotextdiff/span"
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
"github.com/rogpeppe/go-internal/semver"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
helmtime "helm.sh/helm/v3/pkg/time"
|
||||
"k8s.io/utils/strings/slices"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HelmHandler struct {
|
||||
*Contexted
|
||||
}
|
||||
|
||||
func (h *HelmHandler) getRelease(c *gin.Context) *objects.Release {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rel, err := app.Releases.ByName(c.Param("ns"), c.Param("name"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return nil
|
||||
}
|
||||
return rel
|
||||
}
|
||||
|
||||
func (h *HelmHandler) GetReleases(c *gin.Context) {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
rels, err := app.Releases.List()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
res := []*ReleaseElement{}
|
||||
for _, r := range rels {
|
||||
res = append(res, HReleaseToJSON(r.Orig))
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) Uninstall(c *gin.Context) {
|
||||
rel := h.getRelease(c)
|
||||
if rel == nil {
|
||||
return // error state is set inside
|
||||
}
|
||||
|
||||
err := rel.Uninstall()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) Rollback(c *gin.Context) {
|
||||
rel := h.getRelease(c)
|
||||
if rel == nil {
|
||||
return // error state is set inside
|
||||
}
|
||||
|
||||
revn, err := strconv.Atoi(c.PostForm("revision"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = rel.Rollback(revn)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) History(c *gin.Context) {
|
||||
rel := h.getRelease(c)
|
||||
if rel == nil {
|
||||
return // error state is set inside
|
||||
}
|
||||
|
||||
revs, err := rel.History()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
res := []*HistoryElement{}
|
||||
for _, r := range revs {
|
||||
res = append(res, HReleaseToHistElem(r.Orig))
|
||||
}
|
||||
|
||||
sort.Slice(res, func(i, j int) bool {
|
||||
return res[i].Revision < res[j].Revision
|
||||
})
|
||||
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) Resources(c *gin.Context) {
|
||||
// can't enable the client cache because resource list changes with time
|
||||
|
||||
rel := h.getRelease(c)
|
||||
if rel == nil {
|
||||
return // error state is set inside
|
||||
}
|
||||
|
||||
res, err := objects.ParseManifests(rel.Orig.Manifest)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) RepoVersions(c *gin.Context) {
|
||||
qp, err := utils.GetQueryProps(c)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
repos, err := app.Repositories.Containing(qp.Name)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
res := []*RepoChartElement{}
|
||||
for _, r := range repos {
|
||||
res = append(res, &RepoChartElement{
|
||||
Name: r.Name,
|
||||
Version: r.Version,
|
||||
AppVersion: r.AppVersion,
|
||||
Description: r.Description,
|
||||
Repository: r.Annotations[objects.AnnRepo],
|
||||
URLs: r.URLs,
|
||||
})
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) RepoLatestVer(c *gin.Context) {
|
||||
qp, err := utils.GetQueryProps(c)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
rep, err := app.Repositories.Containing(qp.Name)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
res := []*RepoChartElement{}
|
||||
for _, r := range rep {
|
||||
res = append(res, &RepoChartElement{
|
||||
Name: r.Name,
|
||||
Version: r.Version,
|
||||
AppVersion: r.AppVersion,
|
||||
Description: r.Description,
|
||||
Repository: r.Annotations[objects.AnnRepo],
|
||||
URLs: r.URLs,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(res, func(i, j int) bool {
|
||||
return semver.Compare(res[i].Version, res[j].Version) > 0
|
||||
})
|
||||
|
||||
if len(res) > 0 {
|
||||
c.IndentedJSON(http.StatusOK, res[:1])
|
||||
} else {
|
||||
// caching it to avoid too many requests
|
||||
found, err := h.Data.Cache.String("chart-artifacthub-query/"+qp.Name, nil, func() (string, error) {
|
||||
return h.repoFromArtifactHub(qp.Name)
|
||||
})
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if found == "" {
|
||||
c.Status(http.StatusNoContent)
|
||||
} else {
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.String(http.StatusOK, found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HelmHandler) RepoCharts(c *gin.Context) {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
rep, err := app.Repositories.Get(c.Param("name"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
charts, err := rep.Charts()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
installed, err := app.Releases.List()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
enrichRepoChartsWithInstalled(charts, installed)
|
||||
|
||||
sort.Slice(charts, func(i, j int) bool {
|
||||
return charts[i].Name < charts[j].Name
|
||||
})
|
||||
|
||||
c.IndentedJSON(http.StatusOK, charts)
|
||||
}
|
||||
|
||||
func enrichRepoChartsWithInstalled(charts []*repo.ChartVersion, installed []*objects.Release) {
|
||||
for _, rchart := range charts {
|
||||
for _, rel := range installed {
|
||||
if rchart.Metadata.Name == rel.Orig.Chart.Name() {
|
||||
log.Debugf("Matched") // TODO: restore implementation
|
||||
// TODO: there can be more than one
|
||||
//rchart.InstalledNamespace = rel.Orig.Namespace
|
||||
//rchart.InstalledName = rel.Orig.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HelmHandler) RepoUpdate(c *gin.Context) {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
rep, err := app.Repositories.Get(c.Param("name"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = rep.Update()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) Install(c *gin.Context) {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
values := map[string]interface{}{}
|
||||
err := yaml.Unmarshal([]byte(c.PostForm("values")), &values)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
repoChart, err := h.checkLocalRepo(c.PostForm("chart"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
justTemplate := c.PostForm("preview") == "true"
|
||||
ns := c.Param("ns")
|
||||
if ns == "[empty]" {
|
||||
ns = ""
|
||||
}
|
||||
|
||||
rel, err := app.Releases.Install(ns, c.PostForm("name"), repoChart, c.PostForm("version"), justTemplate, values)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if justTemplate {
|
||||
c.IndentedJSON(http.StatusOK, rel)
|
||||
} else {
|
||||
c.IndentedJSON(http.StatusAccepted, rel)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HelmHandler) checkLocalRepo(repoChart string) (string, error) {
|
||||
if strings.HasPrefix(repoChart, "file://") {
|
||||
repoChart = repoChart[len("file://"):]
|
||||
if !slices.Contains(h.Data.LocalCharts, repoChart) {
|
||||
return "", fmt.Errorf("chart path is not present in local charts: %s", repoChart)
|
||||
}
|
||||
}
|
||||
return repoChart, nil
|
||||
}
|
||||
|
||||
func (h *HelmHandler) Upgrade(c *gin.Context) {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
existing, err := app.Releases.ByName(c.Param("ns"), c.Param("name"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
values := map[string]interface{}{}
|
||||
err = yaml.Unmarshal([]byte(c.PostForm("values")), &values)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
repoChart, err := h.checkLocalRepo(c.PostForm("chart"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
justTemplate := c.PostForm("preview") == "true"
|
||||
rel, err := existing.Upgrade(repoChart, c.PostForm("version"), justTemplate, values)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if justTemplate {
|
||||
c.IndentedJSON(http.StatusOK, rel)
|
||||
} else {
|
||||
c.IndentedJSON(http.StatusAccepted, rel)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HelmHandler) RunTests(c *gin.Context) {
|
||||
rel := h.getRelease(c)
|
||||
if rel == nil {
|
||||
return // error state is set inside
|
||||
}
|
||||
|
||||
out, err := rel.RunTests()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) GetInfoSection(c *gin.Context) {
|
||||
if c.Query("revision") != "" { // don't cache if latest is requested
|
||||
h.EnableClientCache(c)
|
||||
}
|
||||
|
||||
rel := h.getRelease(c)
|
||||
if rel == nil {
|
||||
return // error state is set inside
|
||||
}
|
||||
|
||||
revn, err := strconv.Atoi(c.Query("revision"))
|
||||
if c.Query("revision") != "" && err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
rev, err := rel.GetRev(revn)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
var revDiff *objects.Release
|
||||
revS := c.Query("revisionDiff")
|
||||
if revS != "" {
|
||||
revN, err := strconv.Atoi(revS)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
revDiff, err = rel.GetRev(revN)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
flag := c.Query("userDefined") == "true"
|
||||
|
||||
res, err := h.handleGetSection(rev, c.Param("section"), revDiff, flag)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) RepoValues(c *gin.Context) {
|
||||
h.EnableClientCache(c)
|
||||
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
repoChart, err := h.checkLocalRepo(c.Query("chart"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := app.Repositories.GetChartValues(repoChart, c.Query("version"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) RepoList(c *gin.Context) {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
repos, err := app.Repositories.List()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
out := []RepositoryElement{}
|
||||
for _, r := range repos {
|
||||
out = append(out, RepositoryElement{
|
||||
Name: r.Name(),
|
||||
URL: r.URL(),
|
||||
})
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) RepoAdd(c *gin.Context) {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
// TODO: more repo options to accept
|
||||
err := app.Repositories.Add(c.PostForm("name"), c.PostForm("url"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) RepoDelete(c *gin.Context) {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
err := app.Repositories.Delete(c.Param("name"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *HelmHandler) handleGetSection(rel *objects.Release, section string, rDiff *objects.Release, flag bool) (string, error) {
|
||||
sections := map[string]objects.SectionFn{
|
||||
"manifests": func(qp *release.Release, b bool) (string, error) { return qp.Manifest, nil },
|
||||
"notes": func(qp *release.Release, b bool) (string, error) { return qp.Info.Notes, nil },
|
||||
"values": func(qp *release.Release, b bool) (string, error) {
|
||||
allVals := qp.Config
|
||||
|
||||
if !b {
|
||||
merged, err := chartutil.CoalesceValues(qp.Chart, qp.Config)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to merge chart vals with user defined")
|
||||
}
|
||||
allVals = merged
|
||||
}
|
||||
|
||||
if len(allVals) > 0 {
|
||||
data, err := yaml.Marshal(allVals)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to serialize values into YAML")
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
}
|
||||
|
||||
functor, found := sections[section]
|
||||
if !found {
|
||||
return "", errors.New("unsupported section: " + section)
|
||||
}
|
||||
|
||||
if rDiff != nil {
|
||||
ext := ".yaml"
|
||||
if section == "notes" {
|
||||
ext = ".txt"
|
||||
}
|
||||
|
||||
res, err := RevisionDiff(functor, ext, rDiff.Orig, rel.Orig, flag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
res, err := functor(rel.Orig, flag)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to get section info")
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (h *HelmHandler) repoFromArtifactHub(name string) (string, error) {
|
||||
results, err := objects.QueryArtifactHub(name)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to query ArtifactHub: %s", err)
|
||||
return "", nil // swallowing the error to not annoy users
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
sort.SliceStable(results, func(i, j int) bool {
|
||||
ri, rj := results[i], results[j]
|
||||
|
||||
// we prefer official repos
|
||||
if ri.Repository.Official && !rj.Repository.Official {
|
||||
return true
|
||||
}
|
||||
|
||||
// or from verified publishers
|
||||
if ri.Repository.VerifiedPublisher && !rj.Repository.VerifiedPublisher {
|
||||
return true
|
||||
}
|
||||
|
||||
// or just more popular
|
||||
if ri.Stars > rj.Stars {
|
||||
return true
|
||||
}
|
||||
|
||||
// or with more recent app version
|
||||
|
||||
if semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
r := results[0]
|
||||
buf, err := json.Marshal([]*RepoChartElement{{
|
||||
Name: r.Name,
|
||||
Version: r.Version,
|
||||
AppVersion: r.AppVersion,
|
||||
Description: r.Description,
|
||||
Repository: r.Repository.Name,
|
||||
URLs: []string{r.Repository.Url},
|
||||
IsSuggestedRepo: true,
|
||||
}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
type RepoChartElement struct { // TODO: do we need it at all? there is existing repo.ChartVersion in Helm
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
AppVersion string `json:"app_version"`
|
||||
Description string `json:"description"`
|
||||
|
||||
InstalledNamespace string `json:"installed_namespace"`
|
||||
InstalledName string `json:"installed_name"`
|
||||
Repository string `json:"repository"`
|
||||
URLs []string `json:"urls"`
|
||||
IsSuggestedRepo bool `json:"isSuggestedRepo"`
|
||||
}
|
||||
|
||||
func HReleaseToJSON(o *release.Release) *ReleaseElement {
|
||||
return &ReleaseElement{
|
||||
Name: o.Name,
|
||||
Namespace: o.Namespace,
|
||||
Revision: strconv.Itoa(o.Version),
|
||||
Updated: o.Info.LastDeployed,
|
||||
Status: o.Info.Status,
|
||||
Chart: fmt.Sprintf("%s-%s", o.Chart.Name(), o.Chart.Metadata.Version),
|
||||
ChartName: o.Chart.Name(),
|
||||
ChartVersion: o.Chart.Metadata.Version,
|
||||
AppVersion: o.Chart.AppVersion(),
|
||||
Icon: o.Chart.Metadata.Icon,
|
||||
Description: o.Chart.Metadata.Description,
|
||||
}
|
||||
}
|
||||
|
||||
type ReleaseElement struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Revision string `json:"revision"`
|
||||
Updated helmtime.Time `json:"updated"`
|
||||
Status release.Status `json:"status"`
|
||||
Chart string `json:"chart"`
|
||||
ChartName string `json:"chartName"`
|
||||
ChartVersion string `json:"chartVersion"`
|
||||
AppVersion string `json:"app_version"`
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type RepositoryElement struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type HistoryElement struct {
|
||||
Revision int `json:"revision"`
|
||||
Updated helmtime.Time `json:"updated"`
|
||||
Status release.Status `json:"status"`
|
||||
Chart string `json:"chart"`
|
||||
AppVersion string `json:"app_version"`
|
||||
Description string `json:"description"`
|
||||
|
||||
ChartName string `json:"chart_name"` // custom addition on top of Helm
|
||||
ChartVer string `json:"chart_ver"` // custom addition on top of Helm
|
||||
HasTests bool `json:"has_tests"`
|
||||
}
|
||||
|
||||
func HReleaseToHistElem(o *release.Release) *HistoryElement {
|
||||
return &HistoryElement{
|
||||
Revision: o.Version,
|
||||
Updated: o.Info.LastDeployed,
|
||||
Status: o.Info.Status,
|
||||
Chart: fmt.Sprintf("%s-%s", o.Chart.Name(), o.Chart.Metadata.Version),
|
||||
AppVersion: o.Chart.AppVersion(),
|
||||
Description: o.Info.Description,
|
||||
ChartName: o.Chart.Name(),
|
||||
ChartVer: o.Chart.Metadata.Version,
|
||||
HasTests: releaseHasTests(o),
|
||||
}
|
||||
}
|
||||
|
||||
func RevisionDiff(functor objects.SectionFn, ext string, revision1 *release.Release, revision2 *release.Release, flag bool) (string, error) {
|
||||
if revision1 == nil || revision2 == nil {
|
||||
log.Debugf("One of revisions is nil: %v %v", revision1, revision2)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
manifest1, err := functor(revision1, flag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
manifest2, err := functor(revision2, flag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
diff := GetDiff(manifest1, manifest2, strconv.Itoa(revision1.Version)+ext, strconv.Itoa(revision2.Version)+ext)
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
func GetDiff(text1 string, text2 string, name1 string, name2 string) string {
|
||||
edits := myers.ComputeEdits(span.URIFromPath(""), text1, text2)
|
||||
unified := gotextdiff.ToUnified(name1, name2, text1, edits)
|
||||
diff := fmt.Sprint(unified)
|
||||
log.Debugf("The diff is: %s", diff)
|
||||
return diff
|
||||
}
|
||||
|
||||
func releaseHasTests(o *release.Release) bool {
|
||||
for _, h := range o.Hooks {
|
||||
for _, e := range h.Events {
|
||||
if e == release.HookTest {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
113
pkg/dashboard/handlers/kubeHandlers.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/joomcode/errorx"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
)
|
||||
|
||||
type KubeHandler struct {
|
||||
*Contexted
|
||||
}
|
||||
|
||||
func (h *KubeHandler) GetContexts(c *gin.Context) {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
res, err := h.Data.ListContexts()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
|
||||
qp, err := utils.GetQueryProps(c)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
res, err := app.K8s.GetResourceInfo(c.Param("kind"), qp.Namespace, qp.Name)
|
||||
if errors.IsNotFound(err) {
|
||||
res = &v12.Carp{Status: v12.CarpStatus{Phase: "NotFound", Message: err.Error()}}
|
||||
//_ = c.AbortWithError(http.StatusNotFound, err)
|
||||
//return
|
||||
} else if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
EnhanceStatus(res)
|
||||
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func EnhanceStatus(res *v12.Carp) {
|
||||
// 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 {
|
||||
res.Status.Phase = v12.CarpPhase(res.Status.Conditions[len(res.Status.Conditions)-1].Type)
|
||||
res.Status.Message = res.Status.Conditions[len(res.Status.Conditions)-1].Message
|
||||
res.Status.Reason = res.Status.Conditions[len(res.Status.Conditions)-1].Reason
|
||||
if res.Status.Conditions[len(res.Status.Conditions)-1].Status == "False" {
|
||||
res.Status.Phase = "Not" + res.Status.Phase
|
||||
}
|
||||
} else if res.Status.Phase == "" {
|
||||
res.Status.Phase = "Exists"
|
||||
}
|
||||
}
|
||||
|
||||
func (h *KubeHandler) Describe(c *gin.Context) {
|
||||
qp, err := utils.GetQueryProps(c)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
res, err := app.K8s.DescribeResource(c.Param("kind"), qp.Namespace, qp.Name)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *KubeHandler) GetNameSpaces(c *gin.Context) {
|
||||
if c.Param("kind") != "namespaces" {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, errorx.AssertionFailed.New("Only 'namespaces' kind is allowed for listing"))
|
||||
return
|
||||
}
|
||||
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
res, err := app.K8s.GetNameSpaces()
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
63
pkg/dashboard/handlers/scannerHandlers.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ScannersHandler struct {
|
||||
*Contexted
|
||||
}
|
||||
|
||||
func (h *ScannersHandler) List(c *gin.Context) {
|
||||
type ScannerInfo struct {
|
||||
SupportedResourceKinds []string
|
||||
ManifestScannable bool
|
||||
}
|
||||
res := map[string]ScannerInfo{}
|
||||
for _, scanner := range h.Data.Scanners {
|
||||
res[scanner.Name()] = ScannerInfo{
|
||||
SupportedResourceKinds: scanner.SupportedResourceKinds(),
|
||||
ManifestScannable: scanner.ManifestIsScannable(),
|
||||
}
|
||||
}
|
||||
c.IndentedJSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (h *ScannersHandler) ScanManifest(c *gin.Context) {
|
||||
reps := map[string]*subproc.ScanResults{}
|
||||
for _, scanner := range h.Data.Scanners {
|
||||
sr, err := scanner.ScanManifests(c.PostForm("manifest"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
reps[scanner.Name()] = sr
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, reps)
|
||||
}
|
||||
|
||||
func (h *ScannersHandler) ScanResource(c *gin.Context) {
|
||||
qp, err := utils.GetQueryProps(c)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
reps := map[string]*subproc.ScanResults{}
|
||||
for _, scanner := range h.Data.Scanners {
|
||||
sr, err := scanner.ScanResource(qp.Namespace, c.Param("kind"), qp.Name)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
reps[scanner.Name()] = sr
|
||||
}
|
||||
|
||||
c.IndentedJSON(http.StatusOK, reps)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
helmtime "helm.sh/helm/v3/pkg/time"
|
||||
)
|
||||
|
||||
// unpleasant copy from Helm sources, where they have it non-public
|
||||
type releaseElement struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Revision string `json:"revision"`
|
||||
Updated helmtime.Time `json:"updated"`
|
||||
Status release.Status `json:"status"`
|
||||
Chart string `json:"chart"`
|
||||
AppVersion string `json:"app_version"`
|
||||
}
|
||||
|
||||
type historyElement struct {
|
||||
Revision int `json:"revision"`
|
||||
Updated helmtime.Time `json:"updated"`
|
||||
Status release.Status `json:"status"`
|
||||
Chart string `json:"chart"`
|
||||
AppVersion string `json:"app_version"`
|
||||
Description string `json:"description"`
|
||||
ChartName string `json:"chart_name"`
|
||||
ChartVer string `json:"chart_ver"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
type repoChartElement struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
AppVersion string `json:"app_version"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
56
pkg/dashboard/objects/app.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"github.com/joomcode/errorx"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
|
||||
// Import to initialize client auth plugins.
|
||||
// From https://github.com/kubernetes/client-go/issues/242
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
type HelmConfigGetter = func(sett *cli.EnvSettings, ns string) (*action.Configuration, error)
|
||||
type HelmNSConfigGetter = func(ns string) (*action.Configuration, error)
|
||||
|
||||
type Application struct {
|
||||
Settings *cli.EnvSettings
|
||||
HelmConfig HelmNSConfigGetter
|
||||
|
||||
K8s *K8s
|
||||
|
||||
Releases *Releases
|
||||
Repositories *Repositories
|
||||
}
|
||||
|
||||
func NewApplication(settings *cli.EnvSettings, helmConfig HelmNSConfigGetter, namespaces []string, devel bool) (*Application, error) {
|
||||
hc, err := helmConfig(settings.Namespace())
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
|
||||
}
|
||||
|
||||
k8s, err := NewK8s(hc, namespaces)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get k8s client")
|
||||
}
|
||||
|
||||
semVerConstraint, err := versionConstaint(devel)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to create semantic version constraint")
|
||||
}
|
||||
|
||||
return &Application{
|
||||
HelmConfig: helmConfig,
|
||||
K8s: k8s,
|
||||
Releases: &Releases{
|
||||
Namespaces: namespaces,
|
||||
Settings: settings,
|
||||
HelmConfig: helmConfig,
|
||||
},
|
||||
Repositories: &Repositories{
|
||||
Settings: settings,
|
||||
HelmConfig: hc,
|
||||
versionConstraint: semVerConstraint,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
90
pkg/dashboard/objects/artifacthub.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
neturl "net/url"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var mxArtifactHub sync.Mutex
|
||||
|
||||
func QueryArtifactHub(chartName string) ([]*ArtifactHubResult, error) {
|
||||
mxArtifactHub.Lock() // to avoid parallel request spike
|
||||
defer mxArtifactHub.Unlock()
|
||||
|
||||
url := os.Getenv("HD_ARTIFACT_HUB_URL")
|
||||
if url == "" {
|
||||
url = "https://artifacthub.io/api/v1/packages/search"
|
||||
}
|
||||
|
||||
p, err := neturl.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.RawQuery = "offset=0&limit=5&facets=false&kind=0&deprecated=false&sort=relevance&ts_query_web=" + neturl.QueryEscape(chartName)
|
||||
|
||||
req, err := http.NewRequest("GET", p.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Komodor Helm Dashboard/"+os.Getenv("HD_VERSION")) // TODO
|
||||
|
||||
log.Debugf("Making HTTP request: %v", req)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("failed to fetch %s : %s", p.String(), res.Status)
|
||||
}
|
||||
|
||||
result := ArtifactHubResults{}
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result.Packages, nil
|
||||
}
|
||||
|
||||
type ArtifactHubResults struct {
|
||||
Packages []*ArtifactHubResult `json:"packages"`
|
||||
}
|
||||
|
||||
type ArtifactHubResult struct {
|
||||
PackageId string `json:"package_id"`
|
||||
Name string `json:"name"`
|
||||
NormalizedName string `json:"normalized_name"`
|
||||
LogoImageId string `json:"logo_image_id"`
|
||||
Stars int `json:"stars"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
AppVersion string `json:"app_version"`
|
||||
Deprecated bool `json:"deprecated"`
|
||||
Signed bool `json:"signed"`
|
||||
ProductionOrganizationsCount int `json:"production_organizations_count"`
|
||||
Ts int `json:"ts"`
|
||||
Repository ArtifactHubRepo `json:"repository"`
|
||||
}
|
||||
|
||||
type ArtifactHubRepo struct {
|
||||
Url string `json:"url"`
|
||||
Kind int `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Official bool `json:"official"`
|
||||
DisplayName string `json:"display_name"`
|
||||
RepositoryId string `json:"repository_id"`
|
||||
ScannerDisabled bool `json:"scanner_disabled"`
|
||||
OrganizationName string `json:"organization_name"`
|
||||
VerifiedPublisher bool `json:"verified_publisher"`
|
||||
OrganizationDisplayName string `json:"organization_display_name"`
|
||||
}
|
||||
76
pkg/dashboard/objects/cache.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/eko/gocache/v3/marshaler"
|
||||
"github.com/eko/gocache/v3/store"
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CacheKey = string
|
||||
|
||||
type Cache struct {
|
||||
Marshaler *marshaler.Marshaler `json:"-"`
|
||||
HitCount int
|
||||
MissCount int
|
||||
}
|
||||
|
||||
func NewCache() *Cache {
|
||||
gocacheClient := gocache.New(60*time.Minute, 10*time.Minute)
|
||||
gocacheStore := store.NewGoCache(gocacheClient)
|
||||
|
||||
// TODO: use tiered cache with some disk backend, allow configuring that static cache folder
|
||||
|
||||
// Initializes marshaler
|
||||
marshal := marshaler.New(gocacheStore)
|
||||
return &Cache{
|
||||
Marshaler: marshal,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) String(key CacheKey, tags []string, callback func() (string, error)) (string, error) {
|
||||
if tags == nil {
|
||||
tags = make([]string, 0)
|
||||
}
|
||||
tags = append(tags, key)
|
||||
|
||||
ctx := context.Background()
|
||||
out := ""
|
||||
_, err := c.Marshaler.Get(ctx, key, &out)
|
||||
if err == nil {
|
||||
log.Debugf("Using cached value for %s", key)
|
||||
c.HitCount++
|
||||
return out, nil
|
||||
} else if !errors.Is(err, store.NotFound{}) {
|
||||
return "", err
|
||||
}
|
||||
c.MissCount++
|
||||
|
||||
out, err = callback()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = c.Marshaler.Set(ctx, key, out, store.WithTags(tags))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Cache) Invalidate(tags ...CacheKey) {
|
||||
log.Debugf("Invalidating tags %v", tags)
|
||||
err := c.Marshaler.Invalidate(context.Background(), store.WithInvalidateTags(tags))
|
||||
if err != nil {
|
||||
log.Warnf("Failed to invalidate tags %v: %s", tags, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) Clear() error {
|
||||
c.HitCount = 0
|
||||
c.MissCount = 0
|
||||
return c.Marshaler.Clear(context.Background())
|
||||
}
|
||||
238
pkg/dashboard/objects/data.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"io"
|
||||
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
type DataLayer struct {
|
||||
KubeContext string
|
||||
Scanners []subproc.Scanner
|
||||
StatusInfo *StatusInfo
|
||||
Namespaces []string
|
||||
Cache *Cache
|
||||
|
||||
ConfGen HelmConfigGetter
|
||||
appPerContext map[string]*Application
|
||||
appPerContextMx *sync.Mutex
|
||||
devel bool
|
||||
LocalCharts []string
|
||||
}
|
||||
|
||||
type StatusInfo struct {
|
||||
CurVer string
|
||||
LatestVer string
|
||||
Analytics bool
|
||||
CacheHitRatio float64
|
||||
ClusterMode bool
|
||||
}
|
||||
|
||||
func NewDataLayer(ns []string, ver string, cg HelmConfigGetter, devel bool) (*DataLayer, error) {
|
||||
if cg == nil {
|
||||
return nil, errors.New("HelmConfigGetter can't be nil")
|
||||
}
|
||||
|
||||
return &DataLayer{
|
||||
Namespaces: ns,
|
||||
Cache: NewCache(),
|
||||
StatusInfo: &StatusInfo{
|
||||
CurVer: ver,
|
||||
Analytics: false,
|
||||
},
|
||||
|
||||
ConfGen: cg,
|
||||
appPerContext: map[string]*Application{},
|
||||
appPerContextMx: new(sync.Mutex),
|
||||
devel: devel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) ListContexts() ([]KubeContext, error) {
|
||||
res := []KubeContext{}
|
||||
|
||||
if d.StatusInfo.ClusterMode {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
cfg, err := clientcmd.NewDefaultPathOptions().GetStartingConfig()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get kubectl config")
|
||||
}
|
||||
|
||||
for name, ctx := range cfg.Contexts {
|
||||
res = append(res, KubeContext{
|
||||
IsCurrent: cfg.CurrentContext == name,
|
||||
Name: name,
|
||||
Cluster: ctx.Cluster,
|
||||
AuthInfo: ctx.AuthInfo,
|
||||
Namespace: ctx.Namespace,
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) GetStatus() *StatusInfo {
|
||||
sum := float64(d.Cache.HitCount + d.Cache.MissCount)
|
||||
if sum > 0 {
|
||||
d.StatusInfo.CacheHitRatio = float64(d.Cache.HitCount) / sum
|
||||
} else {
|
||||
d.StatusInfo.CacheHitRatio = 0
|
||||
}
|
||||
return d.StatusInfo
|
||||
}
|
||||
|
||||
type SectionFn = func(*release.Release, bool) (string, error)
|
||||
|
||||
func ParseManifests(out string) ([]*v1.Carp, error) {
|
||||
dec := yaml.NewDecoder(bytes.NewReader([]byte(out)))
|
||||
|
||||
res := make([]*v1.Carp, 0)
|
||||
var tmp interface{}
|
||||
for {
|
||||
err := dec.Decode(&tmp)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to parse manifest document #%d", len(res)+1)
|
||||
}
|
||||
|
||||
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
|
||||
// we can juggle it
|
||||
jsoned, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var doc v1.Carp
|
||||
err = json.Unmarshal(jsoned, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if doc.Kind == "" {
|
||||
log.Warnf("Manifest piece is not k8s resource: %s", jsoned)
|
||||
continue
|
||||
}
|
||||
|
||||
res = append(res, &doc)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) SetContext(ctx string) error {
|
||||
if d.KubeContext != ctx {
|
||||
err := d.Cache.Clear()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to set context")
|
||||
}
|
||||
}
|
||||
|
||||
d.KubeContext = ctx
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) AppForCtx(ctx string) (*Application, error) {
|
||||
d.appPerContextMx.Lock()
|
||||
defer d.appPerContextMx.Unlock()
|
||||
|
||||
app, ok := d.appPerContext[ctx]
|
||||
if !ok {
|
||||
settings := cli.New()
|
||||
settings.KubeContext = ctx
|
||||
|
||||
settings.SetNamespace(d.nsForCtx(ctx))
|
||||
|
||||
cfgGetter := func(ns string) (*action.Configuration, error) {
|
||||
return d.ConfGen(settings, ns)
|
||||
}
|
||||
|
||||
a, err := NewApplication(settings, cfgGetter, d.Namespaces, d.devel)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "Failed to create application for context '%s'", ctx)
|
||||
}
|
||||
|
||||
a.Repositories.LocalCharts = d.LocalCharts
|
||||
|
||||
app = a
|
||||
d.appPerContext[ctx] = app
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (d *DataLayer) nsForCtx(ctx string) string {
|
||||
lst, err := d.ListContexts()
|
||||
if err != nil {
|
||||
log.Debugf("Failed to get contexts for NS lookup: %+v", err)
|
||||
}
|
||||
for _, c := range lst {
|
||||
if c.Name == ctx {
|
||||
return c.Namespace
|
||||
}
|
||||
}
|
||||
log.Debugf("Strange: no context found for '%s'", ctx)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d *DataLayer) PeriodicTasks(ctx context.Context) {
|
||||
// TODO: separate scanning setup for in-cluster?
|
||||
|
||||
if os.Getenv("HD_NO_AUTOUPDATE") == "" {
|
||||
// auto-update repos
|
||||
go d.loopUpdateRepos(ctx, 10*time.Minute) // TODO: parameterize interval?
|
||||
}
|
||||
|
||||
// auto-scan
|
||||
}
|
||||
|
||||
func (d *DataLayer) loopUpdateRepos(ctx context.Context, interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
for {
|
||||
app, err := d.AppForCtx("")
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get app object while in background repo update: %v", err)
|
||||
break // no point in retrying
|
||||
} else {
|
||||
repos, err := app.Repositories.List()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get list of repos while in background update: %v", err)
|
||||
}
|
||||
|
||||
for _, repo := range repos {
|
||||
err := repo.Update()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to update repo %s: %v", repo.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
continue
|
||||
}
|
||||
}
|
||||
log.Debugf("Update repo loop done.")
|
||||
}
|
||||
61
pkg/dashboard/objects/data_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
)
|
||||
|
||||
func TestNewDataLayer(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
namespaces []string
|
||||
version string
|
||||
helmConfig HelmConfigGetter
|
||||
devel bool
|
||||
errorExpected bool
|
||||
}{
|
||||
{
|
||||
name: "should return error when helm config is nil",
|
||||
namespaces: []string{"namespace1", "namespace2"},
|
||||
version: "1.0.0",
|
||||
helmConfig: nil,
|
||||
devel: false,
|
||||
errorExpected: true,
|
||||
},
|
||||
{
|
||||
name: "should return data layer when all parameters are correct",
|
||||
namespaces: []string{
|
||||
"namespace1",
|
||||
"namespace2",
|
||||
},
|
||||
version: "1.0.0",
|
||||
helmConfig: func(sett *cli.EnvSettings, ns string) (*action.Configuration, error) {
|
||||
return &action.Configuration{}, nil
|
||||
},
|
||||
devel: false,
|
||||
errorExpected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dl, err := NewDataLayer(tt.namespaces, tt.version, tt.helmConfig, tt.devel)
|
||||
if tt.errorExpected {
|
||||
assert.Error(t, err, "Expected error but got nil")
|
||||
} else {
|
||||
assert.Nil(t, err, "NewDataLayer returned an error: %v", err)
|
||||
assert.NotNil(t, dl, "NewDataLayer returned nil")
|
||||
assert.Equal(t, tt.namespaces, dl.Namespaces, "NewDataLayer returned incorrect namespaces: %v", dl.Namespaces)
|
||||
assert.NotNil(t, dl.Cache, "NewDataLayer returned nil cache")
|
||||
assert.Equal(t, tt.version, dl.StatusInfo.CurVer, "NewDataLayer returned incorrect version: %v", dl.StatusInfo.CurVer)
|
||||
assert.False(t, dl.StatusInfo.Analytics, "NewDataLayer returned incorrect version: %v", dl.StatusInfo.CurVer)
|
||||
assert.NotNil(t, dl.appPerContext, "NewDataLayer returned nil appPerContext")
|
||||
assert.NotNil(t, dl.ConfGen, "NewDataLayer returned nil ConfGen")
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
198
pkg/dashboard/objects/kubectl.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/kube"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
testapiv1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
describecmd "k8s.io/kubectl/pkg/cmd/describe"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/describe"
|
||||
"k8s.io/utils/strings/slices"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type KubeContext struct {
|
||||
IsCurrent bool
|
||||
Name string
|
||||
Cluster string
|
||||
AuthInfo string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// maps action.RESTClientGetter into genericclioptions.RESTClientGetter
|
||||
type cfgProxyObject struct {
|
||||
Impl action.RESTClientGetter
|
||||
}
|
||||
|
||||
func (p *cfgProxyObject) ToRESTConfig() (*rest.Config, error) {
|
||||
return p.Impl.ToRESTConfig()
|
||||
}
|
||||
|
||||
func (p *cfgProxyObject) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||
return p.Impl.ToDiscoveryClient()
|
||||
}
|
||||
|
||||
func (p *cfgProxyObject) ToRESTMapper() (meta.RESTMapper, error) {
|
||||
return p.Impl.ToRESTMapper()
|
||||
}
|
||||
|
||||
func (p *cfgProxyObject) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||
panic("Not implemented, stub")
|
||||
}
|
||||
|
||||
type K8s struct {
|
||||
Namespaces []string
|
||||
Factory kube.Factory
|
||||
RestClientGetter genericclioptions.RESTClientGetter
|
||||
}
|
||||
|
||||
func NewK8s(helmConfig *action.Configuration, namespaces []string) (*K8s, error) {
|
||||
factory := cmdutil.NewFactory(&cfgProxyObject{Impl: helmConfig.RESTClientGetter})
|
||||
|
||||
return &K8s{
|
||||
Namespaces: namespaces,
|
||||
Factory: factory,
|
||||
RestClientGetter: factory,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (k *K8s) GetNameSpaces() (res *corev1.NamespaceList, err error) {
|
||||
clientset, err := k.Factory.KubernetesClientSet()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get KubernetesClientSet")
|
||||
}
|
||||
|
||||
lst, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get list of namespaces")
|
||||
}
|
||||
|
||||
if !slices.Contains(k.Namespaces, "") {
|
||||
filtered := []corev1.Namespace{}
|
||||
for _, ns := range lst.Items {
|
||||
if slices.Contains(k.Namespaces, ns.Name) {
|
||||
filtered = append(filtered, ns)
|
||||
}
|
||||
}
|
||||
lst.Items = filtered
|
||||
}
|
||||
|
||||
return lst, nil
|
||||
}
|
||||
|
||||
func (k *K8s) DescribeResource(kind string, ns string, name string) (string, error) {
|
||||
log.Debugf("Describing resource: %s %s in %s", kind, name, ns)
|
||||
streams, _, out, errout := genericclioptions.NewTestIOStreams()
|
||||
o := &describecmd.DescribeOptions{
|
||||
Describer: func(mapping *meta.RESTMapping) (describe.ResourceDescriber, error) {
|
||||
return describe.DescriberFn(k.RestClientGetter, mapping)
|
||||
},
|
||||
FilenameOptions: &resource.FilenameOptions{},
|
||||
DescriberSettings: &describe.DescriberSettings{
|
||||
ShowEvents: true,
|
||||
ChunkSize: cmdutil.DefaultChunkSize,
|
||||
},
|
||||
|
||||
IOStreams: streams,
|
||||
|
||||
NewBuilder: k.Factory.NewBuilder,
|
||||
}
|
||||
|
||||
o.Namespace = ns
|
||||
o.BuilderArgs = []string{kind, name}
|
||||
|
||||
err := o.Run()
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "Failed to run describe command: %s", errout.String())
|
||||
}
|
||||
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.Object, error) {
|
||||
builder := k.Factory.NewBuilder()
|
||||
resp := builder.Unstructured().NamespaceParam(namespace).Flatten().ResourceNames(kind, name).Do()
|
||||
if resp.Err() != nil {
|
||||
return nil, errorx.Decorate(resp.Err(), "failed to get k8s resource")
|
||||
}
|
||||
|
||||
obj, err := resp.Object()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get k8s resulting object")
|
||||
}
|
||||
return &obj, nil
|
||||
}
|
||||
|
||||
func (k *K8s) GetResourceInfo(kind string, namespace string, name string) (*testapiv1.Carp, error) {
|
||||
obj, err := k.GetResource(kind, namespace, name)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get k8s object")
|
||||
}
|
||||
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to marshal k8s object into JSON")
|
||||
}
|
||||
|
||||
res := new(testapiv1.Carp)
|
||||
err = json.Unmarshal(data, &res)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to decode k8s object from JSON")
|
||||
}
|
||||
|
||||
sort.Slice(res.Status.Conditions, func(i, j int) bool {
|
||||
// some condition types always bubble up
|
||||
if res.Status.Conditions[i].Type == "Available" {
|
||||
return false
|
||||
}
|
||||
|
||||
if res.Status.Conditions[j].Type == "Available" {
|
||||
return true
|
||||
}
|
||||
|
||||
t1 := res.Status.Conditions[i].LastTransitionTime
|
||||
t2 := res.Status.Conditions[j].LastTransitionTime
|
||||
return t1.Time.Before(t2.Time)
|
||||
})
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (k *K8s) GetResourceYAML(kind string, namespace string, name string) (string, error) {
|
||||
obj, err := k.GetResource(kind, namespace, name)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to get k8s object")
|
||||
}
|
||||
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to marshal k8s object into JSON")
|
||||
}
|
||||
|
||||
res := map[string]interface{}{}
|
||||
err = json.Unmarshal(data, &res)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to decode k8s object from JSON")
|
||||
}
|
||||
|
||||
ydata, err := yaml.Marshal(res)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to marshal k8s object into JSON")
|
||||
}
|
||||
return string(ydata), nil
|
||||
}
|
||||
401
pkg/dashboard/objects/releases.go
Normal file
@@ -0,0 +1,401 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/downloader"
|
||||
"helm.sh/helm/v3/pkg/getter"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
)
|
||||
|
||||
type Releases struct {
|
||||
Namespaces []string
|
||||
HelmConfig HelmNSConfigGetter
|
||||
Settings *cli.EnvSettings
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
func (a *Releases) List() ([]*Release, error) {
|
||||
a.mx.Lock()
|
||||
defer a.mx.Unlock()
|
||||
|
||||
releases := []*Release{}
|
||||
for _, ns := range a.Namespaces {
|
||||
log.Debugf("Listing releases in namespace: %s", ns)
|
||||
hc, err := a.HelmConfig(ns)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
|
||||
}
|
||||
|
||||
client := action.NewList(hc)
|
||||
client.All = true
|
||||
client.AllNamespaces = true
|
||||
client.Limit = 0
|
||||
client.SetStateMask() // required to apply proper filtering
|
||||
rels, err := client.Run()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get list of releases")
|
||||
}
|
||||
for _, r := range rels {
|
||||
releases = append(releases, &Release{HelmConfig: a.HelmConfig, Orig: r, Settings: a.Settings})
|
||||
}
|
||||
}
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
func (a *Releases) ByName(namespace string, name string) (*Release, error) {
|
||||
rels, err := a.List()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get list of releases")
|
||||
}
|
||||
|
||||
for _, r := range rels {
|
||||
if r.Orig.Namespace == namespace && r.Orig.Name == name {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errorx.DataUnavailable.New(fmt.Sprintf("release '%s' is not found in namespace '%s'", name, namespace))
|
||||
}
|
||||
|
||||
func (a *Releases) Install(namespace string, name string, repoChart string, version string, justTemplate bool, values map[string]interface{}) (*release.Release, error) {
|
||||
a.mx.Lock()
|
||||
defer a.mx.Unlock()
|
||||
|
||||
if namespace == "" {
|
||||
namespace = a.Settings.Namespace()
|
||||
}
|
||||
|
||||
hc, err := a.HelmConfig(namespace)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
|
||||
}
|
||||
|
||||
cmd := action.NewInstall(hc)
|
||||
|
||||
cmd.ReleaseName = name
|
||||
cmd.CreateNamespace = true
|
||||
cmd.Namespace = namespace
|
||||
cmd.Version = version
|
||||
|
||||
cmd.DryRun = justTemplate
|
||||
|
||||
chrt, err := locateChart(cmd.ChartPathOptions, repoChart, a.Settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := cmd.Run(chrt, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !justTemplate {
|
||||
log.Infof("Installed new release: %s/%s", namespace, name)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func locateChart(pathOpts action.ChartPathOptions, chart string, settings *cli.EnvSettings) (*chart.Chart, error) {
|
||||
// from cmd/helm/install.go and cmd/helm/upgrade.go
|
||||
cp, err := pathOpts.LocateChart(chart, settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Located chart %s: %s\n", chart, cp)
|
||||
|
||||
p := getter.All(settings)
|
||||
|
||||
// Check chart dependencies to make sure all are present in /charts
|
||||
chartRequested, err := loader.Load(cp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := checkIfInstallable(chartRequested); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req := chartRequested.Metadata.Dependencies; req != nil {
|
||||
// If CheckDependencies returns an error, we have unfulfilled dependencies.
|
||||
// As of Helm 2.4.0, this is treated as a stopping condition:
|
||||
// https://github.com/helm/helm/issues/2209
|
||||
if err := action.CheckDependencies(chartRequested, req); err != nil {
|
||||
err = errorx.Decorate(err, "An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies")
|
||||
if true { // client.DependencyUpdate
|
||||
man := &downloader.Manager{
|
||||
Out: ioutil.Discard,
|
||||
ChartPath: cp,
|
||||
Keyring: pathOpts.Keyring,
|
||||
SkipUpdate: false,
|
||||
Getters: p,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
Debug: settings.Debug,
|
||||
}
|
||||
if err := man.Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Reload the chart with the updated Chart.lock file.
|
||||
if chartRequested, err = loader.Load(cp); err != nil {
|
||||
return nil, errorx.Decorate(err, "failed reloading chart after repo update")
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chartRequested, nil
|
||||
}
|
||||
|
||||
type Release struct {
|
||||
Settings *cli.EnvSettings
|
||||
HelmConfig HelmNSConfigGetter
|
||||
Orig *release.Release
|
||||
revisions []*Release
|
||||
mx sync.Mutex
|
||||
restoredChartPath string
|
||||
}
|
||||
|
||||
func (r *Release) History() ([]*Release, error) {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
hc, err := r.HelmConfig(r.Orig.Namespace)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
|
||||
}
|
||||
|
||||
client := action.NewHistory(hc)
|
||||
revs, err := client.Run(r.Orig.Name)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get revisions of release")
|
||||
}
|
||||
|
||||
r.revisions = []*Release{}
|
||||
for _, rev := range revs {
|
||||
r.revisions = append(r.revisions, &Release{HelmConfig: r.HelmConfig, Orig: rev, Settings: r.Settings})
|
||||
}
|
||||
|
||||
return r.revisions, nil
|
||||
}
|
||||
|
||||
func (r *Release) Uninstall() error {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
hc, err := r.HelmConfig(r.Orig.Namespace)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
|
||||
}
|
||||
|
||||
client := action.NewUninstall(hc)
|
||||
_, err = client.Run(r.Orig.Name)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "failed to uninstall release")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Release) Rollback(toRevision int) error {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
hc, err := r.HelmConfig(r.Orig.Namespace)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
|
||||
}
|
||||
|
||||
client := action.NewRollback(hc)
|
||||
client.Version = toRevision
|
||||
err = client.Run(r.Orig.Name)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "failed to rollback the release")
|
||||
}
|
||||
log.Infof("Rolled back %s/%s to %d=>%d", r.Orig.Namespace, r.Orig.Name, r.Orig.Version, toRevision)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Release) RunTests() (string, error) {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
hc, err := r.HelmConfig(r.Orig.Namespace)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to get helm config for namespace '%s'", r.Orig.Namespace)
|
||||
}
|
||||
|
||||
client := action.NewReleaseTesting(hc)
|
||||
client.Namespace = r.Orig.Namespace
|
||||
|
||||
rel, err := client.Run(r.Orig.Name)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to execute 'helm test' for release '%s'", r.Orig.Name)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := client.GetPodLogs(&buf, rel); err != nil {
|
||||
return "", errorx.Decorate(err, "failed to fetch logs for 'helm test' command")
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (r *Release) ParsedManifests() ([]*v1.Carp, error) {
|
||||
carps, err := ParseManifests(r.Orig.Manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, carp := range carps {
|
||||
if carp.Namespace == "" {
|
||||
carp.Namespace = r.Orig.Namespace
|
||||
}
|
||||
}
|
||||
|
||||
return carps, err
|
||||
}
|
||||
|
||||
func (r *Release) GetRev(revNo int) (*Release, error) {
|
||||
if revNo == 0 {
|
||||
revNo = r.Orig.Version
|
||||
}
|
||||
|
||||
hist, err := r.History()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get history")
|
||||
}
|
||||
|
||||
for _, rev := range hist {
|
||||
if rev.Orig.Version == revNo {
|
||||
return rev, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errorx.InternalError.New("No revision found for number %d", revNo)
|
||||
}
|
||||
|
||||
func (r *Release) Upgrade(repoChart string, version string, justTemplate bool, values map[string]interface{}) (*release.Release, error) {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
// if repo chart is not passed, let's try to restore it from secret
|
||||
if repoChart == "" {
|
||||
var err error
|
||||
repoChart, err = r.restoreChart()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to revive chart for release")
|
||||
}
|
||||
}
|
||||
|
||||
ns := r.Settings.Namespace()
|
||||
if r.Orig != nil {
|
||||
ns = r.Orig.Namespace
|
||||
}
|
||||
|
||||
hc, err := r.HelmConfig(ns)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", ns)
|
||||
}
|
||||
|
||||
cmd := action.NewUpgrade(hc)
|
||||
|
||||
cmd.Namespace = r.Settings.Namespace()
|
||||
cmd.Version = version
|
||||
|
||||
cmd.DryRun = justTemplate
|
||||
cmd.ResetValues = true
|
||||
|
||||
chrt, err := locateChart(cmd.ChartPathOptions, repoChart, r.Settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := cmd.Run(r.Orig.Name, chrt, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !justTemplate {
|
||||
log.Infof("Upgraded release: %s/%s#%d", res.Namespace, res.Name, res.Version)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *Release) restoreChart() (string, error) {
|
||||
if r.restoredChartPath != "" {
|
||||
return r.restoredChartPath, nil
|
||||
}
|
||||
|
||||
// we're unlikely to have the original chart, let's try the cheesy thing...
|
||||
|
||||
log.Infof("Attempting to restore the chart for %s", r.Orig.Name)
|
||||
dir, err := ioutil.TempDir("", "khd-*")
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to get temporary directory")
|
||||
}
|
||||
|
||||
//restore Chart.yaml
|
||||
cdata, err := yaml.Marshal(r.Orig.Chart.Metadata)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to restore Chart.yaml")
|
||||
}
|
||||
err = ioutil.WriteFile(path.Join(dir, "Chart.yaml"), cdata, 0644)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to write file Chart.yaml")
|
||||
}
|
||||
|
||||
//restore known values
|
||||
vdata, err := yaml.Marshal(r.Orig.Chart.Values)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to restore values.yaml")
|
||||
}
|
||||
err = ioutil.WriteFile(path.Join(dir, "values.yaml"), vdata, 0644)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to write file values.yaml")
|
||||
}
|
||||
|
||||
// if possible, overwrite files with better alternatives
|
||||
for _, f := range append(r.Orig.Chart.Raw, r.Orig.Chart.Templates...) {
|
||||
fname := path.Join(dir, f.Name)
|
||||
log.Debugf("Restoring file: %s", fname)
|
||||
err := os.MkdirAll(path.Dir(fname), 0755)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to create directory for file: %s", fname)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(fname, f.Data, 0644)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to write file to restore chart: %s", fname)
|
||||
}
|
||||
}
|
||||
|
||||
r.restoredChartPath = dir
|
||||
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
func checkIfInstallable(ch *chart.Chart) error {
|
||||
switch ch.Metadata.Type {
|
||||
case "", "application":
|
||||
return nil
|
||||
}
|
||||
return errors.Errorf("%s charts are not installable", ch.Metadata.Type)
|
||||
}
|
||||
80
pkg/dashboard/objects/releases_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
kubefake "helm.sh/helm/v3/pkg/kube/fake"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
"helm.sh/helm/v3/pkg/storage"
|
||||
"helm.sh/helm/v3/pkg/storage/driver"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeKubeClient *kubefake.PrintingKubeClient
|
||||
fakeStorage *storage.Storage
|
||||
)
|
||||
|
||||
func fakeHelmNSConfigGetter(ns string) (*action.Configuration, error) {
|
||||
return &action.Configuration{
|
||||
KubeClient: fakeKubeClient,
|
||||
Releases: fakeStorage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestListReleases(t *testing.T) {
|
||||
fakeStorage = storage.Init(driver.NewMemory())
|
||||
err := fakeStorage.Create(&release.Release{
|
||||
Name: "release1",
|
||||
Info: &release.Info{
|
||||
Status: release.StatusDeployed,
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
err = fakeStorage.Create(&release.Release{
|
||||
Name: "release2",
|
||||
Info: &release.Info{
|
||||
Status: release.StatusDeployed,
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
err = fakeStorage.Create(&release.Release{
|
||||
Name: "release3",
|
||||
Info: &release.Info{
|
||||
Status: release.StatusDeployed,
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
err = fakeStorage.Create(&release.Release{
|
||||
Name: "release4",
|
||||
Info: &release.Info{
|
||||
Status: release.StatusDeployed,
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
err = fakeStorage.Create(&release.Release{
|
||||
Name: "release5",
|
||||
Info: &release.Info{
|
||||
Status: release.StatusDeployed,
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
releases := &Releases{
|
||||
Namespaces: []string{"testNamespace"},
|
||||
HelmConfig: fakeHelmNSConfigGetter,
|
||||
mx: sync.Mutex{},
|
||||
}
|
||||
|
||||
res, err := releases.List()
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(res), 5)
|
||||
assert.Equal(t, res[0].Orig.Name, "release1")
|
||||
assert.Equal(t, res[1].Orig.Name, "release2")
|
||||
assert.Equal(t, res[2].Orig.Name, "release3")
|
||||
assert.Equal(t, res[3].Orig.Name, "release4")
|
||||
assert.Equal(t, res[4].Orig.Name, "release5")
|
||||
}
|
||||
427
pkg/dashboard/objects/repos.go
Normal file
@@ -0,0 +1,427 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/getter"
|
||||
"helm.sh/helm/v3/pkg/helmpath"
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
)
|
||||
|
||||
const AnnRepo = "helm-dashboard/repository-name"
|
||||
|
||||
type Repositories struct {
|
||||
Settings *cli.EnvSettings
|
||||
HelmConfig *action.Configuration
|
||||
mx sync.Mutex
|
||||
versionConstraint *semver.Constraints
|
||||
LocalCharts []string
|
||||
}
|
||||
|
||||
func (r *Repositories) load() (*repo.File, error) {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
// copied from cmd/helm/repo_list.go
|
||||
f, err := repo.LoadFile(r.Settings.RepositoryConfig)
|
||||
if err != nil && !isNotExist(err) {
|
||||
return nil, errorx.Decorate(err, "failed to load repository list")
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (r *Repositories) List() ([]Repository, error) {
|
||||
f, err := r.load()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to load repo information")
|
||||
}
|
||||
|
||||
res := []Repository{}
|
||||
for _, item := range f.Repositories {
|
||||
res = append(res, &HelmRepo{
|
||||
Settings: r.Settings,
|
||||
Orig: item,
|
||||
versionConstraint: r.versionConstraint,
|
||||
})
|
||||
}
|
||||
|
||||
if len(r.LocalCharts) > 0 {
|
||||
lc := LocalChart{
|
||||
LocalCharts: r.LocalCharts,
|
||||
}
|
||||
res = append(res, &lc)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *Repositories) Add(name string, url string) error {
|
||||
if name == "" || url == "" {
|
||||
return errors.New("Name and URL are required parameters to add the repository")
|
||||
}
|
||||
|
||||
// copied from cmd/helm/repo_add.go
|
||||
repoFile := r.Settings.RepositoryConfig
|
||||
|
||||
// Ensure the file directory exists as it is required for file locking
|
||||
err := os.MkdirAll(filepath.Dir(repoFile), os.ModePerm)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := r.load()
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "Failed to load repo config")
|
||||
}
|
||||
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
c := repo.Entry{
|
||||
Name: name,
|
||||
URL: url,
|
||||
//Username: o.username,
|
||||
//Password: o.password,
|
||||
//PassCredentialsAll: o.passCredentialsAll,
|
||||
//CertFile: o.certFile,
|
||||
//KeyFile: o.keyFile,
|
||||
//CAFile: o.caFile,
|
||||
//InsecureSkipTLSverify: o.insecureSkipTLSverify,
|
||||
}
|
||||
|
||||
// Check if the repo name is legal
|
||||
if strings.Contains(c.Name, "/") {
|
||||
return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", c.Name)
|
||||
}
|
||||
|
||||
rep, err := repo.NewChartRepository(&c, getter.All(r.Settings))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := rep.DownloadIndexFile(); err != nil {
|
||||
return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", url)
|
||||
}
|
||||
|
||||
f.Update(&c)
|
||||
|
||||
if err := f.WriteFile(repoFile, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repositories) Delete(name string) error {
|
||||
f, err := r.load()
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "failed to load repo information")
|
||||
}
|
||||
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
// copied from cmd/helm/repo_remove.go
|
||||
if !f.Remove(name) {
|
||||
return errors.Errorf("no repo named %q found", name)
|
||||
}
|
||||
if err := f.WriteFile(r.Settings.RepositoryConfig, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := removeRepoCache(r.Settings.RepositoryCache, name); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repositories) Get(name string) (Repository, error) {
|
||||
l, err := r.List()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get list of repos")
|
||||
}
|
||||
|
||||
for _, entry := range l {
|
||||
if entry.Name() == name {
|
||||
return entry, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errorx.DataUnavailable.New("Could not find repository '%s'", name)
|
||||
}
|
||||
|
||||
// Containing returns list of chart versions for the given chart name, across all repositories
|
||||
func (r *Repositories) Containing(name string) (repo.ChartVersions, error) {
|
||||
list, err := r.List()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get list of repos")
|
||||
}
|
||||
|
||||
res := repo.ChartVersions{}
|
||||
for _, rep := range list {
|
||||
vers, err := rep.ByName(name)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get data from repo '%s', updating it might help", rep.Name())
|
||||
log.Debugf("The error was: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var updatedChartVersions repo.ChartVersions
|
||||
for _, v := range vers {
|
||||
// just using annotations here to attach a bit of information to the object
|
||||
// it has nothing to do with k8s annotations and should not get into manifests
|
||||
if v.Annotations == nil {
|
||||
v.Annotations = map[string]string{}
|
||||
}
|
||||
|
||||
v.Annotations[AnnRepo] = rep.Name()
|
||||
|
||||
// Validate the versions against semantic version constraints and filter
|
||||
version, err := semver.NewVersion(v.Version)
|
||||
if err != nil {
|
||||
// Ignored if version string is not parsable
|
||||
log.Debugf("failed to parse version string %q: %v", v.Version, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if r.versionConstraint.Check(version) {
|
||||
// Add only versions that satisfy the semantic version constraint
|
||||
updatedChartVersions = append(updatedChartVersions, v)
|
||||
}
|
||||
}
|
||||
|
||||
res = append(res, updatedChartVersions...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *Repositories) GetChartValues(chart string, ver string) (string, error) {
|
||||
// comes from cmd/helm/show.go
|
||||
client := action.NewShowWithConfig(action.ShowValues, r.HelmConfig)
|
||||
client.Version = ver
|
||||
|
||||
cp, err := client.ChartPathOptions.LocateChart(chart, r.Settings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
out, err := client.Run(cp)
|
||||
if err != nil {
|
||||
return "", errorx.Decorate(err, "failed to get values for chart '%s'", chart)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
Name() string
|
||||
URL() string
|
||||
Update() error
|
||||
Charts() (repo.ChartVersions, error)
|
||||
ByName(name string) (repo.ChartVersions, error)
|
||||
}
|
||||
|
||||
type HelmRepo struct {
|
||||
Settings *cli.EnvSettings
|
||||
Orig *repo.Entry
|
||||
mx sync.Mutex
|
||||
|
||||
versionConstraint *semver.Constraints
|
||||
}
|
||||
|
||||
func (r *HelmRepo) Name() string {
|
||||
return r.Orig.Name
|
||||
}
|
||||
|
||||
func (r *HelmRepo) URL() string {
|
||||
return r.Orig.URL
|
||||
}
|
||||
|
||||
func (r *HelmRepo) indexFileName() string {
|
||||
return filepath.Join(r.Settings.RepositoryCache, helmpath.CacheIndexFile(r.Orig.Name))
|
||||
}
|
||||
|
||||
func (r *HelmRepo) getIndex() (*repo.IndexFile, error) {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
f := r.indexFileName()
|
||||
ind, err := repo.LoadIndexFile(f)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "Repo index is corrupt or missing. Try updating repo")
|
||||
}
|
||||
|
||||
ind.SortEntries()
|
||||
return ind, nil
|
||||
}
|
||||
|
||||
func (r *HelmRepo) Charts() (repo.ChartVersions, error) {
|
||||
ind, err := r.getIndex()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get repo index")
|
||||
}
|
||||
|
||||
res := repo.ChartVersions{}
|
||||
for _, cv := range ind.Entries {
|
||||
for _, v := range cv {
|
||||
version, err := semver.NewVersion(v.Version)
|
||||
if err != nil {
|
||||
// Ignored if version string is not parsable
|
||||
log.Debugf("failed to parse version string %q: %v", v.Version, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if r.versionConstraint.Check(version) {
|
||||
// Add only versions that satisfy the semantic version constraint
|
||||
res = append(res, v)
|
||||
|
||||
// Only the highest version satisfying the constraint is required. Hence, break.
|
||||
// The constraint here is (only stable versions) vs (stable + dev/prerelease).
|
||||
// If dev versions are disabled and chart only has dev versions,
|
||||
// chart is excluded from the result.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *HelmRepo) ByName(name string) (repo.ChartVersions, error) {
|
||||
ind, err := r.getIndex()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get repo index")
|
||||
}
|
||||
|
||||
nx, ok := ind.Entries[name]
|
||||
if ok {
|
||||
return nx, nil
|
||||
}
|
||||
return repo.ChartVersions{}, nil
|
||||
}
|
||||
|
||||
func (r *HelmRepo) Update() error {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
log.Infof("Updating repository: %s", r.Orig.Name)
|
||||
|
||||
// from cmd/helm/repo_update.go
|
||||
|
||||
// TODO: make this object to be an `Orig`?
|
||||
rep, err := repo.NewChartRepository(r.Orig, getter.All(r.Settings))
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "could not create repository object")
|
||||
}
|
||||
rep.CachePath = r.Settings.RepositoryCache
|
||||
|
||||
_, err = rep.DownloadIndexFile()
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "failed to download repo index file")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// copied from cmd/helm/repo.go
|
||||
func isNotExist(err error) bool {
|
||||
return os.IsNotExist(errors.Cause(err))
|
||||
}
|
||||
|
||||
// copied from cmd/helm/repo_remove.go
|
||||
func removeRepoCache(root, name string) error {
|
||||
idx := filepath.Join(root, helmpath.CacheChartsFile(name))
|
||||
if _, err := os.Stat(idx); err == nil {
|
||||
_ = os.Remove(idx)
|
||||
}
|
||||
|
||||
idx = filepath.Join(root, helmpath.CacheIndexFile(name))
|
||||
if _, err := os.Stat(idx); os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return errors.Wrapf(err, "can't remove index file %s", idx)
|
||||
}
|
||||
return os.Remove(idx)
|
||||
}
|
||||
|
||||
// versionConstaint returns semantic version constraint instance that can be used to
|
||||
// validate the version of repositories. The flag isDevelEnabled is used to configure
|
||||
// enabling/disabling of development/prerelease versions of charts.
|
||||
func versionConstaint(isDevelEnabled bool) (*semver.Constraints, error) {
|
||||
// When devel flag is disabled. i.e., Only stable releases are included.
|
||||
version := ">0.0.0"
|
||||
|
||||
if isDevelEnabled {
|
||||
// When devel flag is enabled. i.e., Prereleases (alpha, beta, release candidate, etc.) are included.
|
||||
version = ">0.0.0-0"
|
||||
}
|
||||
|
||||
constraint, err := semver.NewConstraint(version)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid version constraint format %q", version)
|
||||
}
|
||||
|
||||
return constraint, nil
|
||||
}
|
||||
|
||||
type LocalChart struct {
|
||||
LocalCharts []string
|
||||
|
||||
charts map[string]repo.ChartVersions
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
// Update reloads the chart information from disk
|
||||
func (l *LocalChart) Update() error {
|
||||
l.mx.Lock()
|
||||
defer l.mx.Unlock()
|
||||
|
||||
l.charts = map[string]repo.ChartVersions{}
|
||||
for _, lc := range l.LocalCharts {
|
||||
c, err := loader.Load(lc)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to load chart from '%s': %s", lc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// we don't filter out dev versions here, because local chart implies user wants to see the chart anyway
|
||||
l.charts[c.Name()] = repo.ChartVersions{&repo.ChartVersion{
|
||||
URLs: []string{l.URL() + lc},
|
||||
Metadata: c.Metadata,
|
||||
}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LocalChart) Name() string {
|
||||
return "[local]"
|
||||
}
|
||||
|
||||
func (l *LocalChart) URL() string {
|
||||
return "file://"
|
||||
}
|
||||
|
||||
func (l *LocalChart) Charts() (repo.ChartVersions, error) {
|
||||
_ = l.Update() // always re-read, for chart devs to have quick debug loop
|
||||
res := repo.ChartVersions{}
|
||||
for _, c := range l.charts {
|
||||
res = append(res, c...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (l *LocalChart) ByName(name string) (repo.ChartVersions, error) {
|
||||
_ = l.Update() // always re-read, for chart devs to have quick debug loop
|
||||
for n, c := range l.charts {
|
||||
if n == name {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return repo.ChartVersions{}, nil
|
||||
}
|
||||
290
pkg/dashboard/objects/repos_test.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
validRepositoryConfigPath = "./testdata/repositories.yaml"
|
||||
invalidCacheFileRepositoryConfigPath = "./testdata/repositories-invalid-cache-file.yaml"
|
||||
invalidMalformedManifestRepositoryConfigPath = "./testdata/repositories-malformed-manifest.yaml"
|
||||
)
|
||||
|
||||
func initRepository(t *testing.T, filePath string, devel bool) *Repositories {
|
||||
t.Helper()
|
||||
|
||||
settings := cli.New()
|
||||
|
||||
fname, err := ioutil.TempFile("", "repo-*.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
input, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(fname.Name(), input, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := os.Remove(fname.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
vc, err := versionConstaint(devel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Sets the repository file path
|
||||
settings.RepositoryConfig = fname.Name()
|
||||
settings.RepositoryCache = path.Dir(filePath)
|
||||
|
||||
testRepository := &Repositories{
|
||||
Settings: settings,
|
||||
HelmConfig: &action.Configuration{}, // maybe use copy of getFakeHelmConfig from api_test.go
|
||||
versionConstraint: vc,
|
||||
LocalCharts: []string{"../../../charts/helm-dashboard"},
|
||||
}
|
||||
|
||||
return testRepository
|
||||
}
|
||||
|
||||
func TestFlow(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, false)
|
||||
|
||||
// initial list
|
||||
repos, err := testRepository.List()
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, len(repos), 5)
|
||||
|
||||
testRepoName := "TEST"
|
||||
testRepoUrl := "https://helm.github.io/examples"
|
||||
|
||||
// add repo
|
||||
err = testRepository.Add(testRepoName, testRepoUrl)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// get repo
|
||||
r, err := testRepository.Get(testRepoName)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, r.URL(), testRepoUrl)
|
||||
|
||||
// update repo
|
||||
err = r.Update()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// list charts
|
||||
c, err := r.Charts()
|
||||
assert.NilError(t, err)
|
||||
|
||||
// contains chart
|
||||
c, err = testRepository.Containing(c[0].Name)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// chart by name from repo
|
||||
c, err = r.ByName(c[0].Name)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// get chart values
|
||||
v, err := testRepository.GetChartValues(r.Name()+"/"+c[0].Name, c[0].Version)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, v != "")
|
||||
|
||||
// delete added
|
||||
err = testRepository.Delete(testRepoName)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// final list
|
||||
repos, err = testRepository.List()
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, len(repos), 5)
|
||||
}
|
||||
|
||||
func TestRepository_Charts_DevelDisabled(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, false)
|
||||
|
||||
r, err := testRepository.Get("testing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
charts, err := r.Charts()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Total charts in ./testdata/testing-index.yaml = 4
|
||||
// Excluded charts = 2 (1 has invalid version, 1 has only dev version)
|
||||
// Included charts = 2 (2 stable versions)
|
||||
expectedCount := 2
|
||||
if len(charts) != expectedCount {
|
||||
t.Fatalf("Wrong charts count: %d, expected: %d", len(charts), expectedCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_Charts_DevelEnabled(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, true)
|
||||
|
||||
r, err := testRepository.Get("testing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
charts, err := r.Charts()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Total charts in ./testdata/testing-index.yaml = 4
|
||||
// Excluded charts = 1 (1 has invalid version)
|
||||
// Included charts = 3 (2 stable versions, 1 has only dev version)
|
||||
expectedCount := 3
|
||||
if len(charts) != expectedCount {
|
||||
t.Fatalf("Wrong charts count: %d, expected: %d", len(charts), expectedCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_Charts_InvalidCacheFile(t *testing.T) {
|
||||
testRepository := initRepository(t, invalidCacheFileRepositoryConfigPath, false)
|
||||
|
||||
r, err := testRepository.Get("non-existing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = r.Charts()
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for invalid cache file path, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositories_Containing_DevelDisable(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, false)
|
||||
|
||||
chartVersions, err := testRepository.Containing("alpine")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Total versions of chart alpine in ./testdata/testing-index.yaml = 3
|
||||
// Excluded charts = 1 (1 dev version)
|
||||
// Included charts = 2 (2 stable versions)
|
||||
expectedCount := 2
|
||||
if len(chartVersions) != expectedCount {
|
||||
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRepositories_Containing_DevelEnabled(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, true)
|
||||
|
||||
chartVersions, err := testRepository.Containing("alpine")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Total versions of chart alpine in ./testdata/testing-index.yaml = 3
|
||||
// Excluded charts = 0
|
||||
// Included charts = 3 (2 stable versions, 1 dev version)
|
||||
expectedCount := 3
|
||||
if len(chartVersions) != expectedCount {
|
||||
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRepositories_Containing_DevelDisable_OnlyDevVersionsOfChartAvailable(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, false)
|
||||
|
||||
chartVersions, err := testRepository.Containing("traefik")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Total versions of chart traefik in ./testdata/testing-index.yaml = 1
|
||||
// Excluded charts = 1 (1 dev version)
|
||||
// Included charts = 0
|
||||
expectedCount := 0
|
||||
if len(chartVersions) != expectedCount {
|
||||
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRepositories_Containing_DevelEnabled_OnlyDevVersionsOfChartAvailable(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, true)
|
||||
|
||||
chartVersions, err := testRepository.Containing("traefik")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Total versions of chart traefik in ./testdata/testing-index.yaml = 1
|
||||
// Excluded charts = 0
|
||||
// Included charts = 1 (1 dev version)
|
||||
expectedCount := 1
|
||||
if len(chartVersions) != expectedCount {
|
||||
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRepositories_Containing_DevelDisable_InvalidChartVersion(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, false)
|
||||
|
||||
chartVersions, err := testRepository.Containing("rabbitmq")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Total versions of chart rabbitmq in ./testdata/testing-index.yaml = 1
|
||||
// Excluded charts = 1 (1 invalid version)
|
||||
// Included charts = 0
|
||||
expectedCount := 0
|
||||
if len(chartVersions) != expectedCount {
|
||||
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRepositories_Containing_DevelEnabled_InvalidChartVersion(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, true)
|
||||
|
||||
chartVersions, err := testRepository.Containing("rabbitmq")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Total versions of chart rabbitmq in ./testdata/testing-index.yaml = 1
|
||||
// Excluded charts = 1 (1 invalid version)
|
||||
// Included charts = 0
|
||||
expectedCount := 0
|
||||
if len(chartVersions) != expectedCount {
|
||||
t.Fatalf("Wrong charts versions count: %d, expected: %d", len(chartVersions), expectedCount)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRepositories_Containing_MalformedRepositoryConfigFile(t *testing.T) {
|
||||
testRepository := initRepository(t, invalidMalformedManifestRepositoryConfigPath, false)
|
||||
|
||||
_, err := testRepository.Containing("alpine")
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for malformed RepositoryConfig file, got nil")
|
||||
}
|
||||
}
|
||||
6
pkg/dashboard/objects/testdata/repositories-invalid-cache-file.yaml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: ""
|
||||
generated: "0001-01-01T00:00:00Z"
|
||||
repositories:
|
||||
- cache: non-existing-index.yaml
|
||||
name: non-existing
|
||||
url: http://example.com/charts
|
||||
12
pkg/dashboard/objects/testdata/repositories-malformed-manifest.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: ""
|
||||
generated: "0001-01-01T00:00:00Z"
|
||||
- repositories:
|
||||
- caFile: ""
|
||||
certFile: ""
|
||||
insecure_skip_tls_verify: false
|
||||
keyFile: ""
|
||||
name: charts
|
||||
pass_credentials_all: false
|
||||
password: ""
|
||||
url: https://charts.helm.sh/stable
|
||||
username: ""
|
||||
33
pkg/dashboard/objects/testdata/repositories.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
apiVersion: ""
|
||||
generated: "0001-01-01T00:00:00Z"
|
||||
repositories:
|
||||
- caFile: ""
|
||||
certFile: ""
|
||||
insecure_skip_tls_verify: false
|
||||
keyFile: ""
|
||||
name: charts
|
||||
pass_credentials_all: false
|
||||
password: ""
|
||||
url: https://charts.helm.sh/stable
|
||||
username: ""
|
||||
- caFile: ""
|
||||
certFile: ""
|
||||
insecure_skip_tls_verify: false
|
||||
keyFile: ""
|
||||
name: firstexample
|
||||
pass_credentials_all: false
|
||||
password: ""
|
||||
url: http://firstexample.com
|
||||
username: ""
|
||||
- caFile: ""
|
||||
certFile: ""
|
||||
insecure_skip_tls_verify: false
|
||||
keyFile: ""
|
||||
name: secondexample
|
||||
pass_credentials_all: false
|
||||
password: ""
|
||||
url: http://secondexample.com
|
||||
username: ""
|
||||
- cache: testing-index.yaml
|
||||
name: testing
|
||||
url: http://example.com/charts
|
||||
100
pkg/dashboard/objects/testdata/testing-index.yaml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
apiVersion: v1
|
||||
entries:
|
||||
alpine:
|
||||
- name: alpine
|
||||
url: https://charts.helm.sh/stable/alpine-0.1.0.tgz
|
||||
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
|
||||
created: "2018-06-27T10:00:18.230700509Z"
|
||||
deprecated: true
|
||||
home: https://helm.sh/helm
|
||||
sources:
|
||||
- https://github.com/helm/helm
|
||||
version: 0.1.0
|
||||
appVersion: 1.2.3
|
||||
description: Deploy a basic Alpine Linux pod
|
||||
keywords: []
|
||||
maintainers: []
|
||||
icon: ""
|
||||
apiVersion: v2
|
||||
- name: alpine
|
||||
url: https://charts.helm.sh/stable/alpine-0.2.0.tgz
|
||||
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
|
||||
created: "2018-07-09T11:34:37.797864902Z"
|
||||
home: https://helm.sh/helm
|
||||
sources:
|
||||
- https://github.com/helm/helm
|
||||
version: 0.2.0
|
||||
appVersion: 2.3.4
|
||||
description: Deploy a basic Alpine Linux pod
|
||||
keywords: []
|
||||
maintainers: []
|
||||
icon: ""
|
||||
apiVersion: v2
|
||||
- name: alpine
|
||||
url: https://charts.helm.sh/stable/alpine-0.3.0-rc.1.tgz
|
||||
checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d
|
||||
created: "2020-11-12T08:44:58.872726222Z"
|
||||
home: https://helm.sh/helm
|
||||
sources:
|
||||
- https://github.com/helm/helm
|
||||
version: 0.3.0-rc.1
|
||||
appVersion: 3.0.0
|
||||
description: Deploy a basic Alpine Linux pod
|
||||
keywords: []
|
||||
maintainers: []
|
||||
icon: ""
|
||||
apiVersion: v2
|
||||
mariadb:
|
||||
- name: mariadb
|
||||
url: https://charts.helm.sh/stable/mariadb-0.3.0.tgz
|
||||
checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56
|
||||
created: "2018-04-23T08:20:27.160959131Z"
|
||||
home: https://mariadb.org
|
||||
sources:
|
||||
- https://github.com/bitnami/bitnami-docker-mariadb
|
||||
version: 0.3.0
|
||||
description: Chart for MariaDB
|
||||
keywords:
|
||||
- mariadb
|
||||
- mysql
|
||||
- database
|
||||
- sql
|
||||
maintainers:
|
||||
- name: Bitnami
|
||||
email: containers@bitnami.com
|
||||
icon: ""
|
||||
apiVersion: v2
|
||||
traefik:
|
||||
- apiVersion: v1
|
||||
appVersion: 1.7.26
|
||||
deprecated: true
|
||||
description: A Traefik based Kubernetes ingress controller with Let's
|
||||
Encrypt support
|
||||
home: https://traefik.io/
|
||||
icon: https://docs.traefik.io/assets/img/traefik.logo.png
|
||||
keywords:
|
||||
- traefik
|
||||
- ingress
|
||||
- acme
|
||||
- letsencrypt
|
||||
name: traefik
|
||||
sources:
|
||||
- https://github.com/containous/traefik
|
||||
- https://github.com/helm/charts/tree/master/stable/traefik
|
||||
version: 1.87.7-rc1
|
||||
rabbitmq:
|
||||
- apiVersion: v1
|
||||
appVersion: 3.8.2
|
||||
deprecated: true
|
||||
description: DEPRECATED Open source message broker software that implements the Advanced
|
||||
Message Queuing Protocol (AMQP)
|
||||
home: https://www.rabbitmq.com
|
||||
icon: https://bitnami.com/assets/stacks/rabbitmq/img/rabbitmq-stack-220x234.png
|
||||
keywords:
|
||||
- rabbitmq
|
||||
- message queue
|
||||
- AMQP
|
||||
name: rabbitmq
|
||||
sources:
|
||||
- https://github.com/bitnami/bitnami-docker-rabbitmq
|
||||
version: invalid-version
|
||||
171
pkg/dashboard/scanners/checkov.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package scanners
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Checkov struct {
|
||||
Data *objects.DataLayer
|
||||
}
|
||||
|
||||
func (c *Checkov) ManifestIsScannable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Checkov) SupportedResourceKinds() []string {
|
||||
// from https://github.com/bridgecrewio/checkov//blob/master/docs/5.Policy%20Index/kubernetes.md
|
||||
return []string{
|
||||
"AdmissionConfiguration",
|
||||
"ClusterRole",
|
||||
"ClusterRoleBinding",
|
||||
"ConfigMap",
|
||||
"CronJob",
|
||||
"DaemonSet",
|
||||
"Deployment",
|
||||
"DeploymentConfig",
|
||||
"Ingress",
|
||||
"Job",
|
||||
"Pod",
|
||||
"PodSecurityPolicy",
|
||||
"PodTemplate",
|
||||
"Policy",
|
||||
"ReplicaSet",
|
||||
"ReplicationController",
|
||||
"Role",
|
||||
"RoleBinding",
|
||||
"Secret",
|
||||
"Service",
|
||||
"ServiceAccount",
|
||||
"StatefulSet",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Checkov) Name() string {
|
||||
return "Checkov"
|
||||
}
|
||||
|
||||
func (c *Checkov) Test() bool {
|
||||
utils.FailLogLevel = log.DebugLevel
|
||||
defer func() { utils.FailLogLevel = log.WarnLevel }()
|
||||
|
||||
res, err := utils.RunCommand([]string{"checkov", "--version"}, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
log.Infof("Discovered Checkov version: %s", strings.TrimSpace(res))
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) {
|
||||
fname, fclose, err := utils.TempFile(mnf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fclose()
|
||||
|
||||
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "json", "--file", fname}
|
||||
out, err := utils.RunCommand(cmd, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &subproc.ScanResults{}
|
||||
|
||||
err = json.Unmarshal([]byte(out), &res.OrigReport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Checkov) ScanResource(ns string, kind string, name string) (*subproc.ScanResults, error) {
|
||||
carp := v1.Carp{}
|
||||
carp.Kind = kind
|
||||
carp.Name = name
|
||||
app, err := c.Data.AppForCtx(c.Data.KubeContext)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get app for context")
|
||||
}
|
||||
|
||||
mnf, err := app.K8s.GetResourceYAML(kind, ns, name)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get YAML for resource")
|
||||
}
|
||||
|
||||
fname, fclose, err := utils.TempFile(mnf)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to create temporary file")
|
||||
}
|
||||
defer fclose()
|
||||
|
||||
cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "json", "--file", fname}
|
||||
out, err := utils.RunCommand(cmd, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cr := CheckovReport{}
|
||||
err = json.Unmarshal([]byte(out), &cr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &subproc.ScanResults{
|
||||
PassedCount: cr.Summary.Passed,
|
||||
FailedCount: cr.Summary.Failed,
|
||||
OrigReport: checkovReportTable(&cr),
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func checkovReportTable(c *CheckovReport) string {
|
||||
data := [][]string{}
|
||||
for _, item := range c.Results.FailedChecks {
|
||||
data = append(data, []string{item.Id, item.Name + "\n", item.Guideline})
|
||||
}
|
||||
|
||||
tableString := &strings.Builder{}
|
||||
table := tablewriter.NewWriter(tableString)
|
||||
table.SetHeader([]string{"ID", "Name", "Guideline"})
|
||||
table.SetBorder(false)
|
||||
table.SetColWidth(64)
|
||||
table.AppendBulk(data)
|
||||
table.Render()
|
||||
return tableString.String()
|
||||
}
|
||||
|
||||
type CheckovReport struct {
|
||||
Summary CheckovSummary `json:"summary"`
|
||||
Results CheckovResults `json:"results"`
|
||||
}
|
||||
|
||||
type CheckovSummary struct {
|
||||
Failed int `json:"failed"`
|
||||
Passed int `json:"passed"`
|
||||
ResourceCount int `json:"resource_count"`
|
||||
// parsing errors?
|
||||
// skipped ?
|
||||
}
|
||||
|
||||
type CheckovResults struct {
|
||||
FailedChecks []CheckovCheck `json:"failed_checks"`
|
||||
}
|
||||
|
||||
type CheckovCheck struct {
|
||||
Id string `json:"check_id"`
|
||||
BcId string `json:"bc_check_id"`
|
||||
Name string `json:"check_name"`
|
||||
Resource string `json:"resource"`
|
||||
Guideline string `json:"guideline"`
|
||||
FileLineRange []int `json:"file_line_range"`
|
||||
}
|
||||
108
pkg/dashboard/scanners/trivy.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package scanners
|
||||
|
||||
import (
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Trivy struct {
|
||||
Data *objects.DataLayer
|
||||
}
|
||||
|
||||
func (c *Trivy) ManifestIsScannable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Trivy) SupportedResourceKinds() []string {
|
||||
// from https://github.com/aquasecurity/trivy-kubernetes/blob/main/pkg/k8s/k8s.go#L190
|
||||
return []string{
|
||||
"ReplicaSet",
|
||||
"ReplicationController",
|
||||
"StatefulSet",
|
||||
"Deployment",
|
||||
"CronJob",
|
||||
"DaemonSet",
|
||||
"Job",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Trivy) Name() string {
|
||||
return "Trivy"
|
||||
}
|
||||
|
||||
func (c *Trivy) Test() bool {
|
||||
utils.FailLogLevel = log.DebugLevel
|
||||
defer func() { utils.FailLogLevel = log.WarnLevel }()
|
||||
|
||||
res, err := utils.RunCommand([]string{"trivy", "--version"}, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
parts := strings.Split(res, "\n")
|
||||
log.Infof("Discovered Trivy: %s", strings.TrimSpace(parts[0]))
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Trivy) ScanManifests(_ string) (*subproc.ScanResults, error) {
|
||||
return nil, nil // Trivy is unable to scan manifests
|
||||
}
|
||||
|
||||
func (c *Trivy) scanResource(ns string, kind string, name string) (string, error) {
|
||||
cmd := []string{"trivy", "kubernetes", "--quiet", "--format", "table", "--report", "all", "--no-progress",
|
||||
"--context", c.Data.KubeContext, "--namespace", ns, kind + "/" + name}
|
||||
out, err := utils.RunCommand(cmd, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *Trivy) ScanResource(ns string, kind string, name string) (*subproc.ScanResults, error) {
|
||||
res := subproc.ScanResults{}
|
||||
resource, err := c.scanResource(ns, kind, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(resource, "\n") {
|
||||
if strings.HasPrefix(line, "Tests:") {
|
||||
parts := strings.FieldsFunc(line, func(r rune) bool {
|
||||
return r == ':' || r == ',' || r == ')'
|
||||
})
|
||||
|
||||
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[2])); err == nil {
|
||||
res.PassedCount += cnt
|
||||
} else {
|
||||
log.Warnf("Failed to parse Trivy output: %s", err)
|
||||
}
|
||||
|
||||
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[4])); err == nil {
|
||||
res.FailedCount += cnt
|
||||
} else {
|
||||
log.Warnf("Failed to parse Trivy output: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "Total:") {
|
||||
parts := strings.FieldsFunc(line, func(r rune) bool {
|
||||
return r == ':' || r == ',' || r == '('
|
||||
})
|
||||
|
||||
if cnt, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil {
|
||||
res.FailedCount += cnt
|
||||
} else {
|
||||
log.Warnf("Failed to parse Trivy output: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
res.OrigReport = resource
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
@@ -2,57 +2,221 @@ package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/registry"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/scanners"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func StartServer() (string, ControlChan) {
|
||||
data := DataLayer{}
|
||||
err := data.CheckConnectivity()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to check that Helm is operational, cannot continue. The error was: %s", err)
|
||||
os.Exit(1) // TODO: propagate error instead?
|
||||
}
|
||||
|
||||
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(ControlChan)
|
||||
api := NewRouter(abort, &data)
|
||||
done := startBackgroundServer(address, api, abort)
|
||||
|
||||
return "http://" + address, done
|
||||
type Server struct {
|
||||
Version string
|
||||
Namespaces []string
|
||||
Address string
|
||||
Debug bool
|
||||
NoTracking bool
|
||||
Devel bool
|
||||
LocalCharts []string
|
||||
}
|
||||
|
||||
func startBackgroundServer(addr string, routes *gin.Engine, abort ControlChan) ControlChan {
|
||||
done := make(ControlChan)
|
||||
server := &http.Server{Addr: addr, Handler: routes}
|
||||
func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (string, utils.ControlChan, error) {
|
||||
data, err := objects.NewDataLayer(s.Namespaces, s.Version, NewHelmConfig, s.Devel)
|
||||
if err != nil {
|
||||
return "", nil, errorx.Decorate(err, "Failed to create data layer")
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
panic(err) // TODO: in case of "port busy", check that it's another instance of us and just open browser
|
||||
data.LocalCharts = s.LocalCharts
|
||||
|
||||
isDevModeWithAnalytics := os.Getenv("HD_DEV_ANALYTICS") == "true"
|
||||
data.StatusInfo.Analytics = (!s.NoTracking && s.Version != "0.0.0") || isDevModeWithAnalytics
|
||||
|
||||
err = s.detectClusterMode(data)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
go checkUpgrade(data.StatusInfo)
|
||||
|
||||
discoverScanners(data)
|
||||
|
||||
go data.PeriodicTasks(ctx)
|
||||
|
||||
api := NewRouter(cancel, data, s.Debug)
|
||||
done := s.startBackgroundServer(api, ctx)
|
||||
|
||||
return "http://" + s.Address, done, nil
|
||||
}
|
||||
|
||||
func (s *Server) detectClusterMode(data *objects.DataLayer) error {
|
||||
data.StatusInfo.ClusterMode = os.Getenv("HD_CLUSTER_MODE") != ""
|
||||
if data.StatusInfo.ClusterMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctxs, err := data.ListContexts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(ctxs) == 0 {
|
||||
log.Infof("Got no kubectl config contexts, will attempt to detect if we're inside cluster...")
|
||||
app, err := data.AppForCtx("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
ns, err := app.K8s.GetNameSpaces()
|
||||
if err != nil { // no point in continuing without kubectl context and k8s connection
|
||||
return err
|
||||
}
|
||||
log.Debugf("Got %d namespaces listed", len(ns.Items))
|
||||
data.StatusInfo.ClusterMode = true
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) startBackgroundServer(routes *gin.Engine, ctx context.Context) utils.ControlChan {
|
||||
done := make(utils.ControlChan)
|
||||
server := &http.Server{
|
||||
Addr: s.Address,
|
||||
Handler: routes,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-abort
|
||||
<-ctx.Done()
|
||||
err := server.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
log.Warnf("Had problems shutting down the server: %s", err)
|
||||
}
|
||||
log.Infof("Web server has been shut down.")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.Warnf("Looks like port is busy for %s, checking if it's us...", s.Address)
|
||||
if s.itIsUs() {
|
||||
log.Infof("Yes, it's another instance of us. Just reuse it.")
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
done <- struct{}{}
|
||||
}()
|
||||
|
||||
return done
|
||||
}
|
||||
|
||||
func (s *Server) itIsUs() bool {
|
||||
url := fmt.Sprintf("http://%s/status", s.Address)
|
||||
var myClient = &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
r, err := myClient.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("It's not us on %s: %s", s.Address, err)
|
||||
return false
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
return strings.HasPrefix(r.Header.Get("X-Application-Name"), "Helm Dashboard")
|
||||
}
|
||||
|
||||
func discoverScanners(data *objects.DataLayer) {
|
||||
potential := []subproc.Scanner{
|
||||
&scanners.Checkov{Data: data},
|
||||
&scanners.Trivy{Data: data},
|
||||
}
|
||||
|
||||
data.Scanners = []subproc.Scanner{}
|
||||
for _, scanner := range potential {
|
||||
if scanner.Test() {
|
||||
data.Scanners = append(data.Scanners, scanner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkUpgrade(d *objects.StatusInfo) { // TODO: check it once an hour
|
||||
url := "https://api.github.com/repos/komodorio/helm-dashboard/releases/latest"
|
||||
type GHRelease struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var myClient = &http.Client{Timeout: 5 * time.Second}
|
||||
r, err := myClient.Get(url)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to check for new version: %s", err)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
target := new(GHRelease)
|
||||
err = json.NewDecoder(r.Body).Decode(target)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to decode new release version: %s", err)
|
||||
return
|
||||
}
|
||||
d.LatestVer = target.Name
|
||||
|
||||
v1, err := version.NewVersion(d.CurVer)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to parse CurVer: %s", err)
|
||||
v1 = &version.Version{}
|
||||
}
|
||||
|
||||
v2, err := version.NewVersion(d.LatestVer)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to parse RepoLatestVer: %s", err)
|
||||
} else {
|
||||
if v1.LessThan(v2) {
|
||||
log.Warnf("Newer Helm Dashboard version is available: %s", d.LatestVer)
|
||||
log.Warnf("Upgrade instructions: https://github.com/komodorio/helm-dashboard#installing")
|
||||
} else {
|
||||
log.Debugf("Got latest version from GH: %s", d.LatestVer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewHelmConfig(origSettings *cli.EnvSettings, ns string) (*action.Configuration, error) {
|
||||
// TODO: cache it into map
|
||||
// TODO: I feel there should be more elegant way to organize this code
|
||||
actionConfig := new(action.Configuration)
|
||||
|
||||
settings := cli.New()
|
||||
settings.KubeContext = origSettings.KubeContext
|
||||
settings.SetNamespace(ns) // important for RESTClientGetter to have correct namespace
|
||||
|
||||
registryClient, err := registry.NewClient(
|
||||
registry.ClientOptDebug(false),
|
||||
registry.ClientOptEnableCache(true),
|
||||
//registry.ClientOptWriter(out),
|
||||
registry.ClientOptCredentialsFile(settings.RegistryConfig),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to crete helm config object")
|
||||
}
|
||||
actionConfig.RegistryClient = registryClient
|
||||
|
||||
helmDriver := os.Getenv("HELM_DRIVER")
|
||||
if err := actionConfig.Init(
|
||||
settings.RESTClientGetter(),
|
||||
ns,
|
||||
helmDriver, log.Debugf); err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to init Helm action config")
|
||||
}
|
||||
|
||||
return actionConfig, nil
|
||||
}
|
||||
|
||||
435
pkg/dashboard/static/actions.js
Normal file
@@ -0,0 +1,435 @@
|
||||
$("#btnUpgradeCheck").click(function () {
|
||||
const self = $(this)
|
||||
self.find(".bi-repeat").hide()
|
||||
self.find(".spinner-border").show()
|
||||
const repoName = self.data("repo")
|
||||
$("#btnUpgrade span").text("Checking...")
|
||||
$("#btnUpgrade .icon").removeClass("bi-arrow-up bi-pencil").addClass("bi-hourglass-split")
|
||||
$.post("/api/helm/repositories/" + repoName).fail(function (xhr) {
|
||||
reportError("Failed to update chart repo", xhr)
|
||||
}).done(function () {
|
||||
self.find(".spinner-border").hide()
|
||||
self.find(".bi-repeat").show()
|
||||
|
||||
checkUpgradeable(self.data("chart"))
|
||||
$("#btnUpgradeCheck").prop("disabled", true)
|
||||
})
|
||||
})
|
||||
|
||||
function checkUpgradeable(name) {
|
||||
$.getJSON("/api/helm/repositories/latestver?name=" + name).fail(function (xhr) {
|
||||
reportError("Failed to find chart in repo", xhr)
|
||||
}).done(function (data) {
|
||||
let elm = {name: "", version: "0"}
|
||||
const btnUpgradeCheck = $("#btnUpgradeCheck");
|
||||
if (!data || !data.length) {
|
||||
btnUpgradeCheck.prop("disabled", true)
|
||||
btnUpgradeCheck.text("")
|
||||
$("#btnAddRepository").text("Add repository for it").data("suggestRepo", "")
|
||||
} else if (data[0].isSuggestedRepo) {
|
||||
btnUpgradeCheck.prop("disabled", true)
|
||||
btnUpgradeCheck.text("")
|
||||
$("#btnAddRepository").text("Add repository for it: "+data[0].repository).data("suggestRepo", data[0].repository).data("suggestRepoUrl", data[0].urls[0])
|
||||
} else {
|
||||
$("#btnAddRepository").text("")
|
||||
btnUpgradeCheck.text("Check for new version")
|
||||
elm = data[0]
|
||||
}
|
||||
|
||||
$("#btnUpgrade .icon").removeClass("bi-arrow-up bi-pencil").addClass("bi-hourglass-split")
|
||||
const verCur = $("#specRev").data("last-chart-ver");
|
||||
btnUpgradeCheck.data("repo", elm.repository)
|
||||
btnUpgradeCheck.data("chart", elm.name)
|
||||
|
||||
const canUpgrade = isNewerVersion(verCur, elm.version);
|
||||
btnUpgradeCheck.prop("disabled", false)
|
||||
if (canUpgrade) {
|
||||
$("#btnUpgrade span").text("Upgrade to " + elm.version)
|
||||
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-arrow-up")
|
||||
} else {
|
||||
$("#btnUpgrade span").text("Reconfigure")
|
||||
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-pencil")
|
||||
}
|
||||
|
||||
$("#btnUpgrade").off("click").click(function () {
|
||||
popUpUpgrade(elm, getHashParam("namespace"), getHashParam("chart"), verCur, $("#specRev").data("last-rev"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function popUpUpgrade(elm, ns, name, verCur, lastRev) {
|
||||
$("#upgradeModal .btn-confirm").prop("disabled", true)
|
||||
|
||||
$('#upgradeModal').data("initial", !verCur)
|
||||
$('#upgradeModal').data("newManifest", "")
|
||||
|
||||
$("#upgradeModalLabel .name").text(elm.name)
|
||||
|
||||
$("#upgradeModal .rel-cluster").text(getHashParam("context"))
|
||||
|
||||
if (verCur) {
|
||||
$("#upgradeModalLabel .type").text("Upgrade")
|
||||
$("#upgradeModal .ver-old").show().find("span").text(verCur)
|
||||
$("#upgradeModal .rel-name").prop("disabled", true).val(name)
|
||||
$("#upgradeModal .rel-ns").prop("disabled", true).val(ns)
|
||||
|
||||
$.get("/api/helm/releases/" + ns + "/" + name + "/manifests").fail(function (xhr) {
|
||||
reportError("Failed to get current manifest", xhr)
|
||||
}).done(function (text) {
|
||||
$('#upgradeModal').data("curManifest", text)
|
||||
})
|
||||
|
||||
} else {
|
||||
$("#upgradeModalLabel .type").text("Install")
|
||||
$("#upgradeModal .ver-old").hide()
|
||||
$("#upgradeModal .rel-name").prop("disabled", false).val(elm.name.split("/").pop())
|
||||
$("#upgradeModal .rel-ns").prop("disabled", false).val(ns)
|
||||
$('#upgradeModal').data("curManifest", "")
|
||||
}
|
||||
|
||||
if (elm.name) {
|
||||
$.getJSON("/api/helm/repositories/versions?name=" + elm.name).fail(function (xhr) {
|
||||
reportError("Failed to find chart in repo", xhr)
|
||||
}).done(function (vers) {
|
||||
vers.sort((b, a) => (a.version > b.version) - (a.version < b.version))
|
||||
|
||||
// fill versions
|
||||
$('#upgradeModal select').empty()
|
||||
for (let i = 0; i < vers.length; i++) {
|
||||
const opt = $("<option value='" + vers[i].version + "'></option>").data("ver", vers[i]);
|
||||
const label = vers[i].repository + " @ " + vers[i].version;
|
||||
if (vers[i].version === verCur) {
|
||||
opt.html(label + " ✓")
|
||||
} else {
|
||||
opt.html(label)
|
||||
}
|
||||
$('#upgradeModal select').append(opt)
|
||||
}
|
||||
|
||||
$('#upgradeModal select').val(elm.version).parent().show()
|
||||
upgrPopUpCommon(verCur, ns, lastRev, name)
|
||||
})
|
||||
} else { // chart without repo reconfigure
|
||||
$('#upgradeModal select').empty().parent().hide()
|
||||
upgrPopUpCommon(verCur, ns, lastRev, name)
|
||||
}
|
||||
}
|
||||
|
||||
function upgrPopUpCommon(verCur, ns, lastRev, name) {
|
||||
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
|
||||
myModal.show()
|
||||
|
||||
if (verCur) {
|
||||
// fill current values
|
||||
$.get("/api/helm/releases/" + ns + "/" + name + "/values?userDefined=true&revision=" + lastRev).fail(function (xhr) {
|
||||
reportError("Failed to get charts values info", xhr)
|
||||
}).done(function (data) {
|
||||
$("#upgradeModal textarea").val(data).data("dirty", false)
|
||||
$('#upgradeModal select').trigger("change")
|
||||
})
|
||||
} else {
|
||||
$("#upgradeModal textarea").val("").data("dirty", true)
|
||||
$('#upgradeModal select').trigger("change")
|
||||
}
|
||||
}
|
||||
|
||||
$("#upgradeModal .btn-confirm").click(function () {
|
||||
const btnConfirm = $("#upgradeModal .btn-confirm")
|
||||
btnConfirm.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
$('#upgradeModal form .preview-mode').val("false")
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: upgradeModalURL(),
|
||||
data: $("#upgradeModal form").serialize(),
|
||||
}).fail(function (xhr) {
|
||||
reportError("Failed to upgrade the chart", xhr)
|
||||
}).done(function (data) {
|
||||
if (data.version) {
|
||||
setHashParam("section", null)
|
||||
const ns = $("#upgradeModal .rel-ns").val();
|
||||
setHashParam("namespace", ns ? ns : "default") // TODO: relaets issue #51
|
||||
setHashParam("chart", $("#upgradeModal .rel-name").val())
|
||||
setHashParam("revision", data.version)
|
||||
window.location.reload()
|
||||
} else {
|
||||
reportError("Failed to get new revision number")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let reconfigTimeout = null;
|
||||
|
||||
function changeTimer() {
|
||||
const self = $(this);
|
||||
self.data("dirty", true)
|
||||
if (reconfigTimeout) {
|
||||
window.clearTimeout(reconfigTimeout)
|
||||
}
|
||||
reconfigTimeout = window.setTimeout(requestChangeDiff, 500)
|
||||
}
|
||||
|
||||
$("#upgradeModal textarea").keyup(changeTimer)
|
||||
$("#upgradeModal .rel-name").keyup(changeTimer)
|
||||
$("#upgradeModal .rel-ns").keyup(changeTimer)
|
||||
|
||||
$('#upgradeModal select').change(function () {
|
||||
const self = $(this)
|
||||
const ver = self.find("option:selected").data("ver");
|
||||
|
||||
let chart = ""
|
||||
if (ver) {
|
||||
chart = ver.repository + "/" + ver.name;
|
||||
// local chart case
|
||||
if (ver.urls && ver.urls.length && ver.urls[0].startsWith("file://")) {
|
||||
chart = ver.urls[0];
|
||||
}
|
||||
}
|
||||
|
||||
$('#upgradeModal').data("chart", chart)
|
||||
$('#upgradeModal form .chart-name').val(chart)
|
||||
|
||||
requestChangeDiff()
|
||||
|
||||
// fill reference values
|
||||
$("#upgradeModal .ref-vals").html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
|
||||
// TODO: if chart is empty, query different URL that will restore values without repo
|
||||
if (chart) {
|
||||
$.get("/api/helm/repositories/values?chart=" + chart + "&version=" + self.val()).fail(function (xhr) {
|
||||
reportError("Failed to get upgrade info", xhr)
|
||||
}).done(function (data) {
|
||||
data = hljs.highlight(data, {language: 'yaml'}).value
|
||||
$("#upgradeModal .ref-vals").html(data)
|
||||
})
|
||||
} else {
|
||||
$("#upgradeModal .ref-vals").html("No original values information found")
|
||||
}
|
||||
})
|
||||
|
||||
$('#upgradeModal .btn-scan').click(function () {
|
||||
const self = $(this)
|
||||
|
||||
self.prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
|
||||
const form = new FormData();
|
||||
form.append('manifest', $('#upgradeModal').data("newManifest"));
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/scanners/manifests",
|
||||
processData: false,
|
||||
contentType: false,
|
||||
data: form,
|
||||
}).fail(function (xhr) {
|
||||
reportError("Failed to scan the manifest", xhr)
|
||||
}).done(function (data) {
|
||||
self.prop("disabled", false).find(".spinner-border").hide()
|
||||
|
||||
const container = $("<div></div>")
|
||||
for (let name in data) {
|
||||
const res = data[name]
|
||||
|
||||
if (!res) {
|
||||
continue
|
||||
}
|
||||
|
||||
const pre = $("<pre></pre>").text(JSON.stringify(res.OrigReport, null, 2))
|
||||
|
||||
container.append("<h2>" + name + " Scan Results</h2>")
|
||||
container.append(pre)
|
||||
}
|
||||
|
||||
const tab = window.open('about:blank', '_blank');
|
||||
tab.document.write(container.prop('outerHTML')); // where 'html' is a variable containing your HTML
|
||||
tab.document.close(); // to finish loading the page
|
||||
})
|
||||
})
|
||||
|
||||
function requestChangeDiff() {
|
||||
const diffBody = $("#upgradeModalBody");
|
||||
diffBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Calculating diff...')
|
||||
$("#upgradeModal .btn-confirm").prop("disabled", true)
|
||||
|
||||
$('#upgradeModal form .preview-mode').val("true")
|
||||
let form = $("#upgradeModal form").serialize();
|
||||
if ($("#upgradeModal textarea").data("dirty")) {
|
||||
$("#upgradeModal .invalid-feedback").hide()
|
||||
|
||||
try {
|
||||
jsyaml.load($("#upgradeModal textarea").val())
|
||||
} catch (e) {
|
||||
$("#upgradeModal .invalid-feedback").text("YAML parse error: " + e.message).show()
|
||||
$("#upgradeModalBody").html("Invalid values YAML")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: upgradeModalURL(),
|
||||
data: form,
|
||||
}).fail(function (xhr) {
|
||||
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>")
|
||||
}).done(function (data) {
|
||||
$('#upgradeModal').data("newManifest", data.manifest)
|
||||
|
||||
const form = new FormData();
|
||||
form.append('a', $('#upgradeModal').data("curManifest"));
|
||||
form.append('b', data.manifest);
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/diff",
|
||||
processData: false,
|
||||
contentType: false,
|
||||
data: form,
|
||||
}).fail(function (xhr) {
|
||||
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>")
|
||||
}).done(function (data) {
|
||||
diffBody.empty();
|
||||
$("#upgradeModal .btn-confirm").prop("disabled", false)
|
||||
|
||||
const targetElement = document.getElementById('upgradeModalBody');
|
||||
const configuration = {
|
||||
inputFormat: 'diff', outputFormat: 'side-by-side',
|
||||
drawFileList: false, showFiles: false, highlight: true,
|
||||
};
|
||||
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
||||
diff2htmlUi.draw()
|
||||
if (!data) {
|
||||
diffBody.html("No changes will happen to the cluster")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function upgradeModalURL() {
|
||||
let ns = $("#upgradeModal .rel-ns").val();
|
||||
if (!ns) {
|
||||
ns = "[empty]"
|
||||
}
|
||||
|
||||
let qstr = "/api/helm/releases/" + ns;
|
||||
if (!$("#upgradeModal").data("initial")) {
|
||||
qstr += "/" + $("#upgradeModal .rel-name").val()
|
||||
}
|
||||
|
||||
return qstr
|
||||
}
|
||||
|
||||
const btnConfirm = $("#confirmModal .btn-confirm");
|
||||
$("#btnUninstall").click(function () {
|
||||
const chart = getHashParam('chart');
|
||||
const namespace = getHashParam('namespace');
|
||||
const revision = $("#specRev").data("last-rev")
|
||||
$("#confirmModalLabel").html("Uninstall <b class='text-danger'>" + chart + "</b> from namespace <b class='text-danger'>" + namespace + "</b>")
|
||||
$("#confirmModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
btnConfirm.prop("disabled", true).off('click').click(function () {
|
||||
btnConfirm.prop("disabled", true).append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
const url = "/api/helm/releases/" + namespace + "/" + chart;
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'DELETE',
|
||||
}).fail(function (xhr) {
|
||||
reportError("Failed to delete the chart", xhr)
|
||||
}).done(function () {
|
||||
window.location.href = "/"
|
||||
})
|
||||
})
|
||||
|
||||
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'));
|
||||
myModal.show()
|
||||
|
||||
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources"
|
||||
$.getJSON(url).fail(function (xhr) {
|
||||
reportError("Failed to get list of resources", xhr)
|
||||
}).done(function (data) {
|
||||
$("#confirmModalBody").empty().append("<p>Following resources will be deleted from the cluster:</p>");
|
||||
btnConfirm.prop("disabled", false)
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const res = data[i]
|
||||
$("#confirmModalBody").append("<p class='row'><i class='col-sm-3 text-end'>" + res.kind + "</i><b class='col-sm-9'>" + res.metadata.name + "</b></p>")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
$("#btnRollback").click(function () {
|
||||
const chart = getHashParam('chart');
|
||||
const namespace = getHashParam('namespace');
|
||||
const revisionNew = $("#btnRollback").data("rev")
|
||||
const revisionCur = $("#specRev").data("last-rev")
|
||||
$("#confirmModalLabel").html("Rollback <b class='text-danger'>" + chart + "</b> from revision " + revisionCur + " to " + revisionNew)
|
||||
$("#confirmModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
btnConfirm.prop("disabled", true).off('click').click(function () {
|
||||
btnConfirm.prop("disabled", true).append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
const url = "/api/helm/releases/" + namespace + "/" + chart + "/rollback";
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
data: {
|
||||
revision: revisionNew
|
||||
}
|
||||
}).fail(function (xhr) {
|
||||
reportError("Failed to rollback the chart", xhr)
|
||||
}).done(function () {
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
|
||||
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'), {});
|
||||
myModal.show()
|
||||
|
||||
let qstr = "revision=" + revisionNew + "&revisionDiff=" + revisionCur
|
||||
let url = "/api/helm/releases/" + namespace + "/" + chart + "/manifests"
|
||||
url += "?" + qstr
|
||||
$.get(url).fail(function (xhr) {
|
||||
reportError("Failed to get list of resources", xhr)
|
||||
}).done(function (data) {
|
||||
$("#confirmModalBody").empty();
|
||||
$("#confirmModal .btn-confirm").prop("disabled", false)
|
||||
|
||||
const targetElement = document.getElementById('confirmModalBody');
|
||||
const configuration = {
|
||||
inputFormat: 'diff', outputFormat: 'side-by-side',
|
||||
drawFileList: false, showFiles: false, highlight: true,
|
||||
};
|
||||
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
||||
diff2htmlUi.draw()
|
||||
if (data) {
|
||||
$("#confirmModalBody").prepend("<p>Following changes will happen to cluster:</p>")
|
||||
} else {
|
||||
$("#confirmModalBody").html("<p>No changes will happen to cluster</p>")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
$("#btnAddRepository").click(function () {
|
||||
const self=$(this)
|
||||
setHashParam("section", "repository")
|
||||
if (self.data("suggestRepo")) {
|
||||
setHashParam("suggestRepo", self.data("suggestRepo"))
|
||||
setHashParam("suggestRepoUrl", self.data("suggestRepoUrl"))
|
||||
}
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
$("#btnTest").click(function () {
|
||||
const myModal = new bootstrap.Modal(document.getElementById('testModal'), {});
|
||||
$("#testModal .test-result").empty().prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Waiting for completion...')
|
||||
myModal.show()
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: "/api/helm/releases/" + getHashParam("namespace") + "/" + getHashParam("chart") + "/test"
|
||||
}).fail(function (xhr) {
|
||||
reportError("Failed to execute test for chart", xhr)
|
||||
myModal.hide()
|
||||
}).done(function (data) {
|
||||
var output;
|
||||
if (data.length == 0 || data == null || data == "") {
|
||||
output = "<div>Tests executed successfully<br><br><pre>Empty response from API<pre></div>"
|
||||
} else {
|
||||
output = data.replaceAll("\n", "<br>")
|
||||
}
|
||||
$("#testModal .test-result").empty().html(output)
|
||||
myModal.show()
|
||||
})
|
||||
})
|
||||
69
pkg/dashboard/static/analytics.js
Normal file
@@ -0,0 +1,69 @@
|
||||
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, status.ClusterMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.open('GET', '/status', true);
|
||||
xhr.send(null);
|
||||
|
||||
|
||||
function enableDD(version) {
|
||||
(function (h, o, u, n, d) {
|
||||
h = h[d] = h[d] || {
|
||||
q: [], onReady: function (c) {
|
||||
h.q.push(c)
|
||||
}
|
||||
}
|
||||
d = o.createElement(u);
|
||||
d.async = true;
|
||||
d.src = n
|
||||
n = o.getElementsByTagName(u)[0];
|
||||
n.parentNode.insertBefore(d, n)
|
||||
})(window, document, 'script', 'https://www.datadoghq-browser-agent.com/datadog-rum-v4.js', 'DD_RUM')
|
||||
DD_RUM.onReady(function () {
|
||||
DD_RUM.init({
|
||||
clientToken: 'pub16d64cd1c00cf073ce85af914333bf72',
|
||||
applicationId: 'e75439e5-e1b3-46ba-a9e9-a2e58579a2e2',
|
||||
site: 'datadoghq.com',
|
||||
service: 'helm-dashboard',
|
||||
version: version,
|
||||
trackInteractions: true,
|
||||
trackResources: true,
|
||||
trackLongTasks: true,
|
||||
defaultPrivacyLevel: 'mask',
|
||||
sessionReplaySampleRate: 0
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function enableHeap(version, inCluster) {
|
||||
window.heap = window.heap || [], heap.load = function (e, t) {
|
||||
window.heap.appid = e, window.heap.config = t = t || {};
|
||||
let r = document.createElement("script");
|
||||
r.type = "text/javascript", r.async = !0, r.src = "https://cdn.heapanalytics.com/js/heap-" + e + ".js";
|
||||
let a = document.getElementsByTagName("script")[0];
|
||||
a.parentNode.insertBefore(r, a);
|
||||
for (let n = function (e) {
|
||||
return function () {
|
||||
heap.push([e].concat(Array.prototype.slice.call(arguments, 0)))
|
||||
}
|
||||
}, p = ["addEventProperties", "addUserProperties", "clearEventProperties", "identify", "resetIdentity", "removeEventProperty", "setEventProperties", "track", "unsetEventProperty"], o = 0; o < p.length; o++) heap[p[o]] = n(p[o])
|
||||
};
|
||||
heap.load("4249623943");
|
||||
window.heap.addEventProperties({
|
||||
'version': version,
|
||||
'installationMode': inCluster?"cluster":"local"
|
||||
});
|
||||
}
|
||||
|
||||
function sendStats(name, prop){
|
||||
if (window.heap) {
|
||||
window.heap.track(name, prop);
|
||||
}
|
||||
}
|
||||
70
pkg/dashboard/static/api-docs.html
Normal file
@@ -0,0 +1,70 @@
|
||||
<html lang="">
|
||||
<head>
|
||||
<link rel="icon" href="../static/logo.png"/>
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.css"/>
|
||||
<title>
|
||||
Helm Dashboard API
|
||||
</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<div id="swagger-ui">
|
||||
<div class="center_progress">
|
||||
<div class="lds-dual-ring"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-bundle.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.js"></script>
|
||||
|
||||
<script>
|
||||
let swaggerUrl = "openapi.json";
|
||||
|
||||
function reqOas() {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('GET', swaggerUrl, true);
|
||||
request.setRequestHeader('Accept', 'application/json');
|
||||
|
||||
request.onload = function () {
|
||||
if (request.status >= 200 && request.status < 400) {
|
||||
// Success!
|
||||
const data = JSON.parse(request.responseText);
|
||||
display(data);
|
||||
} else {
|
||||
alert("Failed to get "+ swaggerUrl)
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = function () {
|
||||
alert("Failed to get "+ swaggerUrl)
|
||||
};
|
||||
|
||||
request.send();
|
||||
}
|
||||
|
||||
function display(data) {
|
||||
const parent = document.querySelectorAll('#swagger-ui')[0];
|
||||
parent.innerHTML = '';
|
||||
let el = document.createElement('div');
|
||||
el.id = "swDocs";
|
||||
parent.appendChild(el);
|
||||
|
||||
SwaggerUIBundle({
|
||||
spec: data,
|
||||
dom_id: '#' + el.id,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
reqOas();
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
300
pkg/dashboard/static/details-view.js
Normal file
@@ -0,0 +1,300 @@
|
||||
function revisionClicked(namespace, name, self) {
|
||||
let active = "active border-primary border-1 bg-white";
|
||||
let inactive = "border-secondary bg-secondary";
|
||||
revRow.find(".active").removeClass(active).addClass(inactive)
|
||||
self.removeClass(inactive).addClass(active)
|
||||
const elm = self.data("elm")
|
||||
setHashParam("revision", elm.revision)
|
||||
$("#sectionDetails span.rev").text("#" + elm.revision)
|
||||
statusStyle(elm.status, $("#none"), $("#sectionDetails .rev-details .rev-status"))
|
||||
|
||||
const rdate = luxon.DateTime.fromISO(elm.updated);
|
||||
$("#sectionDetails .rev-date").text(rdate.toJSDate().toLocaleString())
|
||||
$("#sectionDetails .rev-tags .rev-chart").text(elm.chart)
|
||||
$("#sectionDetails .rev-tags .rev-app").text(elm.app_version)
|
||||
$("#sectionDetails .rev-tags .rev-ns").text(getHashParam("namespace"))
|
||||
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") {
|
||||
$("#revDescr").addClass("text-danger")
|
||||
}
|
||||
|
||||
const rev = $("#specRev").data("last-rev") == elm.revision ? elm.revision - 1 : elm.revision
|
||||
if (!rev || getHashParam("revision") === $("#specRev").data("first-rev")) {
|
||||
$("#btnRollback").hide()
|
||||
} else {
|
||||
$("#btnRollback").show().data("rev", rev).find("span").text("Rollback to #" + rev)
|
||||
}
|
||||
|
||||
const tab = getHashParam("tab")
|
||||
if (!tab) {
|
||||
$("#nav-tab [data-tab=resources]").click()
|
||||
} else {
|
||||
$("#nav-tab [data-tab=" + tab + "]").click()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function loadContentWrapper() {
|
||||
let revDiff = 0
|
||||
const revision = parseInt(getHashParam("revision"));
|
||||
if (revision === $("#specRev").data("first-rev")) {
|
||||
revDiff = 0
|
||||
} else if (getHashParam("mode") === "diff-prev") {
|
||||
revDiff = revision - 1
|
||||
} else if (getHashParam("mode") === "diff-rev") {
|
||||
revDiff = $("#specRev").val()
|
||||
}
|
||||
|
||||
const flag = $("#userDefinedVals").prop("checked");
|
||||
loadContent(getHashParam("tab"), getHashParam("namespace"), getHashParam("chart"), revision, revDiff, flag)
|
||||
}
|
||||
|
||||
function loadContent(mode, namespace, name, revision, revDiff, userDefined) {
|
||||
let qstr = "revision=" + revision
|
||||
if (revDiff) {
|
||||
qstr += "&revisionDiff=" + revDiff
|
||||
}
|
||||
|
||||
if (userDefined) {
|
||||
qstr += "&userDefined=" + userDefined
|
||||
}
|
||||
|
||||
let url = "/api/helm/releases/" + namespace + "/" + name + "/" + mode
|
||||
url += "?" + qstr
|
||||
const diffDisplay = $("#manifestText");
|
||||
diffDisplay.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
$.get(url).fail(function (xhr) {
|
||||
reportError("Failed to get diff of " + mode, xhr)
|
||||
}).done(function (data) {
|
||||
diffDisplay.empty();
|
||||
if (data === "") {
|
||||
diffDisplay.text("No differences to display")
|
||||
} else {
|
||||
if (revDiff) {
|
||||
const targetElement = document.getElementById('manifestText');
|
||||
const configuration = {
|
||||
inputFormat: 'diff', outputFormat: 'side-by-side',
|
||||
|
||||
drawFileList: false, showFiles: false, highlight: true, //matching: 'lines',
|
||||
};
|
||||
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
||||
diff2htmlUi.draw()
|
||||
} else {
|
||||
data = hljs.highlight(data, {language: 'yaml'}).value
|
||||
const code = $("#manifestText").empty().append("<pre class='bg-white rounded p-3'></pre>").find("pre");
|
||||
code.html(data)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$('#specRev').keyup(function (event) {
|
||||
let keycode = (event.keyCode ? event.keyCode : event.which);
|
||||
if (keycode == '13') {
|
||||
$("#diffModeRev").click()
|
||||
}
|
||||
});
|
||||
|
||||
$("form").submit(function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$("#userDefinedVals").change(function () {
|
||||
const self = $(this)
|
||||
const flag = $("#userDefinedVals").prop("checked");
|
||||
setHashParam("udv", flag)
|
||||
loadContentWrapper()
|
||||
})
|
||||
|
||||
$("#modePanel [data-mode]").click(function () {
|
||||
const self = $(this)
|
||||
const mode = self.data("mode")
|
||||
setHashParam("mode", mode)
|
||||
loadContentWrapper()
|
||||
})
|
||||
|
||||
$("#nav-tab [data-tab]").click(function () {
|
||||
const self = $(this)
|
||||
setHashParam("tab", self.data("tab"))
|
||||
|
||||
if (self.data("tab") === "values") {
|
||||
$("#userDefinedVals").parent().show()
|
||||
} else {
|
||||
$("#userDefinedVals").parent().hide()
|
||||
}
|
||||
|
||||
const flag = getHashParam("udv") === "true";
|
||||
$("#userDefinedVals").prop("checked", flag)
|
||||
|
||||
if (self.data("tab") === "resources") {
|
||||
showResources(getHashParam("namespace"), getHashParam("chart"), getHashParam("revision"))
|
||||
} else {
|
||||
const mode = getHashParam("mode")
|
||||
if (!mode) {
|
||||
$("#modePanel [data-mode=view]").trigger('click')
|
||||
} else {
|
||||
$("#modePanel [data-mode=" + mode + "]").trigger('click')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
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 url = "/api/helm/releases/" + namespace + "/" + chart + "/resources"
|
||||
$.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-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>
|
||||
`)
|
||||
|
||||
resBlock.find(".res-kind").text(res.kind)
|
||||
resBlock.find(".res-name").text(res.metadata.name)
|
||||
|
||||
resBody.append(resBlock)
|
||||
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
|
||||
$.getJSON("/api/k8s/" + res.kind.toLowerCase() + "/get?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
|
||||
//reportError("Failed to get list of resources")
|
||||
}).done(function (data) {
|
||||
const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase);
|
||||
if (["Available", "Active", "Established", "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")
|
||||
} else if (["Progressing"].includes(data.status.phase)) {
|
||||
badge.addClass("bg-warning")
|
||||
} else {
|
||||
badge.addClass("bg-danger")
|
||||
}
|
||||
const statusBlock = resBlock.find(".res-status");
|
||||
statusBlock.empty().append(badge).attr("title", data.status.phase)
|
||||
const statusMessage = getStatusMessage(data.status)
|
||||
resBlock.find(".res-statusmsg").html("<span class='text-muted small me-2'>" + (statusMessage ? statusMessage : '') + "</span>")
|
||||
|
||||
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
|
||||
resBlock.find(".res-actions")
|
||||
|
||||
const btn = $("<button class=\"btn btn-sm btn-white border-secondary\">Describe</button>");
|
||||
resBlock.find(".res-actions").append(btn)
|
||||
btn.click(function () {
|
||||
showDescribe(ns, res.kind, res.metadata.name, badge.clone())
|
||||
})
|
||||
|
||||
if (scannableResKinds.has(res.kind)) {
|
||||
const btn2 = $("<button class='btn btn-sm btn-white border-secondary ms-2'>Scan</button>");
|
||||
resBlock.find(".res-actions").append(btn2)
|
||||
btn2.click(function () {
|
||||
scanResource(ns, res.kind, res.metadata.name, badge.clone())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (badge.hasClass("bg-danger")) {
|
||||
resBlock.find(".res-statusmsg").append("<a href='" + KomodorCTALink + "' class='btn btn-primary btn-sm fw-normal fs-80' target='_blank'>Troubleshoot in Komodor <i class='bi-box-arrow-up-right'></i></a>")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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"))
|
||||
$("#describeModalBody").empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
|
||||
const myModal = new bootstrap.Offcanvas(document.getElementById('describeModal'));
|
||||
myModal.show()
|
||||
$.get("/api/k8s/" + kind.toLowerCase() + "/describe?name=" + name + "&namespace=" + ns).fail(function (xhr) {
|
||||
reportError("Failed to describe resource", xhr)
|
||||
}).done(function (data) {
|
||||
data = hljs.highlight(data, {language: 'yaml'}).value
|
||||
$("#describeModalBody").empty().append("<pre class='bg-white rounded p-3'></pre>").find("pre").html(data)
|
||||
})
|
||||
}
|
||||
|
||||
function scanResource(ns, kind, name, badge) {
|
||||
$("#describeModal .offcanvas-header p").text(kind)
|
||||
$("#describeModalLabel").text(name).append(badge.addClass("ms-3 small fw-normal"))
|
||||
const body = $("#describeModalBody");
|
||||
body.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Scanning...')
|
||||
|
||||
const myModal = new bootstrap.Offcanvas(document.getElementById('describeModal'));
|
||||
myModal.show()
|
||||
$.get("/api/scanners/resource/" + kind.toLowerCase() + "?name=" + name + "&namespace=" + ns).fail(function (xhr) {
|
||||
reportError("Failed to scan resource", xhr)
|
||||
}).done(function (data) {
|
||||
body.empty()
|
||||
if ($.isEmptyObject(data)) {
|
||||
body.append("No information from scanners. Make sure you have installed some and scanned object is supported.")
|
||||
}
|
||||
|
||||
const tabs = $('<ul class="nav nav-tabs mt-3" role="tablist"></ul>')
|
||||
const content = $('<div class="tab-content"></div>')
|
||||
|
||||
for (let name in data) {
|
||||
const res = data[name]
|
||||
|
||||
if (!res.OrigReport && !res.PassedCount) continue
|
||||
|
||||
const hdr = $(`<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="` + name + `-tab" data-bs-toggle="tab" data-bs-target="#` + name + `-tab-pane" type="button" role="tab">` + name + `</button>
|
||||
</li>`)
|
||||
|
||||
if (res.FailedCount) {
|
||||
hdr.find('button').append("<span class='badge bg-danger ms-2'>" + res.FailedCount + " failed</span>")
|
||||
}
|
||||
|
||||
if (res.PassedCount) {
|
||||
hdr.find('button').append("<span class='badge bg-info ms-2'>" + res.PassedCount + " passed</span>")
|
||||
}
|
||||
|
||||
const hl = hljs.highlight(res.OrigReport, {language: 'yaml'}).value
|
||||
const pre = $("<pre class='bg-white rounded p-3' style='font-size: inherit; overflow: unset'></pre>").html(hl)
|
||||
const div = $('<div class="tab-pane fade" id="' + name + '-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0"></div>').append(pre)
|
||||
|
||||
tabs.append(hdr)
|
||||
content.append(div)
|
||||
}
|
||||
|
||||
body.append(tabs)
|
||||
body.append(content)
|
||||
tabs.find('li').first().find('button').click()
|
||||
})
|
||||
}
|
||||
65
pkg/dashboard/static/helm-gray-50.svg
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg886"
|
||||
sodipodi:docname="helm-gray-50.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs890" />
|
||||
<sodipodi:namedview
|
||||
id="namedview888"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="64.928571"
|
||||
inkscape:cx="13.992299"
|
||||
inkscape:cy="14"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2059"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg886" />
|
||||
<path
|
||||
d="M7.64558 6.78334C7.61355 6.75296 7.57868 6.72027 7.54422 6.68716C6.83768 6.00837 6.29089 5.22352 5.9606 4.29585C5.86816 4.03621 5.79837 3.7714 5.81075 3.49176C5.81193 3.46522 5.81185 3.4386 5.81368 3.41212C5.8386 3.05114 6.08019 2.86874 6.43295 2.95422C6.54422 2.98304 6.65188 3.0243 6.75391 3.07723C7.13976 3.27073 7.45426 3.55679 7.74346 3.87051C8.25713 4.41715 8.66907 5.05112 8.95989 5.74257C8.96624 5.75904 8.97352 5.77514 8.9817 5.79078C8.98569 5.79798 8.99415 5.8027 9.01302 5.81984C10.3898 4.99388 11.9471 4.51602 13.5501 4.42764C13.5403 4.37858 13.5344 4.34108 13.5251 4.30444C13.3615 3.62754 13.3113 2.9282 13.3765 2.23488C13.4052 1.81941 13.4889 1.40957 13.6254 1.01611C13.6914 0.808874 13.7954 0.615724 13.9321 0.446513C13.9837 0.386073 14.0433 0.33292 14.1092 0.288509C14.1749 0.242074 14.2532 0.216879 14.3337 0.216318C14.4141 0.215757 14.4927 0.239858 14.5591 0.285373C14.6992 0.380274 14.8119 0.510259 14.8861 0.662357C15.0243 0.920312 15.1236 1.19726 15.1808 1.48424C15.3112 2.09109 15.3513 2.71388 15.2997 3.33244C15.2741 3.70997 15.2086 4.08373 15.1042 4.44745C15.503 4.52092 15.8999 4.57785 16.2884 4.67017C16.6761 4.76031 17.0583 4.87256 17.4331 5.00635C17.811 5.14505 18.1807 5.30527 18.5402 5.48623C18.8956 5.66344 19.2338 5.87491 19.5884 6.07638C19.5999 6.05213 19.6167 6.02319 19.628 5.99226C19.9973 4.97054 20.6335 4.06633 21.4704 3.37357C21.6658 3.20585 21.8901 3.07522 22.1324 2.98812C22.1992 2.96486 22.2682 2.94899 22.3384 2.9408C22.6892 2.90063 22.8365 3.12136 22.8624 3.38498C22.8818 3.57931 22.8672 3.77555 22.8191 3.96484C22.6909 4.45379 22.4883 4.92009 22.2182 5.34735C21.8379 5.96177 21.3866 6.51522 20.813 6.96185C20.796 6.97503 20.7812 6.99087 20.7525 7.01731C21.3135 7.53479 21.8132 8.11507 22.2417 8.74672C22.2106 8.75474 22.179 8.76031 22.1471 8.76339C21.5538 8.76423 20.9604 8.76237 20.3671 8.76597C20.3321 8.76513 20.2979 8.75614 20.267 8.73971C20.2362 8.72327 20.2096 8.69986 20.1894 8.67133C18.8949 7.25696 17.1496 6.33565 15.2515 6.06462C14.6902 5.98295 14.1218 5.961 13.5558 5.99914C11.8683 6.10218 10.2539 6.72438 8.93373 7.78053C8.59283 8.04942 8.27485 8.34616 7.98306 8.66767C7.95717 8.6998 7.92412 8.72543 7.88656 8.74252C7.84899 8.75961 7.80796 8.76768 7.76672 8.76609C7.19997 8.76215 6.63318 8.76413 6.0664 8.76413H5.94588C5.98049 8.62928 6.32893 8.15161 6.72336 7.72518C7.01749 7.40716 7.32909 7.10529 7.64558 6.78334Z"
|
||||
fill="#3B3D45"
|
||||
id="path874"
|
||||
style="opacity:1;fill:#3b3d45;fill-opacity:0.5" />
|
||||
<path
|
||||
d="M22.0936 19.4833C21.6995 20.035 21.2496 20.5447 20.7511 21.0044C20.7909 21.0375 20.8231 21.0644 20.8554 21.0913C21.7206 21.799 22.3734 22.7321 22.7417 23.7874C22.8397 24.0448 22.8817 24.3201 22.865 24.595C22.8598 24.6654 22.8458 24.7348 22.8231 24.8017C22.7946 24.8964 22.7324 24.9774 22.6482 25.0292C22.564 25.0811 22.4636 25.1002 22.3663 25.083C22.2348 25.0659 22.107 25.028 21.9875 24.9706C21.8051 24.8806 21.6327 24.7715 21.4734 24.645C20.634 23.9554 19.9967 23.0516 19.629 22.0294C19.6185 22.0007 19.607 21.9723 19.5885 21.9243C19.1415 22.2198 18.6734 22.482 18.1878 22.7087C17.7046 22.929 17.205 23.1111 16.6934 23.2533C16.1735 23.3944 15.6436 23.4952 15.1083 23.555C15.1177 23.6021 15.1231 23.6396 15.1328 23.6759C15.3026 24.342 15.3588 25.032 15.2992 25.7167C15.2768 26.146 15.1934 26.5698 15.0515 26.9755C14.9839 27.1434 14.906 27.3069 14.8183 27.4652C14.7827 27.5266 14.7386 27.5826 14.6873 27.6315C14.4644 27.8617 14.1983 27.8636 13.9811 27.6274C13.8952 27.5322 13.8217 27.4265 13.7623 27.3128C13.59 26.9894 13.5013 26.6377 13.438 26.2791C13.3565 25.7895 13.331 25.2923 13.3619 24.797C13.3787 24.4343 13.4328 24.0743 13.5234 23.7226C13.5312 23.6928 13.5384 23.6627 13.5442 23.6325C13.5457 23.6248 13.5406 23.6158 13.5346 23.5911C11.9318 23.5005 10.3754 23.0201 9.0004 22.1915C8.97745 22.2424 8.95773 22.2853 8.93871 22.3284C8.55337 23.2275 7.96033 24.0225 7.20825 24.648C7.00915 24.8181 6.78047 24.9502 6.53361 25.0377C6.41792 25.0841 6.29167 25.0977 6.16876 25.0769C6.10089 25.0647 6.03733 25.0352 5.9843 24.9911C5.93126 24.9471 5.89056 24.89 5.86618 24.8255C5.78687 24.6338 5.80092 24.4343 5.82785 24.2366C5.87203 23.9609 5.95317 23.6925 6.06908 23.4385C6.41254 22.6386 6.91804 21.9186 7.55371 21.3238C7.57941 21.2995 7.60578 21.2758 7.63103 21.251C7.63906 21.2395 7.64591 21.2272 7.65149 21.2143C7.05256 20.6949 6.51739 20.1062 6.05723 19.4606C6.11237 19.4561 6.14925 19.4505 6.18615 19.4505C6.77495 19.4499 7.36377 19.452 7.95254 19.448C7.99171 19.4469 8.03065 19.4544 8.06652 19.4702C8.1024 19.4859 8.13431 19.5095 8.15994 19.5391C8.80004 20.1976 9.54586 20.7444 10.3665 21.1567C11.2347 21.6034 12.1787 21.884 13.1499 21.984C15.7886 22.2405 18.0585 21.4405 19.9595 19.5841C20.002 19.5382 20.0541 19.5021 20.1121 19.4785C20.1701 19.4548 20.2325 19.4442 20.2951 19.4473C20.844 19.4541 21.393 19.4501 21.9419 19.4501H22.0838L22.0936 19.4833Z"
|
||||
fill="#3B3D45"
|
||||
id="path876"
|
||||
style="opacity:1;fill:#3b3d45;fill-opacity:0.5" />
|
||||
<path
|
||||
d="M19.6412 11.0745C19.7972 11.0745 19.9475 11.0851 20.0956 11.0717C20.2633 11.0565 20.3834 11.1165 20.5057 11.2292C21.212 11.88 21.9257 12.5229 22.637 13.1683C22.6728 13.2008 22.7093 13.2324 22.7552 13.273C22.798 13.2362 22.8381 13.2034 22.8764 13.1686C23.6096 12.5012 24.3422 11.8331 25.0743 11.1645C25.1047 11.1332 25.1414 11.1088 25.182 11.0929C25.2226 11.077 25.2662 11.07 25.3097 11.0724C25.49 11.0797 25.6707 11.0745 25.8608 11.0745V16.9751C25.7643 17.0033 24.4678 17.0089 24.313 16.9785V13.9903L24.283 13.976C23.7784 14.4362 23.2738 14.8964 22.7577 15.3671C22.241 14.9017 21.7305 14.4418 21.22 13.9819L21.1906 13.9927C21.1893 14.2421 21.1902 14.4915 21.19 14.7409C21.1899 14.9888 21.1899 15.2367 21.19 15.4846V16.9894H19.654C19.6253 16.8901 19.6119 11.4083 19.6412 11.0745Z"
|
||||
fill="#3B3D45"
|
||||
id="path878"
|
||||
style="opacity:1;fill:#3b3d45;fill-opacity:0.5" />
|
||||
<path
|
||||
d="M5.46747 11.0815H6.99421C7.02504 11.1797 7.03107 16.8479 6.99951 16.9909H5.47141C5.46299 16.6155 5.46875 16.2414 5.4677 15.8675C5.46665 15.4966 5.46747 15.1258 5.46747 14.7453H3.57536V16.9708C3.46003 17.0052 2.15667 17.0085 2.0271 16.9777V11.0822H3.56924V13.1648C3.67944 13.1966 5.30094 13.2025 5.46607 13.172C5.46653 13.0053 5.46719 12.8346 5.46742 12.6638C5.46765 12.4867 5.46767 12.3096 5.46747 12.1325C5.46747 11.9599 5.46747 11.7872 5.46747 11.6145C5.46747 11.4422 5.46747 11.2698 5.46747 11.0815Z"
|
||||
fill="#3B3D45"
|
||||
id="path880"
|
||||
style="opacity:1;fill:#3b3d45;fill-opacity:0.5" />
|
||||
<path
|
||||
d="M8.82422 16.9889V11.0991C8.91477 11.0695 12.2707 11.0579 12.4901 11.0877V12.3429C12.4409 12.3464 12.3901 12.3531 12.3393 12.3532C11.7417 12.354 11.144 12.3541 10.5463 12.3537H10.3801V13.33H12.2476V14.6287H10.3968C10.3659 14.7399 10.3574 15.5145 10.3825 15.7289C10.4298 15.7321 10.4805 15.7384 10.5312 15.7384C11.1289 15.7391 11.7265 15.7393 12.3242 15.7389H12.4905V16.9889H8.82422Z"
|
||||
fill="#3B3D45"
|
||||
id="path882"
|
||||
style="opacity:1;fill:#3b3d45;fill-opacity:0.5" />
|
||||
<path
|
||||
d="M14.2399 16.991C14.2118 16.833 14.2175 11.1893 14.2453 11.082H15.7664V15.4368C15.832 15.4403 15.8835 15.4452 15.935 15.4452C16.5371 15.4458 17.1392 15.4459 17.7413 15.4456C17.7932 15.4456 17.845 15.4456 17.9042 15.4456V16.991L14.2399 16.991Z"
|
||||
fill="#3B3D45"
|
||||
id="path884"
|
||||
style="opacity:1;fill:#3b3d45;fill-opacity:0.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
8
pkg/dashboard/static/helm-gray.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.64558 6.78334C7.61355 6.75296 7.57868 6.72027 7.54422 6.68716C6.83768 6.00837 6.29089 5.22352 5.9606 4.29585C5.86816 4.03621 5.79837 3.7714 5.81075 3.49176C5.81193 3.46522 5.81185 3.4386 5.81368 3.41212C5.8386 3.05114 6.08019 2.86874 6.43295 2.95422C6.54422 2.98304 6.65188 3.0243 6.75391 3.07723C7.13976 3.27073 7.45426 3.55679 7.74346 3.87051C8.25713 4.41715 8.66907 5.05112 8.95989 5.74257C8.96624 5.75904 8.97352 5.77514 8.9817 5.79078C8.98569 5.79798 8.99415 5.8027 9.01302 5.81984C10.3898 4.99388 11.9471 4.51602 13.5501 4.42764C13.5403 4.37858 13.5344 4.34108 13.5251 4.30444C13.3615 3.62754 13.3113 2.9282 13.3765 2.23488C13.4052 1.81941 13.4889 1.40957 13.6254 1.01611C13.6914 0.808874 13.7954 0.615724 13.9321 0.446513C13.9837 0.386073 14.0433 0.33292 14.1092 0.288509C14.1749 0.242074 14.2532 0.216879 14.3337 0.216318C14.4141 0.215757 14.4927 0.239858 14.5591 0.285373C14.6992 0.380274 14.8119 0.510259 14.8861 0.662357C15.0243 0.920312 15.1236 1.19726 15.1808 1.48424C15.3112 2.09109 15.3513 2.71388 15.2997 3.33244C15.2741 3.70997 15.2086 4.08373 15.1042 4.44745C15.503 4.52092 15.8999 4.57785 16.2884 4.67017C16.6761 4.76031 17.0583 4.87256 17.4331 5.00635C17.811 5.14505 18.1807 5.30527 18.5402 5.48623C18.8956 5.66344 19.2338 5.87491 19.5884 6.07638C19.5999 6.05213 19.6167 6.02319 19.628 5.99226C19.9973 4.97054 20.6335 4.06633 21.4704 3.37357C21.6658 3.20585 21.8901 3.07522 22.1324 2.98812C22.1992 2.96486 22.2682 2.94899 22.3384 2.9408C22.6892 2.90063 22.8365 3.12136 22.8624 3.38498C22.8818 3.57931 22.8672 3.77555 22.8191 3.96484C22.6909 4.45379 22.4883 4.92009 22.2182 5.34735C21.8379 5.96177 21.3866 6.51522 20.813 6.96185C20.796 6.97503 20.7812 6.99087 20.7525 7.01731C21.3135 7.53479 21.8132 8.11507 22.2417 8.74672C22.2106 8.75474 22.179 8.76031 22.1471 8.76339C21.5538 8.76423 20.9604 8.76237 20.3671 8.76597C20.3321 8.76513 20.2979 8.75614 20.267 8.73971C20.2362 8.72327 20.2096 8.69986 20.1894 8.67133C18.8949 7.25696 17.1496 6.33565 15.2515 6.06462C14.6902 5.98295 14.1218 5.961 13.5558 5.99914C11.8683 6.10218 10.2539 6.72438 8.93373 7.78053C8.59283 8.04942 8.27485 8.34616 7.98306 8.66767C7.95717 8.6998 7.92412 8.72543 7.88656 8.74252C7.84899 8.75961 7.80796 8.76768 7.76672 8.76609C7.19997 8.76215 6.63318 8.76413 6.0664 8.76413H5.94588C5.98049 8.62928 6.32893 8.15161 6.72336 7.72518C7.01749 7.40716 7.32909 7.10529 7.64558 6.78334Z" fill="#3B3D45"/>
|
||||
<path d="M22.0936 19.4833C21.6995 20.035 21.2496 20.5447 20.7511 21.0044C20.7909 21.0375 20.8231 21.0644 20.8554 21.0913C21.7206 21.799 22.3734 22.7321 22.7417 23.7874C22.8397 24.0448 22.8817 24.3201 22.865 24.595C22.8598 24.6654 22.8458 24.7348 22.8231 24.8017C22.7946 24.8964 22.7324 24.9774 22.6482 25.0292C22.564 25.0811 22.4636 25.1002 22.3663 25.083C22.2348 25.0659 22.107 25.028 21.9875 24.9706C21.8051 24.8806 21.6327 24.7715 21.4734 24.645C20.634 23.9554 19.9967 23.0516 19.629 22.0294C19.6185 22.0007 19.607 21.9723 19.5885 21.9243C19.1415 22.2198 18.6734 22.482 18.1878 22.7087C17.7046 22.929 17.205 23.1111 16.6934 23.2533C16.1735 23.3944 15.6436 23.4952 15.1083 23.555C15.1177 23.6021 15.1231 23.6396 15.1328 23.6759C15.3026 24.342 15.3588 25.032 15.2992 25.7167C15.2768 26.146 15.1934 26.5698 15.0515 26.9755C14.9839 27.1434 14.906 27.3069 14.8183 27.4652C14.7827 27.5266 14.7386 27.5826 14.6873 27.6315C14.4644 27.8617 14.1983 27.8636 13.9811 27.6274C13.8952 27.5322 13.8217 27.4265 13.7623 27.3128C13.59 26.9894 13.5013 26.6377 13.438 26.2791C13.3565 25.7895 13.331 25.2923 13.3619 24.797C13.3787 24.4343 13.4328 24.0743 13.5234 23.7226C13.5312 23.6928 13.5384 23.6627 13.5442 23.6325C13.5457 23.6248 13.5406 23.6158 13.5346 23.5911C11.9318 23.5005 10.3754 23.0201 9.0004 22.1915C8.97745 22.2424 8.95773 22.2853 8.93871 22.3284C8.55337 23.2275 7.96033 24.0225 7.20825 24.648C7.00915 24.8181 6.78047 24.9502 6.53361 25.0377C6.41792 25.0841 6.29167 25.0977 6.16876 25.0769C6.10089 25.0647 6.03733 25.0352 5.9843 24.9911C5.93126 24.9471 5.89056 24.89 5.86618 24.8255C5.78687 24.6338 5.80092 24.4343 5.82785 24.2366C5.87203 23.9609 5.95317 23.6925 6.06908 23.4385C6.41254 22.6386 6.91804 21.9186 7.55371 21.3238C7.57941 21.2995 7.60578 21.2758 7.63103 21.251C7.63906 21.2395 7.64591 21.2272 7.65149 21.2143C7.05256 20.6949 6.51739 20.1062 6.05723 19.4606C6.11237 19.4561 6.14925 19.4505 6.18615 19.4505C6.77495 19.4499 7.36377 19.452 7.95254 19.448C7.99171 19.4469 8.03065 19.4544 8.06652 19.4702C8.1024 19.4859 8.13431 19.5095 8.15994 19.5391C8.80004 20.1976 9.54586 20.7444 10.3665 21.1567C11.2347 21.6034 12.1787 21.884 13.1499 21.984C15.7886 22.2405 18.0585 21.4405 19.9595 19.5841C20.002 19.5382 20.0541 19.5021 20.1121 19.4785C20.1701 19.4548 20.2325 19.4442 20.2951 19.4473C20.844 19.4541 21.393 19.4501 21.9419 19.4501H22.0838L22.0936 19.4833Z" fill="#3B3D45"/>
|
||||
<path d="M19.6412 11.0745C19.7972 11.0745 19.9475 11.0851 20.0956 11.0717C20.2633 11.0565 20.3834 11.1165 20.5057 11.2292C21.212 11.88 21.9257 12.5229 22.637 13.1683C22.6728 13.2008 22.7093 13.2324 22.7552 13.273C22.798 13.2362 22.8381 13.2034 22.8764 13.1686C23.6096 12.5012 24.3422 11.8331 25.0743 11.1645C25.1047 11.1332 25.1414 11.1088 25.182 11.0929C25.2226 11.077 25.2662 11.07 25.3097 11.0724C25.49 11.0797 25.6707 11.0745 25.8608 11.0745V16.9751C25.7643 17.0033 24.4678 17.0089 24.313 16.9785V13.9903L24.283 13.976C23.7784 14.4362 23.2738 14.8964 22.7577 15.3671C22.241 14.9017 21.7305 14.4418 21.22 13.9819L21.1906 13.9927C21.1893 14.2421 21.1902 14.4915 21.19 14.7409C21.1899 14.9888 21.1899 15.2367 21.19 15.4846V16.9894H19.654C19.6253 16.8901 19.6119 11.4083 19.6412 11.0745Z" fill="#3B3D45"/>
|
||||
<path d="M5.46747 11.0815H6.99421C7.02504 11.1797 7.03107 16.8479 6.99951 16.9909H5.47141C5.46299 16.6155 5.46875 16.2414 5.4677 15.8675C5.46665 15.4966 5.46747 15.1258 5.46747 14.7453H3.57536V16.9708C3.46003 17.0052 2.15667 17.0085 2.0271 16.9777V11.0822H3.56924V13.1648C3.67944 13.1966 5.30094 13.2025 5.46607 13.172C5.46653 13.0053 5.46719 12.8346 5.46742 12.6638C5.46765 12.4867 5.46767 12.3096 5.46747 12.1325C5.46747 11.9599 5.46747 11.7872 5.46747 11.6145C5.46747 11.4422 5.46747 11.2698 5.46747 11.0815Z" fill="#3B3D45"/>
|
||||
<path d="M8.82422 16.9889V11.0991C8.91477 11.0695 12.2707 11.0579 12.4901 11.0877V12.3429C12.4409 12.3464 12.3901 12.3531 12.3393 12.3532C11.7417 12.354 11.144 12.3541 10.5463 12.3537H10.3801V13.33H12.2476V14.6287H10.3968C10.3659 14.7399 10.3574 15.5145 10.3825 15.7289C10.4298 15.7321 10.4805 15.7384 10.5312 15.7384C11.1289 15.7391 11.7265 15.7393 12.3242 15.7389H12.4905V16.9889H8.82422Z" fill="#3B3D45"/>
|
||||
<path d="M14.2399 16.991C14.2118 16.833 14.2175 11.1893 14.2453 11.082H15.7664V15.4368C15.832 15.4403 15.8835 15.4452 15.935 15.4452C16.5371 15.4458 17.1392 15.4459 17.7413 15.4456C17.7932 15.4456 17.845 15.4456 17.9042 15.4456V16.991L14.2399 16.991Z" fill="#3B3D45"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
@@ -5,126 +5,473 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Helm Dashboard</title>
|
||||
<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://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/>
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/lightfair.min.css"/>
|
||||
<link rel="stylesheet" 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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<i class="fa-solid fa-arrow-trend-down"></i>
|
||||
<nav class="navbar navbar-expand-lg bg-light rounded" style="margin-bottom: 0.75rem">
|
||||
<div class="container-fluid">
|
||||
<div style="line-height: 90%; font-size: 1.5rem" class="navbar-brand">
|
||||
<img src="static/logo.png" style="height: 3rem; float: left" alt="Logo">
|
||||
<a class="navbar-brand" href="#"><b>Helm Dashboard</b></a><br/>
|
||||
<span style="font-size: 0.8rem;">by <a href="https://komodor.io">komodor.io</a></span>
|
||||
<div class="container-fluid px-0">
|
||||
<!-- 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 me-0">
|
||||
<a href="/"><img src="static/logo-header.svg" alt="Helm Dashboard"></a>
|
||||
</div>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav"
|
||||
aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="mainNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="/">Installed Charts</a>
|
||||
</li>
|
||||
<!-- TODO
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled">Provisional Charts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled">Repositories</a>
|
||||
</li>
|
||||
-->
|
||||
</ul>
|
||||
<form class="d-flex flex-nowrap text-nowrap">
|
||||
<label for="cluster" style="margin-top: 0.5rem">K8s Context:</label>
|
||||
<select id="cluster" class="form-control"></select>
|
||||
</form>
|
||||
<i class="btn fa fa-power-off text-muted" title="Shut down the Helm Dashboard application"></i>
|
||||
<div class="separator-vertical mx-3"><span></span></div>
|
||||
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item mx-2">
|
||||
<a class="nav-link px-3 section-installed">Installed</a>
|
||||
</li>
|
||||
<li class="nav-item mx-2">
|
||||
<a class="nav-link px-3 section-repo">Repository</a>
|
||||
</li>
|
||||
<li class="nav-item mx-2 dropdown">
|
||||
<a class="nav-link dropdown-toggle section-help" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
Help
|
||||
</a>
|
||||
<ul class="dropdown-menu fs-80">
|
||||
<li><a class="dropdown-item"
|
||||
href="https://komodorkommunity.slack.com"
|
||||
target="_blank"><i class="bi-slack"></i> Support Chat</a></li>
|
||||
<li><a class="dropdown-item" href="https://github.com/komodorio/helm-dashboard" target="_blank"><i
|
||||
class="bi-github"></i> Project Page</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<!-- TODO: this should go under the "user menu" -->
|
||||
<button class="dropdown-item" id="cacheClear"><i
|
||||
class="bi-arrow-repeat"></i> Reset Cache
|
||||
</button>
|
||||
<li><a class="dropdown-item" href="api-docs" target="_blank"><i
|
||||
class="bi-braces"></i> REST API</a></li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><a class="dropdown-item disabled" href="#">Version <span id="toolVersion"></span></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item mx-2 display-none upgrade-possible">
|
||||
<a class="nav-link position-relative text-danger"
|
||||
href="https://github.com/komodorio/helm-dashboard/releases" target="_blank">
|
||||
Upgrade to <span id="toolVersionUpgrade"></span>
|
||||
</a></li>
|
||||
</ul>
|
||||
<div>
|
||||
<div class="border-muted text-muted border rounded p-1 pe-2 me-3 d-flex">
|
||||
<img alt="Komodor" src="https://raw.githubusercontent.com/komodorio/helm-charts/master/k8s-watcher.svg" class="me-2" style="width: 42px; height: 42px"/>
|
||||
<span class="text-nowrap">
|
||||
<a href="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash"
|
||||
class="link text-primary fw-bold text-decoration-none">Upgrade your HELM experience - Free
|
||||
<i class="bi-box-arrow-up-right ms-1"></i></a><br/>
|
||||
Auth & RBAC, k8s events, troubleshooting and more
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator-vertical"><span></span></div>
|
||||
<i class="btn bi-power text-muted p-2 m-1 mx-2" title="Shut down the Helm Dashboard application"></i>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- /TOP BAR -->
|
||||
|
||||
<div class="bg-light p-5 pt-0 rounded" id="sectionDetails" style="display: none">
|
||||
<span class="text-muted"
|
||||
style="transform: rotate(270deg); z-index: 100; display: inline-block; position: relative; left:-4rem; top: 4rem; color: #BBB!important; text-transform: uppercase">Revisions</span>
|
||||
<div class="row mb-3">
|
||||
|
||||
<!--REPO SECTION-->
|
||||
<div class="row mt-3 pt-3 me-5 section" id="sectionRepo" style="display: none">
|
||||
<div class="col-3 ps-4 repo-list">
|
||||
<div class="p-2 bg-white rounded-1 b-shadow">
|
||||
<h4 class="fs-6">Repositories</h4>
|
||||
<ul class="list-unstyled p-2">
|
||||
</ul>
|
||||
<button class="btn btn-sm border-secondary text-muted">
|
||||
<i class="bi-plus-lg"></i> Add Repository
|
||||
</button>
|
||||
<div class="mt-2 p-2 small">Charts developers: you can also add local directories as chart source. Use
|
||||
<span class="font-monospace text-success">--local-chart</span> CLI switch to specify it.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1><span class="name"></span>,
|
||||
revision <span class="rev"></span></h1>
|
||||
Chart <b id="chartName"></b>: <i id="revDescr"></i>
|
||||
|
||||
<nav class="mt-2">
|
||||
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-resources" data-tab="resources"
|
||||
type="button" role="tab" aria-controls="nav-resources" aria-selected="false">Resources
|
||||
<div class="col-9 repo-details bg-white b-shadow pt-4 px-5 overflow-auto rounded">
|
||||
<div class="float-end">
|
||||
<button class="me-2 btn btn-sm btn-light bg-white border border-secondary btn-update">
|
||||
<i class="bi-arrow-repeat"></i> Update
|
||||
</button>
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="manifests"
|
||||
type="button" role="tab" aria-controls="nav-manifest-diff" aria-selected="false">Manifests
|
||||
</button>
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="values"
|
||||
type="button" role="tab" aria-controls="nav-disabled" aria-selected="false">
|
||||
Parameterized Values
|
||||
</button>
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="notes"
|
||||
type="button" role="tab" aria-controls="nav-disabled" aria-selected="false">Notes
|
||||
<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>
|
||||
</nav>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane p-3" id="nav-resources" role="tabpanel">
|
||||
<div><span class="text-muted small fw-bold me-3">REPOSITORY</span></div>
|
||||
<h2 class="mb-3">name-of-repo</h2>
|
||||
<div class="mb-5">
|
||||
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">URL: <span class="url fw-bold">http://somerepo/somepath</span></span>
|
||||
</div>
|
||||
<div class="py-2 mb-3 float-end">
|
||||
|
||||
</div>
|
||||
<div class="tab-pane" id="nav-manifest" role="tabpanel">
|
||||
<nav class="navbar bg-light">
|
||||
<form class="container-fluid" id="modePanel">
|
||||
<label class="form-check-label" for="diffModeNone">
|
||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModeNone"
|
||||
data-mode="view">
|
||||
View Current
|
||||
</label>
|
||||
<label class="form-check-label" for="diffModePrev">
|
||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModePrev"
|
||||
data-mode="diff-prev">
|
||||
Diff with previous
|
||||
</label>
|
||||
<label class="form-check-label" for="diffModeRev">
|
||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModeRev"
|
||||
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>
|
||||
|
||||
<div id="manifestText" class="mt-2 bg-white"></div>
|
||||
</div>
|
||||
<div class="tab-pane" id="nav-disabled" role="tabpanel" aria-labelledby="nav-disabled-tab"
|
||||
tabindex="0">...
|
||||
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
|
||||
style="text-transform: uppercase">
|
||||
<div class="col-3">Chart Name</div>
|
||||
<div class="col">Description</div>
|
||||
<div class="col-1">Version</div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
<ul class="list-unstyled mt-4 charts"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-light p-5 rounded" id="sectionList" style="display: none">
|
||||
<h1>Charts List</h1>
|
||||
<div id="charts" class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
<div class="row mt-3 pt-3 me-5 section" id="sectionList" style="display: none">
|
||||
<div class="col-2 ms-3">
|
||||
<!-- FILTER BLOCK -->
|
||||
<div class="p-2 ps-2 bg-white rounded-1 b-shadow" id="filters">
|
||||
<form>
|
||||
<div id="clusterFilterBlock">
|
||||
<h4>Clusters</h4>
|
||||
<ul class="list-unstyled" id="cluster">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">Namespaces</h4>
|
||||
<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 -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 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 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>
|
||||
<div class="col-3">Release Status</div>
|
||||
<div class="col-2">Chart</div>
|
||||
<div class="col-1">Revision</div>
|
||||
<div class="col-1">Namespace</div>
|
||||
<div class="col-1">Updated</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="row flex-nowrap pt-0 mx-0 section" id="sectionDetails" style="display: none">
|
||||
<div class="col-2 px-4 py-4 pe-3 rev-list">
|
||||
<h3 class="fw-bold small">Revisions</h3>
|
||||
<ul class="list-unstyled">
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-10 rev-details bg-white b-shadow pt-4 px-5 overflow-auto">
|
||||
<div><span class="rev-status fw-bold me-3"></span></div>
|
||||
<div>
|
||||
<h1 class="name float-start">Name</h1>
|
||||
<div id="actionButtons" class="float-end">
|
||||
<span><button id="btnUpgrade"
|
||||
class="opacity-10 btn btn-sm btn-light bg-white me-2 border-secondary">
|
||||
<i class="icon bi-hourglass-split"></i> <span></span>
|
||||
</button></span>
|
||||
|
||||
<button id="btnRollback" class="btn btn-sm btn-light bg-white border border-secondary me-2"
|
||||
title="Rollback to this revision"><i class="bi-arrow-repeat"></i> <span>Rollback</span>
|
||||
</button>
|
||||
<button id="btnTest"
|
||||
class="btn btn-sm btn-light bg-white border border-secondary me-2 display-none"
|
||||
title="Run tests for this chart"><i class="bi-check-circle"></i> <span>Run tests</span>
|
||||
</button>
|
||||
<button id="btnUninstall" class="btn btn-sm btn-light bg-white border border-secondary"
|
||||
title="Uninstall the chart"><i class="bi-trash3"></i> Uninstall
|
||||
</button>
|
||||
<br/>
|
||||
<a class="link small" id="btnUpgradeCheck">
|
||||
<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>
|
||||
</div>
|
||||
<div class="fs-2"> </div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Revision <span class="rev fw-bold me-4"></span>
|
||||
<span class="rev-date"></span>
|
||||
</div>
|
||||
|
||||
<div class="rev-tags mt-3">
|
||||
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">chart version: <span
|
||||
class="rev-chart fw-bold"></span></span>
|
||||
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">app version: <span
|
||||
class="rev-app fw-bold"></span></span>
|
||||
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">namespace: <span
|
||||
class="rev-ns fw-bold"></span></span>
|
||||
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">cluster: <span
|
||||
class="rev-cluster fw-bold"></span></span>
|
||||
</div>
|
||||
|
||||
<div id="revDescr" class="mt-3 mb-4"></div>
|
||||
|
||||
<nav class="mt-2">
|
||||
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-resources" data-tab="resources"
|
||||
type="button" role="tab" aria-controls="nav-resources" aria-selected="false" tabindex="-1">
|
||||
Resources
|
||||
</button>
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="manifests"
|
||||
type="button" role="tab" aria-controls="nav-manifest" aria-selected="false"
|
||||
tabindex="-1">Manifests
|
||||
</button>
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="values"
|
||||
type="button" role="tab" aria-controls="nav-manifest" aria-selected="false" tabindex="-1">
|
||||
Values
|
||||
</button>
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-manifest" data-tab="notes"
|
||||
type="button" role="tab" aria-controls="nav-manifest" aria-selected="false" tabindex="-1">
|
||||
Notes
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane p-3 container-fluid" id="nav-resources" role="tabpanel">
|
||||
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
|
||||
style="text-transform: uppercase">
|
||||
<div class="col-2">Resource Type</div>
|
||||
<div class="col-3">Name</div>
|
||||
<div class="col-2">Status</div>
|
||||
<div class="col-5">Status Message</div>
|
||||
<div class="col-1"></div>
|
||||
</div>
|
||||
<div class="body"></div>
|
||||
</div>
|
||||
<div class="tab-pane" id="nav-manifest" role="tabpanel">
|
||||
<nav class="navbar bg-white rounded border border-secondary">
|
||||
<form class="container-fluid" id="modePanel">
|
||||
<label class="form-check-label" for="diffModeNone">
|
||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModeNone"
|
||||
data-mode="view">
|
||||
View
|
||||
</label>
|
||||
<label class="form-check-label" for="diffModePrev">
|
||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModePrev"
|
||||
data-mode="diff-prev">
|
||||
Diff with previous
|
||||
</label>
|
||||
<label class="form-check-label" for="diffModeRev">
|
||||
<input class="form-check-input" type="radio" name="diffMode" id="diffModeRev"
|
||||
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>
|
||||
|
||||
<div id="manifestText" class="mt-2 bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Modals -->
|
||||
<div id="errorAlert" style="z-index: 2000; max-width: 95%; overflow: auto"
|
||||
class="display-none alert alert-sm alert-danger alert-dismissible position-absolute position-absolute top-0 start-50 translate-middle-x mt-3 border-danger"
|
||||
role="alert">
|
||||
<h4 class="alert-heading"><i class="bi-exclamation-triangle-fill"></i> <span></span></h4>
|
||||
<hr>
|
||||
<p style="white-space: pre-wrap"></p>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<hr/>
|
||||
<span class="small text-muted fs-80">Hint: Komodor has the same HELM capabilities, with enterprise features and support. <a href="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash" target="_blank">Sign up for free.</a></span>
|
||||
</div>
|
||||
|
||||
<div class="offcanvas offcanvas-end rounded-start" tabindex="-1" id="describeModal"
|
||||
aria-labelledby="describeModalLabel" style="overflow-x: auto">
|
||||
<div class="offcanvas-header border-bottom p-4">
|
||||
<div>
|
||||
<h5 id="describeModalLabel"></h5>
|
||||
<p class="m-0 mt-4">ResourceType</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href='https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash'
|
||||
class='btn btn-primary btn-sm me-2' target='_blank'>See more details in Komodor <i
|
||||
class='bi-box-arrow-up-right'></i></a>
|
||||
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="offcanvas-body p-2 ps-4" id="describeModalBody">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal" id="confirmModal" tabindex="-1" aria-labelledby="describeModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="confirmModalLabel"></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="confirmModalBody">
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-confirm">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="repoAddModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Add Chart Repository</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form enctype="application/x-www-form-urlencoded">
|
||||
<label class="form-label">Name: <input class="form-control" name="name"></label>
|
||||
<label class="form-label">URL: <input class="form-control" name="url"></label>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-confirm">Add Repository</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="upgradeModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="upgradeModalLabel">
|
||||
<span class="type"></span> <b class='text-success name'></b>
|
||||
</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form class="modal-body border-bottom fs-5" enctype="multipart/form-data">
|
||||
<input name="preview" type="hidden" class="preview-mode"/>
|
||||
<input name="chart" type="hidden" class="chart-name"/>
|
||||
<div class="input-group mb-3 text-muted">
|
||||
<label class="form-label me-4 text-dark">Version to install: <select
|
||||
class='fw-bold text-success ver-new' name="version"></select></label> <span class="ver-old">(current version is <span
|
||||
class='text-success ms-1'>0.0.0</span>)</span>
|
||||
</div>
|
||||
<div class="input-group mb-3 text-muted">
|
||||
<label class="form-label me-4 text-dark">
|
||||
Release Name: <input class="form-control rel-name" name="name">
|
||||
</label>
|
||||
<label class="form-label me-4 text-dark">
|
||||
Namespace (optional):
|
||||
<input type="text" class="form-control rel-ns" list="ns-datalist"/>
|
||||
<datalist id="ns-datalist"></datalist>
|
||||
</label>
|
||||
<label class="form-label me-4 text-dark">
|
||||
Cluster: <span class="form-label rel-cluster"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 pe-3">
|
||||
<label class="form-label">User-Defined Values:</label>
|
||||
</div>
|
||||
<div class="col-6 ps-3">
|
||||
<label class="form-label">Chart Values Reference:</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 pe-3">
|
||||
<textarea name="values" class="form-control w-100 h-100" rows="5"
|
||||
style="font-family: monospace"></textarea>
|
||||
</div>
|
||||
<div class="col-6 ps-3">
|
||||
<pre class="ref-vals fs-6 w-100 bg-secondary p-2 rounded" style="max-height: 20rem"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 pe-3">
|
||||
<span class="invalid-feedback small mb-3"> (wrong YAML)</span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-label mt-4">Manifest changes:</label>
|
||||
<div id="upgradeModalBody" class="small"></div>
|
||||
</form>
|
||||
<div class="modal-footer d-flex">
|
||||
<button type="button" class="btn btn-scan bg-white border-secondary display-none">Scan for Problems
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-confirm">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="testModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="testModalLabel">
|
||||
<span class="type">Test results</span>
|
||||
</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body border-bottom fs-5">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<span class="test-result"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="PowerOffModal" tabindex="-1" aria-labelledby="ModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="ModalLabel">Session Ended</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
The Helm Dashboard application has been shut down. You can now close the browser tab.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
|
||||
crossorigin="anonymous"></script>
|
||||
@@ -133,10 +480,18 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"
|
||||
integrity="sha512-gU7kztaQEl7SHJyraPfZLQCNnrKdaQi5ndOyt4L4UPL/FHDd/uB9Je6KDARIqwnNNE27hnqoWLBq+Kpe4iHfeQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.1/dist/js.cookie.min.js"
|
||||
integrity="sha256-0H3Nuz3aug3afVbUlsu12Puxva3CP4EhJtPExqs54Vg=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/luxon@3.0.3/build/global/luxon.min.js"
|
||||
integrity="sha256-RH4TKnKcKyde0s2jc5BW3pXZl/5annY3fcZI9VrV5WQ=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"
|
||||
integrity="sha512-CSBhVREyzHAjAFfBlIBakjoRUKp5h7VSweP0InR/pAJyptH7peuhCsqAI/snV+TwZmXZqoUklpXp6R6wMnYf5Q=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script src="static/repo.js"></script>
|
||||
<script src="static/list-view.js"></script>
|
||||
<script src="static/revisions-view.js"></script>
|
||||
<script src="static/details-view.js"></script>
|
||||
<script src="static/actions.js"></script>
|
||||
<script src="static/scripts.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
16
pkg/dashboard/static/komodor-logo.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="77" height="19" viewBox="0 0 77 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_231_1524)">
|
||||
<path d="M8.51786 7.17108H5.90895L1.96859 10.9915V3.16663H0V16.0875H1.96859V13.0592L3.52824 11.6745L6.64123 16.0875H9.06309L4.93573 10.4021L8.51786 7.17108Z" fill="#1347FF"/>
|
||||
<path d="M28.2007 7.17127L27.2085 8.16924L26.2321 7.17127H22.6848L21.5024 8.39069V7.17127H19.6448V16.0877H21.6165V9.84712L22.6658 8.79613H24.9356L25.4808 9.33257V16.0877H27.4526V9.49785L28.1627 8.79613H30.7716L31.3168 9.33257V16.0877H33.2854V8.75871L31.7099 7.17127H28.2007Z" fill="#1347FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.38939 8.75871L10.9649 7.17127H16.3508L17.9263 8.75871V14.5003L16.3508 16.0877H10.9649L9.38939 14.5003V8.75871ZM15.4124 14.4628L15.9387 13.9264H15.9418V9.33257L15.4156 8.79613H11.9064L11.3802 9.33257V13.9264L11.9033 14.4628H15.4124Z" fill="#1347FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.8772 7.17127L61.3017 8.75871V14.5003L62.8772 16.0877H68.2631L69.8385 14.5003V8.75871L68.2631 7.17127H62.8772ZM67.8513 13.9264L67.3249 14.4628H63.8158L63.2895 13.9264V9.33257L63.8158 8.79613H67.3249L67.8513 9.33257V13.9264Z" fill="#1347FF"/>
|
||||
<path d="M73.4148 8.49984L74.7271 7.17127H77.0003V9.01758H74.5785L73.5289 10.053V16.0877H71.5571V7.17127H73.4148V8.49984Z" fill="#1347FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.4523 7.17127L34.8768 8.75871V14.5003L36.4523 16.0877H48.0926L49.668 14.5003V8.75871L48.0926 7.17127H36.4523ZM47.6776 13.9264L47.1512 14.4628H37.3875L36.8613 13.9264V9.33257L37.3875 8.79613H47.1512L47.6776 9.33257V13.9264Z" fill="#1347FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.5078 7.17108L57.5955 8.27823V3.16663H59.5637V16.0875H57.5955V14.9991L56.5078 16.0875H52.7546L51.1792 14.5001V8.7585L52.7546 7.17108H56.5078ZM56.546 14.4627L57.5955 13.4303V9.84695L56.546 8.79592H53.6933L53.1478 9.33236V13.9262L53.6933 14.4627H56.546Z" fill="#1347FF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_231_1524">
|
||||
<rect width="77" height="19" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
115
pkg/dashboard/static/list-view.js
Normal file
@@ -0,0 +1,115 @@
|
||||
function loadChartsList() {
|
||||
$("body").removeClass("bg-variant1 bg-variant2").addClass("bg-variant1")
|
||||
$("#sectionList").show()
|
||||
const chartsCards = $("#installedList .body")
|
||||
chartsCards.empty().append("<div><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span> Loading...</div>")
|
||||
$.getJSON("/api/helm/releases").fail(function (xhr) {
|
||||
sendStats('Get releases', {'status': 'failed'});
|
||||
reportError("Failed to get list of charts", xhr)
|
||||
chartsCards.empty().append("<div class=\"row m-0 py-4 bg-white rounded-1 b-shadow border-4 border-start\"><div class=\"col\">Failed to get list of charts</div></div>")
|
||||
}).done(function (data) {
|
||||
chartsCards.empty().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-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>
|
||||
<div class="col-1 rel-ns text-nowrap"><span>default</span><div>Namespace</div></div>
|
||||
<div class="col-1 rel-date text-nowrap"><span>today</span><div>Updated</div></div>
|
||||
</div>`)
|
||||
|
||||
if (elm.icon) {
|
||||
card.find(".rel-name").attr("style", "background-image: url(" + elm.icon + ")")
|
||||
}
|
||||
|
||||
if (elm.description) {
|
||||
card.find(".rel-name div").text(elm.description)
|
||||
}
|
||||
|
||||
card.find(".rel-name span").text(elm.name)
|
||||
card.find(".rel-rev span").text("#" + elm.revision)
|
||||
card.find(".rel-ns span").text(elm.namespace)
|
||||
card.find(".rel-chart span").text(elm.chart)
|
||||
card.find(".rel-date span").text(getAge(elm))
|
||||
|
||||
card.data("namespace", elm.namespace)
|
||||
card.data("name", elm.name)
|
||||
card.data("chart", elm.chart)
|
||||
|
||||
statusStyle(elm.status, card, card.find(".rel-status span"))
|
||||
|
||||
card.find("a").attr("href", '#context=' + getHashParam('context') + '&namespace=' + elm.namespace + '&name=' + elm.name)
|
||||
|
||||
card.data("chart", elm).click(function () {
|
||||
if (window.getSelection().toString()) {
|
||||
return
|
||||
}
|
||||
const self = $(this)
|
||||
$("#sectionList").hide()
|
||||
|
||||
let chart = self.data("chart");
|
||||
setHashParam("namespace", chart.namespace)
|
||||
setHashParam("chart", chart.name)
|
||||
|
||||
loadChartHistory(chart.namespace, chart.name, elm.chart_name)
|
||||
})
|
||||
|
||||
// check if upgrade is possible
|
||||
$.getJSON("/api/helm/repositories/latestver?name=" + elm.chartName).fail(function (xhr) {
|
||||
reportError("Failed to find chart in repo", xhr)
|
||||
}).done(function (data) {
|
||||
if (!data || !data.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isNewerVersion(elm.chartVersion, data[0].version) || data[0].isSuggestedRepo) {
|
||||
const icon = $("<br/><span class='ms-2 fw-bold' data-bs-toggle='tooltip' data-bs-placement='bottom'></span>")
|
||||
if (data[0].isSuggestedRepo) {
|
||||
icon.addClass("bi-plus-circle-fill text-primary")
|
||||
icon.text(" ADD REPO")
|
||||
icon.attr("data-bs-title", "Add '" + data[0].repository+"' to list of known repositories")
|
||||
} else {
|
||||
icon.addClass("bi-arrow-up-circle-fill text-primary")
|
||||
icon.text(" UPGRADE")
|
||||
icon.attr("data-bs-title", "Upgrade available: " + data[0].version + " from " + data[0].repository)
|
||||
}
|
||||
card.find(".rel-chart div").append(icon)
|
||||
|
||||
const tooltipTriggerList = card.find('[data-bs-toggle="tooltip"]')
|
||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||
sendStats('upgradeIconShown', {'isProbable': data[0].isSuggestedRepo})
|
||||
}
|
||||
})
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
$("#installedSearch").keyup(function () {
|
||||
filterInstalledList($("#installedList .body .row"))
|
||||
})
|
||||
109
pkg/dashboard/static/logo-header-inverted.svg
Normal file
|
After Width: | Height: | Size: 14 KiB |
16
pkg/dashboard/static/logo-header.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 4.5 KiB |
92
pkg/dashboard/static/logo.svg
Normal file
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="179"
|
||||
height="164"
|
||||
viewBox="0 0 179 164"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg20"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs24" />
|
||||
<sodipodi:namedview
|
||||
id="namedview22"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
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="svg20" />
|
||||
<rect
|
||||
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="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="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>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
749
pkg/dashboard/static/openapi.json
Normal file
@@ -0,0 +1,749 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "Helm Dashboard API",
|
||||
"version": ""
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Releases"
|
||||
},
|
||||
{
|
||||
"name": "Repositories"
|
||||
},
|
||||
{
|
||||
"name": "K8s"
|
||||
},
|
||||
{
|
||||
"name": "Scanners"
|
||||
},
|
||||
{
|
||||
"name": "Miscellaneous"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/helm/releases": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Get list of installed releases",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns list of installed releases"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace, use '[emtpy]' if you want to use k8s context default"
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Install new release",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"chart": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"values": {
|
||||
"type": "string",
|
||||
"description": "Text of values.yaml to use"
|
||||
},
|
||||
"preview": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "In case preview=true, the preview diff is generated",
|
||||
"content": {
|
||||
"text/plain": {}
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"description": "In case preview=false, the actial install is performed and resulting release object is returned",
|
||||
"content": {
|
||||
"application/json": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Upgrade/reconfigure existing release",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"chart": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"values": {
|
||||
"type": "string",
|
||||
"description": "Text of values.yaml to use"
|
||||
},
|
||||
"preview": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "In case preview=true, the preview diff is generated",
|
||||
"content": {
|
||||
"text/plain": {}
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"description": "In case preview=false, the actial install is performed and resulting release object is returned",
|
||||
"content": {
|
||||
"application/json": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/history": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Get revision history for release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of release revisions"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/manifest": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
},
|
||||
{
|
||||
"name": "revision",
|
||||
"in": "query",
|
||||
"description": "Revision to get data from"
|
||||
},
|
||||
{
|
||||
"name": "revisionDiff",
|
||||
"in": "query",
|
||||
"description": "Revision to diff against"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Get manifest for release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Manifest text, or diff if revisionDiff is specified"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/values": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
},
|
||||
{
|
||||
"name": "revision",
|
||||
"in": "query",
|
||||
"description": "Revision to get data from"
|
||||
},
|
||||
{
|
||||
"name": "revisionDiff",
|
||||
"in": "query",
|
||||
"description": "Revision to diff against"
|
||||
},
|
||||
{
|
||||
"name": "userDefined",
|
||||
"in": "query",
|
||||
"description": "If set, only user-defined values will be listed"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Get values for release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Values YAML text, or diff if revisionDiff is specified"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/notes": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
},
|
||||
{
|
||||
"name": "revision",
|
||||
"in": "query",
|
||||
"description": "Revision to get data from"
|
||||
},
|
||||
{
|
||||
"name": "revisionDiff",
|
||||
"in": "query",
|
||||
"description": "Revision to diff against"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Get textual notes for release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Notes text, or diff if revisionDiff is specified"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/resources": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "List of installed k8s resources for this release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Structured list of resources",
|
||||
"content": {
|
||||
"application/json": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/rollback": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Rollback the release to a previous revision",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"revision": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Rolled back successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/test": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Run the tests on a release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Logs of a test run"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/repositories": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"description": "Get list of Helm repositories",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns list of Helm repositories"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"description": "Adds new repository",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"url"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Empty response in case repository were added"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/repositories/{repo}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"description": "Name of Helm repository"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"description": "Get list of charts in repository",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns list of charts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"description": "Update repository from remote",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Empty response"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"description": "Remove repository",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Empty response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/repositories/latestver": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "Name of Helm chart to search for",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"description": "Find the latest available version of specified chart through all the repositories",
|
||||
"get": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The object with latest available version is returned"
|
||||
},
|
||||
"204": {
|
||||
"description": "In case no matching repository found, the response is empty with status 204"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/repositories/versions": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "Name of Helm chart to search for",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"description": "Get the list of versions for specified chart across the repositories",
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list if chart versions is returned"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/repositories/values": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "chart",
|
||||
"in": "query",
|
||||
"description": "Name of Helm chart to search for, in format of <repository>/<chart-name>",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"in": "query",
|
||||
"description": "Version of Helm chart to get values from",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"description": "Get the original values.yaml file for the chart",
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The content of values.yaml"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/k8s/contexts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"K8s"
|
||||
],
|
||||
"description": "Get list of kubectl contexts configured locally",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns list of contexts"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/k8s/{kind}/get": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "kind",
|
||||
"in": "path",
|
||||
"description": "Kind of kubernetes resource"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "Name of kubernetes resource",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "query",
|
||||
"description": "Namespace of kubernetes resource",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"K8s"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns resources information"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/k8s/{kind}/list": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "kind",
|
||||
"in": "path",
|
||||
"description": "Kind of kubernetes resource",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"namespaces"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"K8s"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns list of resources"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/k8s/{kind}/describe": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "kind",
|
||||
"in": "path",
|
||||
"description": "Kind of kubernetes resource"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "Name of kubernetes resource",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "query",
|
||||
"description": "Namespace of kubernetes resource",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"K8s"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"text/plain": {}
|
||||
},
|
||||
"description": "Returns describe text"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/scanners": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Scanners"
|
||||
],
|
||||
"description": "Get list of discovered scanners",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of scanners"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/scanners/manifests": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Scanners"
|
||||
],
|
||||
"description": "Scan manifests using all applicable scanners",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"manifest": {
|
||||
"type": "string",
|
||||
"description": "Text of manifest to scan"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Map of scan results per scanner type"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/scanners/resource/{kind}": {
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "kind"
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "namespace",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "name",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Scanners"
|
||||
],
|
||||
"description": "Scan specified k8s resource in cluster",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Information with scan results per scanner type"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Miscellaneous"
|
||||
],
|
||||
"description": "Shuts down the Helm Dashboard application",
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Shutdown command has been accepted"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Miscellaneous"
|
||||
],
|
||||
"description": "Gets application status",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns JSON with some options",
|
||||
"headers": {
|
||||
"X-Application-Name": {
|
||||
"description": "A string to self-identify the application"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
pkg/dashboard/static/repo.js
Normal file
@@ -0,0 +1,158 @@
|
||||
function loadRepoView() {
|
||||
$("#sectionRepo .repo-details").hide()
|
||||
$("#sectionRepo").show()
|
||||
|
||||
$("#repoAddModal input[name=name]").val(getHashParam("suggestRepo"))
|
||||
$("#repoAddModal input[name=url]").val(getHashParam("suggestRepoUrl"))
|
||||
|
||||
if (getHashParam("suggestRepo")) {
|
||||
$("#sectionRepo .repo-list .btn").click()
|
||||
}
|
||||
|
||||
$.getJSON("/api/helm/repositories").fail(function (xhr) {
|
||||
reportError("Failed to get list of repositories", xhr)
|
||||
sendStats('Get repo', {'status': 'fail'});
|
||||
}).done(function (data) {
|
||||
const items = $("#sectionRepo .repo-list ul").empty()
|
||||
data.sort((a, b) => (a.name > b.name) - (a.name < b.name))
|
||||
|
||||
data.forEach(function (elm) {
|
||||
let opt = $('<li class="mb-2"><label><input type="radio" name="repo" class="me-2"/><span></span></label></li>');
|
||||
opt.attr('title', elm.url)
|
||||
opt.find("input").val(elm.name).text(elm.name).data("item", elm)
|
||||
opt.find("span").text(elm.name)
|
||||
items.append(opt)
|
||||
})
|
||||
|
||||
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)
|
||||
$("#sectionRepo .repo-details").show()
|
||||
$("#sectionRepo .repo-details h2").text(elm.name)
|
||||
$("#sectionRepo .repo-details .url").text(elm.url)
|
||||
|
||||
$("#sectionRepo .btn-remove").prop("disabled", elm.url.startsWith('file://'))
|
||||
|
||||
$("#sectionRepo .repo-details ul").html('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>')
|
||||
$.getJSON("/api/helm/repositories/" + elm.name).fail(function (xhr) {
|
||||
reportError("Failed to get list of charts in repo", xhr)
|
||||
}).done(function (data) {
|
||||
$("#sectionRepo .repo-details ul").empty()
|
||||
data.forEach(function (elm) {
|
||||
const li = $(`<li class="row p-2 rounded">
|
||||
<h6 class="col-3 py-2">` + elm.name.split('/').pop() + `</h6>
|
||||
<div class="col py-2">` + elm.description + `</div>
|
||||
<div class="col-1 py-2">` + elm.version + `</div>
|
||||
<div class="col-1 action text-nowrap"><button class="btn btn-sm border-secondary bg-white">Install</button></div>
|
||||
</li>`)
|
||||
|
||||
if (elm.icon) {
|
||||
li.find("h6").prepend('<img src="' + elm.icon + '" class="me-1" style="height: 1rem"/>')
|
||||
}
|
||||
|
||||
li.data("item", elm)
|
||||
|
||||
if (elm.installed_namespace) {
|
||||
li.find("button").text("View").addClass("btn-success").removeClass("bg-white")
|
||||
li.find(".action").prepend("<i class='bi-check-circle-fill me-1 text-success' title='Already installed'></i>")
|
||||
}
|
||||
|
||||
li.click(repoChartClicked)
|
||||
|
||||
$("#sectionRepo .repo-details ul").append(li)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (getHashParam("repo")) {
|
||||
items.find("input[value='" + getHashParam("repo") + "']").click()
|
||||
} else {
|
||||
items.find("input").first().click()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$("#inputSearch").keyup(function () {
|
||||
let val = $(this).val().toLowerCase();
|
||||
|
||||
$(".charts li").each(function () {
|
||||
let chartName = $(this.firstElementChild).text().toLowerCase()
|
||||
if (chartName.indexOf(val) >= 0) {
|
||||
$(this).show()
|
||||
} else {
|
||||
$(this).hide()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
$("#sectionRepo .repo-list .btn").click(function () {
|
||||
setHashParam("suggestRepo", null)
|
||||
setHashParam("suggestRepoUrl", null)
|
||||
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
|
||||
myModal.show()
|
||||
})
|
||||
|
||||
$("#repoAddModal .btn-confirm").click(function () {
|
||||
$("#repoAddModal .btn-confirm").prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>')
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: "/api/helm/repositories",
|
||||
data: $("#repoAddModal form").serialize(),
|
||||
}).fail(function (xhr) {
|
||||
reportError("Failed to add repo", xhr)
|
||||
}).done(function () {
|
||||
setHashParam("repo", $("#repoAddModal form input[name=name]").val())
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
|
||||
$("#sectionRepo .btn-remove").click(function () {
|
||||
if (confirm("Confirm removing repository?")) {
|
||||
$.ajax({
|
||||
type: 'DELETE',
|
||||
url: "/api/helm/repositories/" + $("#sectionRepo .repo-details h2").text(),
|
||||
}).fail(function (xhr) {
|
||||
reportError("Failed to add repo", xhr)
|
||||
}).done(function () {
|
||||
setHashParam("repo", null)
|
||||
window.location.reload()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
$("#sectionRepo .btn-update").click(function () {
|
||||
$("#sectionRepo .btn-update i").removeClass("bi-arrow-repeat").append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: "/api/helm/repositories/" + $("#sectionRepo .repo-details h2").text(),
|
||||
}).fail(function (xhr) {
|
||||
reportError("Failed to add repo", xhr)
|
||||
}).done(function () {
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
|
||||
function repoChartClicked() {
|
||||
const self = $(this)
|
||||
const elm = self.data("item")
|
||||
if (elm.installed_namespace) {
|
||||
setHashParam("section", null)
|
||||
setHashParam("namespace", elm.installed_namespace)
|
||||
setHashParam("chart", elm.installed_name)
|
||||
window.location.reload()
|
||||
} else {
|
||||
const contexts = $("body").data("contexts")
|
||||
const ctxFiltered = contexts.filter(obj => {
|
||||
return obj.Name === getHashParam("context")
|
||||
});
|
||||
const contextNamespace = ctxFiltered.length ? ctxFiltered[0].Namespace : ""
|
||||
elm.repository = $("#sectionRepo .repo-details h2").text()
|
||||
popUpUpgrade(elm, contextNamespace)
|
||||
}
|
||||
}
|
||||
75
pkg/dashboard/static/revisions-view.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const revRow = $("#sectionDetails .rev-list ul");
|
||||
|
||||
function loadChartHistory(namespace, name) {
|
||||
$("body").removeClass("bg-variant1 bg-variant2").addClass("bg-variant2")
|
||||
$("#sectionDetails").show()
|
||||
$("#sectionDetails .name").text(name)
|
||||
revRow.empty().append("<li><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span></li>")
|
||||
$.getJSON("/api/helm/releases/" + namespace + "/" + name + "/history").fail(function (xhr) {
|
||||
reportError("Failed to get chart details", xhr)
|
||||
}).done(function (data) {
|
||||
fillChartHistory(data, namespace, name);
|
||||
|
||||
checkUpgradeable(data[0].chart_name)
|
||||
|
||||
$("#btnTest").toggle(data[0].has_tests)
|
||||
|
||||
const rev = getHashParam("revision")
|
||||
if (rev) {
|
||||
revRow.find(".rev-" + rev).click()
|
||||
} else {
|
||||
revRow.find("li:first-child").click()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function fillChartHistory(data, namespace, name) {
|
||||
revRow.empty()
|
||||
data.reverse()
|
||||
for (let x = 0; x < data.length; x++) {
|
||||
const elm = data[x]
|
||||
$("#specRev").data("first-rev", elm.revision)
|
||||
|
||||
if (!x) {
|
||||
$("#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 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>
|
||||
<div class="position-absolute bottom-0 end-0 m-2 text-muted small">AGE: <span class="rev-age"></span></div>
|
||||
</li>`)
|
||||
rev.find(".rev-number").text("#" + elm.revision)
|
||||
//rev.find(".app-ver").text(elm.app_version)
|
||||
//rev.find(".chart-ver").text(elm.chart_ver)
|
||||
rev.find(".rev-date").text(elm.updated.replace("T", " "))
|
||||
|
||||
rev.find(".rev-age").text(getAge(elm, data[x - 1])).parent().attr("title", elm.updated)
|
||||
statusStyle(elm.status, rev.find(".rev-status"), rev.find(".rev-status"))
|
||||
|
||||
if (elm.description.startsWith("Rollback to ")) {
|
||||
//rev.find(".rev-status").append(" <span class='small fw-normal text-lowercase'>(rollback)</span>")
|
||||
rev.find(".rev-status").append(" <i class='bi-arrow-counterclockwise text-muted' title='" + elm.description + "'></i>")
|
||||
}
|
||||
|
||||
const nxt = data[x + 1];
|
||||
if (nxt && isNewerVersion(elm.chart_ver, nxt.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='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)
|
||||
}
|
||||
}
|
||||
@@ -1,284 +1,87 @@
|
||||
const clusterSelect = $("#cluster");
|
||||
const chartsCards = $("#charts");
|
||||
const revRow = $("#sectionDetails .row");
|
||||
|
||||
function reportError(err) {
|
||||
alert(err) // TODO: nice modal/baloon/etc
|
||||
}
|
||||
|
||||
function revisionClicked(namespace, name, self) {
|
||||
let active = "active border-primary border-2 bg-opacity-25 bg-primary";
|
||||
let inactive = "border-secondary bg-white";
|
||||
revRow.find(".active").removeClass(active).addClass(inactive)
|
||||
self.removeClass(inactive).addClass(active)
|
||||
const elm = self.data("elm")
|
||||
setHashParam("revision", elm.revision)
|
||||
$("#sectionDetails h1 span.rev").text(elm.revision)
|
||||
$("#chartName").text(elm.chart)
|
||||
$("#revDescr").text(elm.description).removeClass("text-danger")
|
||||
if (elm.status === "failed") {
|
||||
$("#revDescr").addClass("text-danger")
|
||||
}
|
||||
|
||||
const tab = getHashParam("tab")
|
||||
if (!tab) {
|
||||
$("#nav-tab [data-tab=resources]").click()
|
||||
} else {
|
||||
$("#nav-tab [data-tab=" + tab + "]").click()
|
||||
}
|
||||
}
|
||||
|
||||
$("#nav-tab [data-tab]").click(function () {
|
||||
const self = $(this)
|
||||
setHashParam("tab", self.data("tab"))
|
||||
|
||||
if (self.data("tab") === "values") {
|
||||
$("#userDefinedVals").parent().show()
|
||||
} else {
|
||||
$("#userDefinedVals").parent().hide()
|
||||
}
|
||||
|
||||
const flag = getHashParam("udv") === "true";
|
||||
$("#userDefinedVals").prop("checked", flag)
|
||||
|
||||
if (self.data("tab") === "resources") {
|
||||
showResources(getHashParam("namespace"), getHashParam("chart"), getHashParam("revision"))
|
||||
} else {
|
||||
const mode = getHashParam("mode")
|
||||
if (!mode) {
|
||||
$("#modePanel [data-mode=diff-prev]").trigger('click')
|
||||
} else {
|
||||
$("#modePanel [data-mode=" + mode + "]").trigger('click')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$("#modePanel [data-mode]").click(function () {
|
||||
const self = $(this)
|
||||
const mode = self.data("mode")
|
||||
setHashParam("mode", mode)
|
||||
loadContentWrapper()
|
||||
})
|
||||
|
||||
|
||||
$("#userDefinedVals").change(function () {
|
||||
const self = $(this)
|
||||
const flag = $("#userDefinedVals").prop("checked");
|
||||
setHashParam("udv", flag)
|
||||
loadContentWrapper()
|
||||
})
|
||||
|
||||
function loadContentWrapper() {
|
||||
let revDiff = 0
|
||||
const revision = parseInt(getHashParam("revision"));
|
||||
if (getHashParam("mode") === "diff-prev") {
|
||||
revDiff = revision - 1
|
||||
} else if (getHashParam("mode") === "diff-rev") {
|
||||
revDiff = $("#specRev").val()
|
||||
}
|
||||
const flag = $("#userDefinedVals").prop("checked");
|
||||
loadContent(getHashParam("tab"), getHashParam("namespace"), getHashParam("chart"), revision, revDiff, flag)
|
||||
}
|
||||
|
||||
function loadContent(mode, namespace, name, revision, revDiff, flag) {
|
||||
let qstr = "chart=" + name + "&namespace=" + namespace + "&revision=" + revision
|
||||
if (revDiff) {
|
||||
qstr += "&revisionDiff=" + revDiff
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
qstr += "&flag=" + flag
|
||||
}
|
||||
|
||||
let url = "/api/helm/charts/" + mode
|
||||
url += "?" + qstr
|
||||
const diffDisplay = $("#manifestText");
|
||||
diffDisplay.empty().append("<i class='fa fa-spinner fa-spin fa-2x'></i>")
|
||||
$.get(url).fail(function () {
|
||||
reportError("Failed to get diff of " + mode)
|
||||
}).done(function (data) {
|
||||
diffDisplay.empty();
|
||||
if (data === "") {
|
||||
diffDisplay.text("No differences to display")
|
||||
} else {
|
||||
if (revDiff) {
|
||||
const targetElement = document.getElementById('manifestText');
|
||||
const configuration = {
|
||||
inputFormat: 'diff', outputFormat: 'side-by-side',
|
||||
|
||||
drawFileList: false, showFiles: false, highlight: true, //matching: 'lines',
|
||||
};
|
||||
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
||||
diff2htmlUi.draw()
|
||||
} else {
|
||||
data = hljs.highlight(data, {language: 'yaml'}).value
|
||||
const code = $("#manifestText").empty().append("<pre class='bg-white rounded p-3'></pre>").find("pre");
|
||||
code.html(data)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$('#specRev').keyup(function (event) {
|
||||
let keycode = (event.keyCode ? event.keyCode : event.which);
|
||||
if (keycode == '13') {
|
||||
$("#diffModeRev").click()
|
||||
}
|
||||
event.preventDefault()
|
||||
});
|
||||
|
||||
function loadChartHistory(namespace, name) {
|
||||
$("#sectionDetails").show()
|
||||
$("#sectionDetails h1 span.name").text(name)
|
||||
revRow.empty().append("<div><i class='fa fa-spinner fa-spin fa-2x'></i></div>")
|
||||
$.getJSON("/api/helm/charts/history?chart=" + name + "&namespace=" + namespace).fail(function () {
|
||||
reportError("Failed to get chart details")
|
||||
}).done(function (data) {
|
||||
revRow.empty()
|
||||
for (let x = 0; x < data.length; x++) {
|
||||
const elm = data[x]
|
||||
$("#specRev").val(elm.revision)
|
||||
const rev = $(`<div class="col-md-2 p-2 rounded border border-secondary bg-gradient bg-white">
|
||||
<span><b class="rev-number"></b> - <span class="rev-status"></span></span><br/>
|
||||
<span class="text-muted">Chart:</span> <span class="chart-ver"></span><br/>
|
||||
<span class="text-muted">App ver:</span> <span class="app-ver"></span><br/>
|
||||
<p class="small mt-3 mb-0"><span class="text-muted">Age:</span> <span class="rev-age"></span><br/>
|
||||
<span class="text-muted rev-date"></span><br/></p>
|
||||
</div>`)
|
||||
rev.find(".rev-number").text("#" + elm.revision)
|
||||
rev.find(".app-ver").text(elm.app_version)
|
||||
rev.find(".chart-ver").text(elm.chart_ver)
|
||||
rev.find(".rev-date").text(elm.updated.replace("T", " "))
|
||||
rev.find(".rev-age").text(getAge(elm, data[x + 1]))
|
||||
rev.find(".rev-status").text(elm.status)
|
||||
rev.find(".fa").attr("title", elm.action)
|
||||
|
||||
if (elm.status === "failed") {
|
||||
rev.find(".rev-status").parent().addClass("text-danger")
|
||||
}
|
||||
|
||||
switch (elm.action) {
|
||||
case "app_upgrade":
|
||||
rev.find(".app-ver").append(" <i class='fa fa-angle-double-up text-success'></i>")
|
||||
break
|
||||
case "app_downgrade":
|
||||
rev.find(".app-ver").append(" <i class='fa fa-angle-double-down text-danger'></i>")
|
||||
break
|
||||
case "chart_upgrade":
|
||||
rev.find(".chart-ver").append(" <i class='fa fa-angle-up text-success'></i>")
|
||||
break
|
||||
case "chart_downgrade":
|
||||
rev.find(".chart-ver").append(" <i class='fa fa-angle-down text-danger'></i>")
|
||||
break
|
||||
case "reconfigure": // ?
|
||||
break
|
||||
}
|
||||
|
||||
rev.data("elm", elm)
|
||||
rev.addClass("rev-" + elm.revision)
|
||||
rev.click(function () {
|
||||
revisionClicked(namespace, name, $(this))
|
||||
})
|
||||
|
||||
revRow.append(rev)
|
||||
}
|
||||
|
||||
const rev = getHashParam("revision")
|
||||
if (rev) {
|
||||
revRow.find(".rev-" + rev).click()
|
||||
} else {
|
||||
revRow.find("div.col-md-2:last-child").click()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getHashParam(name) {
|
||||
const params = new URLSearchParams(window.location.hash.substring(1))
|
||||
return params.get(name)
|
||||
}
|
||||
|
||||
function setHashParam(name, val) {
|
||||
const params = new URLSearchParams(window.location.hash.substring(1))
|
||||
params.set(name, val)
|
||||
window.location.hash = new URLSearchParams(params).toString()
|
||||
}
|
||||
|
||||
function loadChartsList() {
|
||||
$("#sectionList").show()
|
||||
chartsCards.empty().append("<div><i class='fa fa-spinner fa-spin fa-2x'></i> Loading...</div>")
|
||||
$.getJSON("/api/helm/charts").fail(function () {
|
||||
reportError("Failed to get list of charts")
|
||||
}).done(function (data) {
|
||||
chartsCards.empty()
|
||||
data.forEach(function (elm) {
|
||||
const header = $("<div class='card-header'></div>")
|
||||
header.append($('<div class="float-end"><h5 class="float-end text-muted text-end">#' + elm.revision + '</h5><br/><div class="badge">' + elm.status + "</div>"))
|
||||
// TODO: for pending- and uninstalling, add the spinner
|
||||
if (elm.status === "failed") {
|
||||
header.find(".badge").addClass("bg-danger text-light")
|
||||
} else if (elm.status === "deployed" || elm.status === "superseded") {
|
||||
header.find(".badge").addClass("bg-info")
|
||||
} else {
|
||||
header.find(".badge").addClass("bg-light text-dark")
|
||||
}
|
||||
|
||||
header.append($('<h5 class="card-title"><a href="#namespace=' + elm.namespace + '&chart=' + elm.name + '" class="link-dark" style="text-decoration: none">' + elm.name + '</a></h5>'))
|
||||
header.append($('<p class="card-text small text-muted"></p>').append("Chart: " + elm.chart))
|
||||
|
||||
const body = $("<div class='card-body'></div>")
|
||||
body.append($('<p class="card-text"></p>').append("Namespace: " + elm.namespace))
|
||||
body.append($('<p class="card-text"></p>').append("Version: " + elm.app_version))
|
||||
body.append($('<p class="card-text"></p>').append("Updated: " + elm.updated))
|
||||
|
||||
let card = $("<div class='card'></div>").append(header).append(body);
|
||||
|
||||
card.data("chart", elm)
|
||||
card.click(function () {
|
||||
const self = $(this)
|
||||
$("#sectionList").hide()
|
||||
|
||||
let chart = self.data("chart");
|
||||
setHashParam("namespace", chart.namespace)
|
||||
setHashParam("chart", chart.name)
|
||||
loadChartHistory(chart.namespace, chart.name)
|
||||
})
|
||||
|
||||
chartsCards.append($("<div class='col'></div>").append(card))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
$(function () {
|
||||
// cluster list
|
||||
clusterSelect.change(function () {
|
||||
Cookies.set("context", clusterSelect.val())
|
||||
window.location.href = "/"
|
||||
let limNS = null
|
||||
$.getJSON("/status").fail(function (xhr) { // maybe /options call in the future
|
||||
reportError("Failed to get tool version", xhr)
|
||||
}).done(function (data) {
|
||||
$("body").data("status", data)
|
||||
fillToolVersion(data)
|
||||
limNS = data.LimitedToNamespace
|
||||
if (limNS) {
|
||||
$("#limitNamespace").show().find("span").text(limNS)
|
||||
}
|
||||
fillClusters(limNS)
|
||||
|
||||
if (data.ClusterMode) {
|
||||
$(".bi-power").hide()
|
||||
$("#clusterFilterBlock").hide()
|
||||
}
|
||||
})
|
||||
|
||||
$.getJSON("/api/kube/contexts").fail(function () {
|
||||
reportError("Failed to get list of clusters")
|
||||
$.getJSON("/api/scanners").fail(function (xhr) {
|
||||
reportError("Failed to get list of scanners", xhr)
|
||||
}).done(function (data) {
|
||||
const context = Cookies.get("context")
|
||||
|
||||
data.forEach(function (elm) {
|
||||
// aws CLI uses complicated context names, the suffix does not work well
|
||||
// maybe we should have an `if` statement here
|
||||
let label = elm.Name //+ " (" + elm.Cluster + "/" + elm.AuthInfo + "/" + elm.Namespace + ")"
|
||||
let opt = $("<option></option>").val(elm.Name).text(label)
|
||||
if (elm.IsCurrent && !context) {
|
||||
opt.attr("selected", "selected")
|
||||
} else if (context && elm.Name === context) {
|
||||
opt.attr("selected", "selected")
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'x-kubecontext': context
|
||||
}
|
||||
});
|
||||
$("body").data("scanners", data)
|
||||
for (let k in data) {
|
||||
if (data[k].ManifestScannable) {
|
||||
$("#upgradeModal .btn-scan").show() // TODO: move this to install flow
|
||||
}
|
||||
clusterSelect.append(opt)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function fillClusters(limNS) {
|
||||
const clusterSelect = $("#cluster");
|
||||
clusterSelect.change(function () {
|
||||
window.location.href = "/#context=" + clusterSelect.find("input:radio:checked").val()
|
||||
window.location.reload()
|
||||
})
|
||||
const namespaceSelect = $("#namespace");
|
||||
namespaceSelect.change(function () {
|
||||
let filteredNamespaces = []
|
||||
namespaceSelect.find("input:checkbox:checked").each(function () {
|
||||
filteredNamespaces.push($(this).val());
|
||||
})
|
||||
setFilteredNamespaces(filteredNamespaces)
|
||||
filterInstalledList($("#installedList .body .row"))
|
||||
})
|
||||
|
||||
$.getJSON("/api/k8s/contexts").fail(function (xhr) {
|
||||
sendStats('contexts', {'status': 'fail'});
|
||||
reportError("Failed to get list of clusters", xhr)
|
||||
}).done(function (data) {
|
||||
$("body").data("contexts", data)
|
||||
const context = getHashParam("context")
|
||||
data.sort((a, b) => (getCleanClusterName(a.Name) > getCleanClusterName(b.Name)) - (getCleanClusterName(a.Name) < getCleanClusterName(b.Name)))
|
||||
fillClusterList(data, context);
|
||||
sendStats('contexts', {'status': 'success', length: data.length});
|
||||
$.getJSON("/api/k8s/namespaces/list").fail(function (xhr) {
|
||||
reportError("Failed to get namespaces", xhr)
|
||||
}).done(function (res) {
|
||||
const ns = res.items.map(i => i.metadata.name)
|
||||
$.each(ns, function (i, item) {
|
||||
$("#upgradeModal #ns-datalist").append($("<option>", {
|
||||
value: item,
|
||||
text: item
|
||||
}))
|
||||
})
|
||||
if (!limNS) {
|
||||
fillNamespaceList(res.items)
|
||||
}
|
||||
}).always(function () {
|
||||
initView(); // can only do it after loading cluster and namespace lists
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$(".section").hide()
|
||||
|
||||
const section = getHashParam("section")
|
||||
if (section === "repository") {
|
||||
$("#topNav ul a.section-repo").addClass("active")
|
||||
loadRepoView()
|
||||
} else {
|
||||
$("#topNav ul a.section-installed").addClass("active")
|
||||
const namespace = getHashParam("namespace")
|
||||
const chart = getHashParam("chart")
|
||||
if (!chart) {
|
||||
@@ -286,9 +89,160 @@ $(function () {
|
||||
} else {
|
||||
loadChartHistory(namespace, chart)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$("#topNav ul a").click(function () {
|
||||
const self = $(this)
|
||||
if (self.hasClass("section-help")) {
|
||||
return;
|
||||
}
|
||||
|
||||
$("#topNav ul a").removeClass("active")
|
||||
|
||||
const ctx = getHashParam("context")
|
||||
const filteredNamespace = getHashParam("filteredNamespace")
|
||||
setHashParam(null, null)
|
||||
setHashParam("context", ctx)
|
||||
setHashParam("filteredNamespace", filteredNamespace)
|
||||
|
||||
if (self.hasClass("section-repo")) {
|
||||
setHashParam("section", "repository")
|
||||
} else if (self.hasClass("section-installed")) {
|
||||
setHashParam("section", null)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
initView()
|
||||
})
|
||||
|
||||
const errAlert = document.getElementById('errorAlert')
|
||||
errAlert.addEventListener('close.bs.alert', event => {
|
||||
event.preventDefault()
|
||||
$("#errorAlert").hide()
|
||||
})
|
||||
|
||||
function reportError(err, xhr) {
|
||||
$("#errorAlert h4 span").text(err)
|
||||
if (xhr) {
|
||||
$("#errorAlert p").text(xhr.responseText)
|
||||
}
|
||||
$("#errorAlert").show()
|
||||
}
|
||||
|
||||
|
||||
function getHashParam(name) {
|
||||
const params = new URLSearchParams(window.location.hash.substring(1))
|
||||
return params.get(name)
|
||||
}
|
||||
|
||||
function setHashParam(name, val) {
|
||||
let params = new URLSearchParams(window.location.hash.substring(1))
|
||||
if (!name) {
|
||||
params = new URLSearchParams()
|
||||
} else if (!val) {
|
||||
params.delete(name)
|
||||
} else {
|
||||
params.set(name, val)
|
||||
}
|
||||
window.location.hash = new URLSearchParams(params).toString()
|
||||
}
|
||||
|
||||
function statusStyle(status, card, txt) {
|
||||
txt.addClass("text-uppercase")
|
||||
txt.html("<span class='fs-6'>●</span> " + status)
|
||||
txt.removeClass("text-failed text-deployed text-pending text-other")
|
||||
if (status === "failed") {
|
||||
card.addClass("border-failed")
|
||||
txt.addClass("text-failed")
|
||||
// TODO: add failure description here
|
||||
} else if (status === "deployed") {
|
||||
card.addClass("border-deployed")
|
||||
txt.addClass("text-deployed")
|
||||
} else if (status.startsWith("pending-")) {
|
||||
card.addClass("border-pending")
|
||||
txt.addClass("text-pending")
|
||||
} else {
|
||||
card.addClass("border-other")
|
||||
txt.addClass("text-other")
|
||||
}
|
||||
}
|
||||
|
||||
function getCleanClusterName(rawClusterName) {
|
||||
if (rawClusterName.indexOf('arn') === 0) {
|
||||
// AWS cluster
|
||||
const clusterSplit = rawClusterName.split(':')
|
||||
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)
|
||||
const isCurrent = elm.IsCurrent && !context;
|
||||
const isSelected = context && elm.Name === context
|
||||
if (isCurrent || isSelected) {
|
||||
opt.find("input").prop("checked", true)
|
||||
setCurrentContext(elm.Name)
|
||||
}
|
||||
$("#cluster").append(opt)
|
||||
})
|
||||
}
|
||||
|
||||
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({
|
||||
headers: {
|
||||
'x-kubecontext': ctx
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getAge(obj1, obj2) {
|
||||
const date = luxon.DateTime.fromISO(obj1.updated);
|
||||
let dateNext = luxon.DateTime.now()
|
||||
@@ -310,50 +264,98 @@ function getAge(obj1, obj2) {
|
||||
return "n/a"
|
||||
}
|
||||
|
||||
function showResources(namespace, chart, revision) {
|
||||
$("#nav-resources").empty().append("<i class='fa fa-spin fa-spinner fa-2x'></i>");
|
||||
let qstr = "chart=" + chart + "&namespace=" + namespace + "&revision=" + revision
|
||||
let url = "/api/helm/charts/resources"
|
||||
url += "?" + qstr
|
||||
$.getJSON(url).fail(function () {
|
||||
reportError("Failed to get list of resources")
|
||||
}).done(function (data) {
|
||||
$("#nav-resources").empty();
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const res = data[i]
|
||||
const resBlock = $(`
|
||||
<div class="input-group row">
|
||||
<span class="input-group-text col-sm-2"><em class="text-muted small">` + res.kind + `</em></span>
|
||||
<span class="input-group-text col-sm-6">` + res.metadata.name + `</span>
|
||||
<span class="form-control col-sm-4"><i class="fa fa-spinner fa-spin"></i> <span class="text-muted small">Getting status...</span></span>
|
||||
</div>`)
|
||||
$("#nav-resources").append(resBlock)
|
||||
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
|
||||
$.getJSON("/api/kube/resources/" + res.kind.toLowerCase() + "?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
|
||||
//reportError("Failed to get list of resources")
|
||||
}).done(function (data) {
|
||||
const badge = $("<span class='badge me-2'></span>").text(data.status.phase);
|
||||
if (["Available", "Active", "Established"].includes(data.status.phase)) {
|
||||
badge.addClass("bg-success")
|
||||
} else if (["Exists"].includes(data.status.phase)) {
|
||||
badge.addClass("bg-success bg-opacity-50")
|
||||
} else {
|
||||
badge.addClass("bg-danger")
|
||||
}
|
||||
|
||||
resBlock.find(".form-control").empty().append(badge).append("<span class='text-muted small'>" + (data.status.message ? data.status.message : '') + "</span>")
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$(".fa-power-off").click(function () {
|
||||
$(".fa-power-off").attr("disabled", "disabled").removeClass(".fa-power-off").addClass("fa-spin fa-spinner")
|
||||
$(".bi-power").click(function () {
|
||||
$(".bi-power").attr("disabled", "disabled").removeClass(".bi-power").append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
$.ajax({
|
||||
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++) {
|
||||
const a = ~~newParts[i] // parse int
|
||||
const b = ~~oldParts[i] // parse int
|
||||
if (a > b) return true
|
||||
if (a < b) return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function fillToolVersion(data) {
|
||||
$("#toolVersion").text(data.CurVer)
|
||||
if (isNewerVersion(data.CurVer, data.LatestVer)) {
|
||||
$("#toolVersionUpgrade").text(data.LatestVer)
|
||||
$(".upgrade-possible").show()
|
||||
}
|
||||
}
|
||||
|
||||
$("#cacheClear").click(function () {
|
||||
$.ajax({
|
||||
url: "/api/cache",
|
||||
type: 'DELETE',
|
||||
}).done(function () {
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
|
||||
function showHideInstalledRelease(card, filteredNamespaces, filterStr) {
|
||||
let releaseNamespace = card.data("namespace")
|
||||
let releaseName = card.data("name")
|
||||
let chartName = card.data("chart").chart
|
||||
const shownByNS = !filteredNamespaces || filteredNamespaces.split('+').includes(releaseNamespace);
|
||||
const shownByStr = releaseName.indexOf(filterStr) >= 0 || chartName.indexOf(filterStr) >= 0
|
||||
if (shownByNS && shownByStr) {
|
||||
card.show()
|
||||
return true
|
||||
} else {
|
||||
card.hide()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function filterInstalledList(list) {
|
||||
const warnMsg = $("#installedList .all-filtered").hide();
|
||||
|
||||
let filterStr = $("#installedSearch").val().toLowerCase();
|
||||
let filteredNamespaces = getHashParam("filteredNamespace")
|
||||
let anyShown = false;
|
||||
let installedCount = 0;
|
||||
|
||||
list.each(function (ix, card) {
|
||||
anyShown = showHideInstalledRelease($(card), filteredNamespaces, filterStr)
|
||||
if (anyShown) {
|
||||
installedCount++;
|
||||
}
|
||||
})
|
||||
|
||||
$("#installedList .header h2 span").text(installedCount)
|
||||
if (list.length && !installedCount) {
|
||||
warnMsg.show()
|
||||
}
|
||||
}
|
||||
|
||||
function setFilteredNamespaces(filteredNamespaces) {
|
||||
if (filteredNamespaces.length === 0 && getHashParam("filteredNamespace")) {
|
||||
setHashParam("filteredNamespace")
|
||||
} else if (filteredNamespaces.length !== 0) {
|
||||
setHashParam("filteredNamespace", filteredNamespaces.join('+'))
|
||||
}
|
||||
}
|
||||
|
||||
const KomodorCTALink="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash"
|
||||
81
pkg/dashboard/static/styles-base.css
Normal 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;
|
||||
}
|
||||
@@ -1,11 +1,303 @@
|
||||
#charts .card, #sectionDetails .row > div {
|
||||
cursor: pointer;
|
||||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.bg-primary .text-muted {
|
||||
color: white!important;
|
||||
body {
|
||||
font-size: 14px;
|
||||
|
||||
background-color: #F4F7FA;
|
||||
font-family: Roboto, serif;
|
||||
color: #3D4048;
|
||||
}
|
||||
|
||||
body.bg-variant1 {
|
||||
background-color: #F4F7FA;
|
||||
background-image: url("topographic.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom left;
|
||||
background-size: auto 100%;
|
||||
}
|
||||
|
||||
body.bg-variant2 {
|
||||
background-color: #E8EDF2;
|
||||
}
|
||||
|
||||
body > .container-fluid {
|
||||
min-height: 100% !important;
|
||||
}
|
||||
|
||||
.navbar-brand > a > img {
|
||||
vertical-align: middle;
|
||||
height: 3rem;
|
||||
display: inline-block;
|
||||
margin: 0.25rem 0.75rem
|
||||
}
|
||||
|
||||
.navbar-brand > div {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.navbar-brand h1 a {
|
||||
font-size: 1.2rem !important;
|
||||
color: #0023A3 !important;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#topNav .navbar i.btn {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.d2h-file-collapse, .d2h-tag {
|
||||
opacity: 0; /* trollface */
|
||||
}
|
||||
|
||||
.display-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.separator-vertical span {
|
||||
font-size: 2rem;
|
||||
border-left: 1px solid #DCDDDF;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
#topNav .nav-link {
|
||||
color: #3B3D45 !important;
|
||||
}
|
||||
|
||||
#topNav .nav-link.text-danger {
|
||||
color: #FC1683 !important;
|
||||
}
|
||||
|
||||
#topNav .nav-link.active {
|
||||
background: #EBEFFF;
|
||||
border-radius: 2px;
|
||||
color: #1347FF !important;
|
||||
}
|
||||
|
||||
.b-shadow {
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
#filters h4 {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#filters {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.8rem;
|
||||
line-height: 175%;
|
||||
inline-size: auto;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
#cluster input, #cluster span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#installedList > div, #installedList .body > div {
|
||||
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;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.bg-secondary {
|
||||
background: #ECEFF2 !important;
|
||||
}
|
||||
|
||||
.border-secondary {
|
||||
border-color: #DCDDDF !important;
|
||||
}
|
||||
|
||||
#installedList .header {
|
||||
font-family: Roboto, serif;
|
||||
font-weight: 600;
|
||||
font-size: 0.6rem;
|
||||
color: #3B3D45;
|
||||
}
|
||||
|
||||
#installedList .header .row {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#installedList .hdr-name {
|
||||
padding-left: 5.5rem
|
||||
}
|
||||
|
||||
#installedList .body {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
#installedList .body .row div div {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
span.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#installedList .body .row:hover 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");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
background-position-x: 1.1rem;
|
||||
background-size: 3rem;
|
||||
}
|
||||
|
||||
#installedList .rel-name div {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#installedList .rel-name span {
|
||||
font-family: Roboto Slab, sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
#sectionDetails h1 {
|
||||
font-family: Roboto Slab, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
|
||||
#installedList .rel-name div {
|
||||
height: 2rem;
|
||||
min-height: 2rem;
|
||||
max-height: 2rem;
|
||||
}
|
||||
|
||||
#installedList .rel-status span {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#installedList .rel-chart span, #installedList .rel-rev span, #installedList .rel-ns span, #installedList .rel-date span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
#installedList .rel-chart div, #installedList .rel-rev div, #installedList .rel-ns div, #installedList .rel-date div {
|
||||
text-transform: uppercase;
|
||||
color: #707583;
|
||||
}
|
||||
|
||||
#actionButtons .link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#actionButtons button > * {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.d2h-code-side-linenumber {
|
||||
position: static;
|
||||
}
|
||||
|
||||
nav .nav-tabs {
|
||||
border: none;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
nav .nav-tabs .nav-link {
|
||||
padding-bottom: 0.25rem;
|
||||
color: #3B3D45;
|
||||
}
|
||||
|
||||
nav .nav-tabs .nav-link.active {
|
||||
border: none;
|
||||
border-bottom: 3px solid #3B3D45;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#installedList .body .b-shadow:hover {
|
||||
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
#btnUpgradeCheck {
|
||||
color: #3B3D45;
|
||||
}
|
||||
|
||||
#btnUpgrade {
|
||||
min-width: 9rem;
|
||||
}
|
||||
|
||||
#sectionDetails > .bg-white {
|
||||
background-color: #F4F7FA !important;
|
||||
}
|
||||
|
||||
#sectionDetails .list-unstyled .bg-secondary {
|
||||
background-color: #F4F7FA !important;
|
||||
}
|
||||
|
||||
#nav-resources .badge {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
#nav-resources .bg-secondary {
|
||||
background-color: #E6E7EB !important;
|
||||
}
|
||||
|
||||
.res-actions .btn-sm {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.offcanvas-header h5 {
|
||||
font-family: Poppins, serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.offcanvas-header h5 .badge {
|
||||
font-family: Roboto, serif;
|
||||
font-weight: normal;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.offcanvas-header p {
|
||||
font-family: Inter, serif;
|
||||
}
|
||||
|
||||
#describeModalBody pre {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#sectionRepo .repo-details ul .row .btn {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#sectionRepo .repo-details ul .row:hover {
|
||||
background-color: #F4F7FA !important;
|
||||
}
|
||||
|
||||
#sectionRepo .repo-details ul .row:hover .btn {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.test-result {
|
||||
font-size: 1rem;
|
||||
}
|
||||
15
pkg/dashboard/static/topographic.svg
Normal file
|
After Width: | Height: | Size: 77 KiB |
17
pkg/dashboard/subproc/scan.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package subproc
|
||||
|
||||
type Scanner interface {
|
||||
Name() string // returns string label for the scanner
|
||||
Test() bool // test if the scanner is available
|
||||
ScanManifests(mnf string) (*ScanResults, error) // run the scanner on manifests
|
||||
ScanResource(ns string, kind string, name string) (*ScanResults, error) // run the scanner on k8s resource
|
||||
SupportedResourceKinds() []string
|
||||
ManifestIsScannable() bool
|
||||
}
|
||||
|
||||
type ScanResults struct {
|
||||
PassedCount int
|
||||
FailedCount int
|
||||
OrigReport interface{}
|
||||
Error error
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ControlChan = chan struct{}
|
||||
|
||||
func chartAndVersion(x string) (string, string, error) {
|
||||
lastInd := strings.LastIndex(x, "-")
|
||||
if lastInd < 0 {
|
||||
return "", "", errors.New("can't parse chart version string")
|
||||
}
|
||||
|
||||
return x[:lastInd], x[lastInd+1:], nil
|
||||
}
|
||||
119
pkg/dashboard/utils/utils.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var FailLogLevel = log.WarnLevel // allows to suppress error logging in some situations
|
||||
|
||||
type ControlChan = chan struct{}
|
||||
|
||||
func ChartAndVersion(x string) (string, string, error) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func TempFile(txt string) (string, func(), error) {
|
||||
file, err := ioutil.TempFile("", "helm_dahsboard_*.yaml")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(file.Name(), []byte(txt), 0600)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return file.Name(), func() { _ = os.Remove(file.Name()) }, nil
|
||||
}
|
||||
|
||||
type CmdError struct {
|
||||
Command []string
|
||||
OrigError error
|
||||
StdErr string
|
||||
}
|
||||
|
||||
func (e CmdError) Error() string {
|
||||
//return fmt.Sprintf("failed to run command %s:\nError: %s\nSTDERR:%s", e.Command, e.OrigError, e.StdErr)
|
||||
return string(e.StdErr)
|
||||
}
|
||||
|
||||
func RunCommand(cmd []string, env map[string]string) (string, error) {
|
||||
log.Debugf("Starting command: %s", cmd)
|
||||
prog := exec.Command(cmd[0], cmd[1:]...)
|
||||
prog.Env = os.Environ()
|
||||
|
||||
for k, v := range env {
|
||||
prog.Env = append(prog.Env, k+"="+v)
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
prog.Stdout = &stdout
|
||||
|
||||
var stderr bytes.Buffer
|
||||
prog.Stderr = &stderr
|
||||
|
||||
if err := prog.Run(); err != nil {
|
||||
log.StandardLogger().Logf(FailLogLevel, "Failed command: %s", cmd)
|
||||
serr := stderr.Bytes()
|
||||
if serr != nil {
|
||||
log.StandardLogger().Logf(FailLogLevel, "STDERR:\n%s", serr)
|
||||
}
|
||||
if eerr, ok := err.(*exec.ExitError); ok {
|
||||
return "", CmdError{
|
||||
Command: cmd,
|
||||
StdErr: string(serr),
|
||||
OrigError: eerr,
|
||||
}
|
||||
}
|
||||
|
||||
return "", CmdError{
|
||||
Command: cmd,
|
||||
StdErr: string(serr),
|
||||
OrigError: err,
|
||||
}
|
||||
}
|
||||
|
||||
sout := stdout.Bytes()
|
||||
serr := stderr.Bytes()
|
||||
log.Debugf("Command STDOUT:\n%s", sout)
|
||||
log.Debugf("Command STDERR:\n%s", serr)
|
||||
return string(sout), nil
|
||||
}
|
||||
|
||||
type QueryProps struct {
|
||||
Namespace string
|
||||
Name string
|
||||
}
|
||||
|
||||
func GetQueryProps(c *gin.Context) (*QueryProps, error) {
|
||||
qp := QueryProps{}
|
||||
|
||||
qp.Namespace = c.Query("namespace")
|
||||
qp.Name = c.Query("name")
|
||||
if qp.Name == "" {
|
||||
return nil, errors.New("missing required query string parameter: name")
|
||||
}
|
||||
|
||||
return &qp, nil
|
||||
}
|
||||
108
pkg/dashboard/utils/utils_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
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
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Get query props - all set with revRequired true",
|
||||
wantErr: false,
|
||||
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing&revision=1",
|
||||
},
|
||||
{
|
||||
name: "Get query props - no namespace with revRequired true",
|
||||
wantErr: false,
|
||||
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&revision=1",
|
||||
},
|
||||
{
|
||||
name: "Get query props - no name with revRequired true",
|
||||
wantErr: 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)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
11
plugin.yaml
@@ -1,8 +1,9 @@
|
||||
name: "dashboard"
|
||||
version: "0.0.0"
|
||||
version: "1.1.1"
|
||||
usage: "A simplified way of working with Helm"
|
||||
description: "View HELM situation in nice web UI"
|
||||
command: "$HELM_PLUGIN_DIR/bin/dashboard"
|
||||
#hooks:
|
||||
# install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
|
||||
# update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh"
|
||||
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"
|
||||
|
||||
BIN
screenshot.png
|
Before Width: | Height: | Size: 210 KiB |
77
scripts/install_plugin.sh
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
# Copied w/ love from the chartmuseum/helm-push :)
|
||||
|
||||
[ ! -z "$HELM_DEBUG" ] && set -x
|
||||
|
||||
name="helm-dashboard"
|
||||
repo="https://github.com/komodorio/${name}"
|
||||
api_repo="https://api.github.com/repos/komodorio/${name}/releases/latest"
|
||||
|
||||
if [ -n "${HELM_PUSH_PLUGIN_NO_INSTALL_HOOK}" ]; then
|
||||
echo "Development mode: not downloading versioned release."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
version="$(curl -s ${api_repo} | grep '\"name\": "v.*\"' | cut -d 'v' -f 2 | cut -d '"' -f 1)"
|
||||
echo Tried to autodetect latest version: $version
|
||||
[ -z "$version" ] && {
|
||||
version="$(cat plugin.yaml | grep "version" | cut -d '"' -f 2)"
|
||||
echo Defaulted to version: $version
|
||||
}
|
||||
echo "Downloading and installing ${name} v${version} ..."
|
||||
|
||||
url=""
|
||||
|
||||
# convert architecture of the target system to a compatible GOARCH value.
|
||||
# Otherwise failes to download of the plugin from github, because the provided
|
||||
# architecture by `uname -m` is not part of the github release.
|
||||
arch=""
|
||||
case $(uname -m) in
|
||||
x86_64)
|
||||
arch="x86_64"
|
||||
;;
|
||||
armv6*)
|
||||
arch="armv6"
|
||||
;;
|
||||
# match every arm processor version like armv7h, armv7l and so on.
|
||||
armv7*)
|
||||
arch="armv7"
|
||||
;;
|
||||
aarch64 | arm64)
|
||||
arch="arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Failed to detect target architecture"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
url="${repo}/releases/download/v${version}/${name}_${version}_Darwin_${arch}.tar.gz"
|
||||
elif [ "$(uname)" = "Linux" ] ; then
|
||||
url="${repo}/releases/download/v${version}/${name}_${version}_Linux_${arch}.tar.gz"
|
||||
else
|
||||
url="${repo}/releases/download/v${version}/${name}_${version}_windows_${arch}.tar.gz"
|
||||
fi
|
||||
|
||||
echo $url
|
||||
|
||||
mkdir -p "bin"
|
||||
mkdir -p "releases/v${version}"
|
||||
|
||||
# Download with curl if possible.
|
||||
if [ -x "$(which curl 2>/dev/null)" ]; then
|
||||
curl --fail -sSL "${url}" -o "releases/v${version}.tar.gz"
|
||||
else
|
||||
wget -q "${url}" -O "releases/v${version}.tar.gz"
|
||||
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
|
||||