Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b61adf133f | ||
|
|
27eb7949e5 | ||
|
|
b90198915e | ||
|
|
64975cac42 | ||
|
|
087399ad49 | ||
|
|
fc385344f4 | ||
|
|
56932f2c34 | ||
|
|
24df4a21d6 | ||
|
|
bea75cb011 | ||
|
|
a07c8f273d | ||
|
|
eb11a8f26e | ||
|
|
f0545d35f1 | ||
|
|
57f7c47dd1 | ||
|
|
0b4031bf24 | ||
|
|
e143963d46 | ||
|
|
b933e2dd9b | ||
|
|
0e15fe2001 | ||
|
|
021fe9c897 | ||
|
|
5f6104dbba | ||
|
|
8e9a464d62 | ||
|
|
3a7bb3efb6 | ||
|
|
d2259241e6 | ||
|
|
aad9992302 | ||
|
|
30eb209043 | ||
|
|
1dcb77812f | ||
|
|
245863b2f9 | ||
|
|
dd1fe05d65 | ||
|
|
450804ba24 | ||
|
|
a2ddb94c16 | ||
|
|
861de33bfe | ||
|
|
26d82dd5ab | ||
|
|
b1294cbe1a | ||
|
|
d4583a222e | ||
|
|
a0bf59edc6 | ||
|
|
79a79979e2 | ||
|
|
76e4fe51b5 | ||
|
|
95ea5e4d6d | ||
|
|
c139f3941d | ||
|
|
80022c3ef8 | ||
|
|
a07cfcdbb4 | ||
|
|
8826124f70 | ||
|
|
703b4029de | ||
|
|
a2dc1ed96b | ||
|
|
29c1682bbb | ||
|
|
c7d18a7fb7 | ||
|
|
e9ee10287b | ||
|
|
57d4d073e9 | ||
|
|
47dae4d35a | ||
|
|
0ac8eec368 | ||
|
|
aec46d43f7 | ||
|
|
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 |
@@ -1,4 +1,5 @@
|
|||||||
Dockerfile
|
Dockerfile
|
||||||
*.md
|
*.md
|
||||||
bin
|
bin
|
||||||
.idea
|
.idea
|
||||||
|
dashboard/node_modules
|
||||||
30
.github/pull_request_template.md
vendored
@@ -1,14 +1,10 @@
|
|||||||
<!-- If your PR fixes an open issue, use `Closes #999` to link your PR with the issue. #999 stands for the issue number you are fixing -->
|
## Changes Proposed
|
||||||
|
|
||||||
## Fixes Issue
|
<!-- Describe the proposed changes and any additional information -->
|
||||||
|
|
||||||
<!-- Remove this section if not applicable -->
|
<!-- Add all the screenshots which illustrate your changes -->
|
||||||
|
|
||||||
<!-- Example: Closes #31 -->
|
## Check List
|
||||||
|
|
||||||
## Changes proposed
|
|
||||||
|
|
||||||
<!-- List all the proposed changes in your PR -->
|
|
||||||
|
|
||||||
<!-- Mark all the applicable boxes. To mark the box as done follow the following conventions -->
|
<!-- Mark all the applicable boxes. To mark the box as done follow the following conventions -->
|
||||||
<!--
|
<!--
|
||||||
@@ -18,18 +14,8 @@
|
|||||||
[ ] - Not correct; marked as **not** done
|
[ ] - Not correct; marked as **not** done
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Check List (Check all the applicable boxes) <!-- Follow the above conventions to check the box -->
|
- [ ] 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
|
||||||
|
|
||||||
- [ ] My code follows the code style of this project.
|
|
||||||
- [ ] My change requires changes to the documentation.
|
|
||||||
- [ ] I have updated the documentation accordingly.
|
|
||||||
- [ ] All new and existing tests passed.
|
|
||||||
- [ ] The title of my pull request is a short description of the requested changes.
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
<!-- Add all the screenshots which support your changes -->
|
|
||||||
|
|
||||||
## Note to reviewers
|
|
||||||
|
|
||||||
<!-- Add notes to reviewers if applicable -->
|
|
||||||
|
|||||||
46
.github/workflows/build.yml
vendored
@@ -2,9 +2,11 @@ name: Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: main
|
branches:
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: "*"
|
branches:
|
||||||
|
- "*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -15,10 +17,12 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: 1.18
|
go-version: "1.20"
|
||||||
- name: Unit tests
|
- name: Unit tests
|
||||||
run: |
|
run: |
|
||||||
go test -v -race ./... -covermode=atomic # Run all the tests with the race detector enabled
|
go test -v -race ./... -covermode=atomic -coverprofile=coverage.out # Run all the tests with the race detector enabled
|
||||||
|
- name: Upload coverage reports to Codecov
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
- name: Static analysis
|
- name: Static analysis
|
||||||
run: |
|
run: |
|
||||||
go vet ./... # go vet is the official Go static analyzer
|
go vet ./... # go vet is the official Go static analyzer
|
||||||
@@ -31,8 +35,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --snapshot --rm-dist
|
args: release --snapshot --rm-dist
|
||||||
- name: Test Binary is Runnable
|
- name: Test if the Binary is Runnable
|
||||||
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
|
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: binaries
|
||||||
|
path: dist/
|
||||||
|
retention-days: 1
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3.3.1
|
uses: golangci/golangci-lint-action@v3.3.1
|
||||||
with:
|
with:
|
||||||
@@ -49,12 +58,33 @@ jobs:
|
|||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
uses: docker/metadata-action@v3
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: komodorio/helm-dashboard
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USER }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASS }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
outputs: local
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: komodorio/helm-dashboard:unstable
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: VER=0.0.0-dev
|
build-args: VER=0.0.0-dev
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
helm_check:
|
helm_check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -68,6 +98,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CHART_LOCATION: ./charts/helm-dashboard
|
CHART_LOCATION: ./charts/helm-dashboard
|
||||||
CHART_VALUES: ./charts/helm-dashboard/values.yaml
|
CHART_VALUES: ./charts/helm-dashboard/values.yaml
|
||||||
- name: Test Helm plugin install script is runnable
|
- name: Test if the Helm plugin install script is runnable
|
||||||
run: |
|
run: |
|
||||||
scripts/install_plugin.sh
|
scripts/install_plugin.sh
|
||||||
|
|||||||
14
.github/workflows/release.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: 1.18
|
go-version: "1.20"
|
||||||
- name: git cleanup
|
- name: git cleanup
|
||||||
run: git clean -f
|
run: git clean -f
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
@@ -62,15 +62,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: komodorio/helm-dashboard
|
images: komodorio/helm-dashboard
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USER }}
|
username: ${{ secrets.DOCKERHUB_USER }}
|
||||||
password: ${{ secrets.DOCKERHUB_PASS }}
|
password: ${{ secrets.DOCKERHUB_PASS }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v4
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -78,6 +83,7 @@ jobs:
|
|||||||
tags: komodorio/helm-dashboard:${{ needs.pre_release.outputs.release_tag }},komodorio/helm-dashboard:latest
|
tags: komodorio/helm-dashboard:${{ needs.pre_release.outputs.release_tag }},komodorio/helm-dashboard:latest
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: VER=${{ needs.pre_release.outputs.release_tag }}
|
build-args: VER=${{ needs.pre_release.outputs.release_tag }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
publish_chart:
|
publish_chart:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -112,4 +118,4 @@ jobs:
|
|||||||
user_email: "komi@komodor.io"
|
user_email: "komi@komodor.io"
|
||||||
user_name: "komodor-bot"
|
user_name: "komodor-bot"
|
||||||
destination_branch: "master"
|
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
|
commit_msg: "feat(OSS helm-dashboard): ${{ github.event.head_commit.message }}" #important!! don't change this commit message unless you change the condition in pipeline.yml on helm-charts repo
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -28,3 +28,4 @@ go.work
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode/
|
.vscode/
|
||||||
|
/pkg/dashboard/objects/testdata/hello-world-0.1.0.tgz
|
||||||
|
|||||||
18
Dockerfile
@@ -1,9 +1,13 @@
|
|||||||
# Stage - builder
|
# Stage - builder
|
||||||
FROM golang as builder
|
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang as builder
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG BUILDPLATFORM
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
ENV GOOS=linux
|
ENV GOOS=${TARGETOS:-linux}
|
||||||
ENV GOARCH=amd64
|
ENV GOARCH=${TARGETARCH:-amd64}
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
@@ -23,7 +27,11 @@ WORKDIR /build/src
|
|||||||
RUN make build
|
RUN make build
|
||||||
|
|
||||||
# Stage - runner
|
# Stage - runner
|
||||||
FROM alpine
|
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG BUILDPLATFORM
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
@@ -34,7 +42,7 @@ RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/
|
|||||||
RUN trivy --version
|
RUN trivy --version
|
||||||
|
|
||||||
# Checkov scanner
|
# Checkov scanner
|
||||||
RUN pip3 install checkov packaging==21.3 && checkov --version
|
RUN (pip3 install checkov packaging==21.3 && checkov --version) || echo Failed to install optional Checkov
|
||||||
|
|
||||||
COPY --from=builder /build/src/bin/dashboard /bin/helm-dashboard
|
COPY --from=builder /build/src/bin/dashboard /bin/helm-dashboard
|
||||||
|
|
||||||
|
|||||||
57
FEATURES.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Shutting down the app
|
||||||
|
To close Helm-dashboard, click on the button in the rightmost corner of the screen. Once you click on it, your Helm-dashboard will be shut down.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Multicluster
|
||||||
|
If you want to switch to a different cluster, simply click on the corresponding cluster as shown in the figure. [Click here](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) to learn how to access multiple clusters.
|
||||||
|

|
||||||
|
|
||||||
|
# Repository
|
||||||
|
Essentially, a repository is a location where charts are gathered and can be shared. If you want to learn more about repositories, [click here](https://helm.sh/docs/topics/chart_repository/). You can find the repository in the home section, as depicted in the figure.
|
||||||
|

|
||||||
|
|
||||||
|
You can add the repository by clicking on 'Add Repository', as shown in the figure.
|
||||||
|

|
||||||
|
|
||||||
|
After completing that step, enter the following data: the repository name and its URL. You can also add the username and password, although this is optional.
|
||||||
|

|
||||||
|
|
||||||
|
Updating means refreshing your repository. You can update your repository as shown in the figure.
|
||||||
|

|
||||||
|
|
||||||
|
If you want to remove your repository from the Helm dashboard, click on the 'Remove' button as shown in the figure.
|
||||||
|

|
||||||
|
|
||||||
|
Use the filter option to find the desired chart quicker from the list of charts.
|
||||||
|

|
||||||
|
|
||||||
|
If you want to install a particular chart, simply hover the pointer over the chart name and an 'Install' button will appear, as shown in the figure.
|
||||||
|

|
||||||
|
|
||||||
|
# Installed Releases list
|
||||||
|
A release is an instance of your selected chart running on your Kubernetes Cluster. That means every time that you install a Helm chart there, it creates a new release or instance that coexists with other releases without conflict. You can filter releases based on namespaces or search for release names
|
||||||
|

|
||||||
|
|
||||||
|
The squares represent k8s resources installed by the release. Hover over each square to view a tooltip with details. Yellow indicates "pending," green signifies a healthy state, and red indicates an unhealthy state.
|
||||||
|

|
||||||
|
|
||||||
|
It indicates the version of chart that corresponds to this release.
|
||||||
|

|
||||||
|
|
||||||
|
A revision is linked to a release to track the number of updates/changes that release encounters.
|
||||||
|

|
||||||
|
|
||||||
|
Namespaces are a way to organize clusters into virtual sub-clusters — they can be helpful when different teams or projects share a Kubernetes cluster. Any number of namespaces are supported within a cluster, each logically separated from others but with the ability to communicate with each other.
|
||||||
|

|
||||||
|
|
||||||
|
Updated" refers to the amount of time that has passed since the last revision of the release. Whenever you install or upgrade the release, a new revision is created. You can think of it as the "age" of the latest revision.
|
||||||
|

|
||||||
|
|
||||||
|
# Release details
|
||||||
|
This indicates the status of the deployed release, and 'Age' represents the amount of time that has passed since the creation of the revision until now.
|
||||||
|

|
||||||
|
|
||||||
|
You can use the Upgrade/Downgrade button to switch to different release versions, as shown in the figure.
|
||||||
|

|
||||||
|
|
||||||
51
README.md
@@ -8,9 +8,11 @@
|
|||||||
|
|
||||||
<p align="center">A simplified way of working with Helm.</p>
|
<p align="center">A simplified way of working with Helm.</p>
|
||||||
|
|
||||||
 [](https://github.com/komodorio/helm-dashboard/issues)      [](https://github.com/komodorio/helm-dashboard)
|
|
||||||
|
|
||||||
<kbd>[<img src="screenshot.png" style="width: 100%; border: 1px solid silver;" border="1" alt="Screenshot">](screenshot.png)</kbd>
|
 [](https://github.com/komodorio/helm-dashboard/issues)    [](https://github.com/komodorio/helm-dashboard/releases)  [](https://github.com/komodorio/helm-dashboard) [](https://codecov.io/gh/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
|
## Description
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ _Helm Dashboard_ is an **open-source project** which offers a UI-driven way to v
|
|||||||
corresponding k8s resources. Also, you can perform simple actions like roll back to a revision or upgrade to newer
|
corresponding k8s resources. Also, you can perform simple actions like roll back to a revision or upgrade to newer
|
||||||
version.
|
version.
|
||||||
This project is part of [Komodor's](https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash-gh) vision of
|
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/).
|
helping Kubernetes users to navigate and troubleshoot their clusters, the project is **NOT** an official project by the [helm team](https://helm.sh/).
|
||||||
|
|
||||||
Key capabilities of the tool:
|
Key capabilities of the tool:
|
||||||
|
|
||||||
@@ -92,29 +94,44 @@ If you want to increase the logging verbosity and see all the debug info, use th
|
|||||||
|
|
||||||
The official helm chart is [available here](https://github.com/komodorio/helm-charts/blob/master/charts/helm-dashboard)
|
The official helm chart is [available here](https://github.com/komodorio/helm-charts/blob/master/charts/helm-dashboard)
|
||||||
|
|
||||||
|
## Selected Features
|
||||||
|
|
||||||
## Execute Helm tests
|
### Support for Local Charts
|
||||||
|
|
||||||
|
Local Helm chart is a directory with specially named files and a `Chart.yaml` file, which you can install via Helm, without the need to publish the chart into Helm repository. Chart developers might want to experiment with the chart locally, before uploading into public repository. Also, a proprietary application might only use non-published chart as an approach to deploy the software.
|
||||||
|
|
||||||
|
For all the above use-cases, you may use Helm Dashboard UI, specifying the location of your local chart folders via special `--local-chart` command-line parameter. The parameter might be specified multiple times, for example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
helm-dashboard --local-chart=/opt/charts/my-private-app --local-chart=/home/dev/sources/app/chart
|
||||||
|
```
|
||||||
|
|
||||||
|
When _valid_ local chart sources specified, the repository list would contain a surrogate `[local]` entry, with those charts listed inside. All the chart operations are normal: installing, reconfiguring and upgrading.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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.
|
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:
|
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:
|
The result of executed `helm test` for the release will be displayed as below:
|
||||||

|

|
||||||
|
|
||||||
## Scanner Integrations
|
### Scanner Integrations
|
||||||
|
|
||||||
Upon startup, Helm Dashboard detects the presence of [Trivy](https://github.com/aquasecurity/trivy)
|
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
|
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.
|
resources page, as well as install/upgrade preview page.
|
||||||
|
|
||||||
You can request scanning of the specific k8s resource in your cluster:
|
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"
|
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:
|
button at the bottom of the dialog:
|
||||||

|

|
||||||
|
|
||||||
## Support Channels
|
## Support Channels
|
||||||
|
|
||||||
@@ -134,15 +151,25 @@ Kindly read our [Contributing Guide](CONTRIBUTING.md) to learn and understand ab
|
|||||||
|
|
||||||
## Local Dev Testing
|
## Local Dev Testing
|
||||||
|
|
||||||
Prerequisites: `helm` and `kubectl` binaries installed and operational.
|
Prerequisites, binaries installed and operational:
|
||||||
|
|
||||||
|
- [Go](https://go.dev/doc/install)
|
||||||
|
|
||||||
There is a need to build binary for plugin to function, run:
|
There is a need to build binary for plugin to function, run:
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
go build -o bin/dashboard .
|
go build -o bin/dashboard .
|
||||||
```
|
```
|
||||||
|
|
||||||
You can just run the `bin/dashboard` binary directly, it will just work.
|
### Windows
|
||||||
|
|
||||||
|
```bat
|
||||||
|
go build -o bin\dashboard.exe .
|
||||||
|
```
|
||||||
|
|
||||||
|
You can just run the `dashboard` or `dashboard.exe` binary directly, it will just work.
|
||||||
|
|
||||||
To install, checkout the source code and run from source dir:
|
To install, checkout the source code and run from source dir:
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ name: helm-dashboard
|
|||||||
description: A GUI Dashboard for Helm by Komodor
|
description: A GUI Dashboard for Helm by Komodor
|
||||||
icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/main/pkg/dashboard/static/logo.svg"
|
icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/main/pkg/dashboard/static/logo.svg"
|
||||||
|
|
||||||
version: 0.1.2
|
version: 0.1.9
|
||||||
appVersion: "0.3.1"
|
appVersion: "1.3.2"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
```bash
|
```bash
|
||||||
helm repo add komodorio https://helm-charts.komodor.io
|
helm repo add komodorio https://helm-charts.komodor.io
|
||||||
helm repo update
|
helm repo update
|
||||||
helm upgrade --install my-release komodorio/helm-dashboard
|
helm upgrade --install helm-dashboard komodorio/helm-dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
@@ -17,14 +17,13 @@ While installed inside cluster, Helm Dashboard will run some additional backgrou
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Kubernetes 1.16+
|
- Kubernetes 1.16+
|
||||||
- Helm 3+
|
|
||||||
|
|
||||||
## Installing the Chart
|
## Installing the Chart
|
||||||
|
|
||||||
To install the chart with the release name `my-release`:
|
To install the chart with the release name `helm-dashboard`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
helm install my-release .
|
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.
|
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.
|
||||||
@@ -33,10 +32,10 @@ The command deploys Helm Dashboard on the Kubernetes cluster in the default conf
|
|||||||
|
|
||||||
## Uninstalling the Chart
|
## Uninstalling the Chart
|
||||||
|
|
||||||
To uninstall/delete the `my-release` deployment:
|
To uninstall/delete the `helm-dashboard` deployment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
helm uninstall my-release
|
helm uninstall helm-dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
The command removes all the Kubernetes components associated with the chart and deletes the release.
|
The command removes all the Kubernetes components associated with the chart and deletes the release.
|
||||||
@@ -80,7 +79,7 @@ The following table lists the configurable parameters of the chart and their def
|
|||||||
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`.
|
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
helm upgrade --install my-release komodorio/helm-dashboard --set dashboard.allowWriteActions=true --set service.port=9090
|
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)
|
> **Tip**: You can use the default [values.yaml](values.yaml)
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ spec:
|
|||||||
{{- if .Values.dashboard.persistence.hostPath }}
|
{{- if .Values.dashboard.persistence.hostPath }}
|
||||||
storageClassName: ""
|
storageClassName: ""
|
||||||
{{- else }}
|
{{- else }}
|
||||||
|
{{- if kindIs "string" .Values.dashboard.persistence.storageClass }}
|
||||||
storageClassName: "{{ .Values.dashboard.persistence.storageClass }}"
|
storageClassName: "{{ .Values.dashboard.persistence.storageClass }}"
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
accessModes:
|
accessModes:
|
||||||
{{- if not (empty .Values.dashboard.persistence.accessModes) }}
|
{{- if not (empty .Values.dashboard.persistence.accessModes) }}
|
||||||
{{- range .Values.dashboard.persistence.accessModes }}
|
{{- range .Values.dashboard.persistence.accessModes }}
|
||||||
@@ -42,7 +44,11 @@ metadata:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- {{ .Values.dashboard.persistence.accessMode | quote }}
|
{{- if not (empty .Values.dashboard.persistence.accessModes) }}
|
||||||
|
{{- range .Values.dashboard.persistence.accessModes }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
capacity:
|
capacity:
|
||||||
storage: {{ .Values.dashboard.persistence.size | quote }}
|
storage: {{ .Values.dashboard.persistence.size | quote }}
|
||||||
hostPath:
|
hostPath:
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ dashboard:
|
|||||||
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||||
## GKE, AWS & OpenStack)
|
## GKE, AWS & OpenStack)
|
||||||
##
|
##
|
||||||
storageClass: ""
|
storageClass: null
|
||||||
|
|
||||||
## Helm Dashboard Persistent Volume access modes
|
## Helm Dashboard Persistent Volume access modes
|
||||||
## Must match those of existing PV or dynamic provisioner
|
## Must match those of existing PV or dynamic provisioner
|
||||||
|
|||||||
156
go.mod
@@ -1,10 +1,11 @@
|
|||||||
module github.com/komodorio/helm-dashboard
|
module github.com/komodorio/helm-dashboard
|
||||||
|
|
||||||
go 1.18
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Masterminds/semver/v3 v3.2.1
|
||||||
github.com/eko/gocache/v3 v3.1.2
|
github.com/eko/gocache/v3 v3.1.2
|
||||||
github.com/gin-gonic/gin v1.8.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/hashicorp/go-version v1.6.0
|
github.com/hashicorp/go-version v1.6.0
|
||||||
github.com/hexops/gotextdiff v1.0.3
|
github.com/hexops/gotextdiff v1.0.3
|
||||||
github.com/jessevdk/go-flags v1.5.0
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
@@ -13,90 +14,98 @@ require (
|
|||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/rogpeppe/go-internal v1.8.0
|
github.com/rogpeppe/go-internal v1.10.0
|
||||||
github.com/sirupsen/logrus v1.9.0
|
github.com/sirupsen/logrus v1.9.2
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.4
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
helm.sh/helm/v3 v3.10.3
|
gotest.tools/v3 v3.4.0
|
||||||
k8s.io/api v0.26.0
|
helm.sh/helm/v3 v3.12.0
|
||||||
k8s.io/apimachinery v0.26.0
|
k8s.io/api v0.27.2
|
||||||
k8s.io/cli-runtime v0.26.0
|
k8s.io/apimachinery v0.27.2
|
||||||
k8s.io/client-go v0.26.0
|
k8s.io/cli-runtime v0.27.2
|
||||||
k8s.io/kubectl v0.26.0
|
k8s.io/client-go v0.27.2
|
||||||
|
k8s.io/kubectl v0.27.2
|
||||||
|
k8s.io/utils v0.0.0-20230505201702-9f6742963106
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
|
||||||
github.com/Masterminds/squirrel v1.5.3 // indirect
|
github.com/Masterminds/squirrel v1.5.3 // indirect
|
||||||
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // 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/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 // indirect
|
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||||
github.com/containerd/containerd v1.6.12 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/containerd/containerd v1.7.0 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/docker/cli v20.10.17+incompatible // indirect
|
github.com/docker/cli v20.10.21+incompatible // indirect
|
||||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||||
github.com/docker/docker v20.10.17+incompatible // indirect
|
github.com/docker/docker v20.10.24+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.6.4 // indirect
|
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-metrics v0.0.1 // indirect
|
github.com/docker/go-metrics v0.0.1 // indirect
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
|
||||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||||
github.com/fatih/camelcase v1.0.0 // indirect
|
github.com/fatih/camelcase v1.0.0 // indirect
|
||||||
github.com/fatih/color v1.13.0 // indirect
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
|
||||||
github.com/fvbommel/sortorder v1.0.1 // indirect
|
github.com/fvbommel/sortorder v1.0.1 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-errors/errors v1.0.1 // indirect
|
github.com/go-errors/errors v1.4.2 // indirect
|
||||||
github.com/go-gorp/gorp/v3 v3.0.2 // indirect
|
github.com/go-gorp/gorp/v3 v3.0.5 // indirect
|
||||||
github.com/go-logr/logr v1.2.3 // indirect
|
github.com/go-logr/logr v1.2.3 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||||
github.com/go-openapi/swag v0.19.14 // indirect
|
github.com/go-openapi/jsonreference v0.20.1 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-openapi/swag v0.22.3 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
github.com/gobwas/glob v0.2.3 // indirect
|
||||||
github.com/goccy/go-json v0.9.11 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/btree v1.0.1 // indirect
|
github.com/google/btree v1.0.1 // indirect
|
||||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/google/gofuzz v1.2.0 // indirect
|
github.com/google/gofuzz v1.2.0 // indirect
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
github.com/google/uuid v1.2.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gorilla/mux v1.8.0 // indirect
|
github.com/gorilla/mux v1.8.0 // indirect
|
||||||
github.com/gosuri/uitable v0.0.4 // indirect
|
github.com/gosuri/uitable v0.0.4 // indirect
|
||||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||||
github.com/huandu/xstrings v1.3.2 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/huandu/xstrings v1.4.0 // indirect
|
||||||
|
github.com/imdario/mergo v0.3.13 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.13.6 // indirect
|
github.com/klauspost/compress v1.16.0 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/lib/pq v1.10.6 // indirect
|
github.com/lib/pq v1.10.7 // indirect
|
||||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||||
github.com/mailru/easyjson v0.7.6 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
@@ -104,64 +113,65 @@ require (
|
|||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/moby/locker v1.0.1 // indirect
|
github.com/moby/locker v1.0.1 // indirect
|
||||||
github.com/moby/spdystream v0.2.0 // indirect
|
github.com/moby/spdystream v0.2.0 // indirect
|
||||||
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
|
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
|
||||||
github.com/pegasus-kv/thrift v0.13.0 // indirect
|
github.com/pegasus-kv/thrift v0.13.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/common v0.37.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.8.0 // indirect
|
||||||
github.com/rubenv/sql-migrate v1.1.2 // indirect
|
github.com/rubenv/sql-migrate v1.3.1 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/shopspring/decimal v1.2.0 // indirect
|
github.com/shopspring/decimal v1.3.1 // indirect
|
||||||
github.com/spf13/cast v1.5.0 // indirect
|
github.com/spf13/cast v1.5.0 // indirect
|
||||||
github.com/spf13/cobra v1.6.0 // indirect
|
github.com/spf13/cobra v1.6.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
github.com/xlab/treeprint v1.1.0 // indirect
|
github.com/xlab/treeprint v1.1.0 // indirect
|
||||||
go.etcd.io/etcd/api/v3 v3.5.4 // indirect
|
go.opentelemetry.io/otel v1.14.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
||||||
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20221110155412-d0897a79cd37 // indirect
|
golang.org/x/exp v0.0.0-20221110155412-d0897a79cd37 // indirect
|
||||||
golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect
|
golang.org/x/net v0.10.0 // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
|
golang.org/x/oauth2 v0.4.0 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/sys v0.3.0 // indirect
|
golang.org/x/sys v0.8.0 // indirect
|
||||||
golang.org/x/term v0.3.0 // indirect
|
golang.org/x/term v0.8.0 // indirect
|
||||||
golang.org/x/text v0.5.0 // indirect
|
golang.org/x/text v0.9.0 // indirect
|
||||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
||||||
google.golang.org/grpc v1.49.0 // indirect
|
google.golang.org/grpc v1.53.0 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gotest.tools/v3 v3.4.0
|
k8s.io/apiextensions-apiserver v0.27.1 // indirect
|
||||||
k8s.io/apiextensions-apiserver v0.25.2 // indirect
|
k8s.io/apiserver v0.27.1 // indirect
|
||||||
k8s.io/apiserver v0.25.2 // indirect
|
k8s.io/component-base v0.27.2 // indirect
|
||||||
k8s.io/component-base v0.26.0 // indirect
|
k8s.io/klog/v2 v2.90.1 // indirect
|
||||||
k8s.io/klog/v2 v2.80.1 // indirect
|
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
|
oras.land/oras-go v1.2.2 // indirect
|
||||||
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d
|
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||||
oras.land/oras-go v1.2.0 // indirect
|
sigs.k8s.io/kustomize/api v0.13.2 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
sigs.k8s.io/kustomize/kyaml v0.14.1 // 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/structured-merge-diff/v4 v4.2.3 // indirect
|
||||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 270 KiB |
BIN
images/screenshot_local_charts.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
images/screenshot_multicluster.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
images/screenshot_release.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
images/screenshot_release1.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
images/screenshot_release2.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
images/screenshot_release3.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
images/screenshot_release4.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
images/screenshot_release5.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
images/screenshot_release_detail.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
images/screenshot_release_detail1.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
images/screenshot_repository.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
images/screenshot_repository2.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
images/screenshot_repository3.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
images/screenshot_repository4.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
images/screenshot_repository5.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
images/screenshot_repository6.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
images/screenshot_repository7.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
BIN
images/screenshot_shut_down.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
42
main.go
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/joomcode/errorx"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -22,16 +23,23 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type options struct {
|
type options struct {
|
||||||
Version bool `long:"version" description:"Show tool version"`
|
Version bool `long:"version" description:"Show tool version"`
|
||||||
Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"`
|
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"`
|
NoBrowser bool `short:"b" long:"no-browser" description:"Do not attempt to open Web browser upon start"`
|
||||||
NoTracking bool `long:"no-analytics" description:"Disable user analytics (GA, DataDog etc.)"`
|
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
|
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"`
|
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"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
opts := parseFlags()
|
opts := parseFlags()
|
||||||
if opts.BindHost == "" {
|
if opts.BindHost == "" {
|
||||||
host := os.Getenv("HD_BIND")
|
host := os.Getenv("HD_BIND")
|
||||||
@@ -45,11 +53,13 @@ func main() {
|
|||||||
setupLogging(opts.Verbose)
|
setupLogging(opts.Verbose)
|
||||||
|
|
||||||
server := dashboard.Server{
|
server := dashboard.Server{
|
||||||
Version: version,
|
Version: version,
|
||||||
Namespaces: strings.Split(opts.Namespace, ","),
|
Namespaces: strings.Split(opts.Namespace, ","),
|
||||||
Address: fmt.Sprintf("%s:%d", opts.BindHost, opts.Port),
|
Address: fmt.Sprintf("%s:%d", opts.BindHost, opts.Port),
|
||||||
Debug: opts.Verbose,
|
Debug: opts.Verbose,
|
||||||
NoTracking: opts.NoTracking,
|
NoTracking: opts.NoTracking,
|
||||||
|
Devel: opts.Devel,
|
||||||
|
LocalCharts: opts.LocalChart,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -64,7 +74,13 @@ func main() {
|
|||||||
|
|
||||||
address, webServerDone, err := server.StartServer(ctx, cancel)
|
address, webServerDone, err := server.StartServer(ctx, cancel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to start Helm Dashboard: %+v", err)
|
if errorx.IsOfType(err, errorx.InitializationFailed) {
|
||||||
|
log.Debugf("Full error: %+v", err)
|
||||||
|
log.Errorf("No Kubernetes cluster connection possible. Make sure you have valid kubeconfig file or run dashboard from inside cluster. See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/")
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
log.Fatalf("Failed to start Helm Dashboard: %+v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.NoTracking {
|
if !opts.NoTracking {
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
package dashboard
|
package dashboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/handlers"
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/handlers"
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||||
@@ -13,14 +21,6 @@ import (
|
|||||||
"helm.sh/helm/v3/pkg/registry"
|
"helm.sh/helm/v3/pkg/registry"
|
||||||
"helm.sh/helm/v3/pkg/storage"
|
"helm.sh/helm/v3/pkg/storage"
|
||||||
"helm.sh/helm/v3/pkg/storage/driver"
|
"helm.sh/helm/v3/pkg/storage/driver"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var inMemStorage *storage.Storage
|
var inMemStorage *storage.Storage
|
||||||
@@ -33,7 +33,7 @@ func TestMain(m *testing.M) { // fixture to set logging level via env variable
|
|||||||
}
|
}
|
||||||
|
|
||||||
inMemStorage = storage.Init(driver.NewMemory())
|
inMemStorage = storage.Init(driver.NewMemory())
|
||||||
d, err := ioutil.TempDir("", "helm")
|
d, err := os.MkdirTemp("", "helm")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ func TestConfigureRoutes(t *testing.T) {
|
|||||||
|
|
||||||
// Required arguements for route configuration
|
// Required arguements for route configuration
|
||||||
abortWeb := func() {}
|
abortWeb := func() {}
|
||||||
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig)
|
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -131,7 +131,7 @@ func TestContextSetter(t *testing.T) {
|
|||||||
con := GetTestGinContext(w)
|
con := GetTestGinContext(w)
|
||||||
|
|
||||||
// Required arguements
|
// Required arguements
|
||||||
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig)
|
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -161,7 +161,7 @@ func TestNewRouter(t *testing.T) {
|
|||||||
|
|
||||||
// Required arguemnets
|
// Required arguemnets
|
||||||
abortWeb := func() {}
|
abortWeb := func() {}
|
||||||
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig)
|
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -183,7 +183,7 @@ func TestConfigureScanners(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Required arguemnets
|
// Required arguemnets
|
||||||
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig)
|
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -206,7 +206,7 @@ func TestConfigureKubectls(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Required arguemnets
|
// Required arguemnets
|
||||||
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig)
|
data, err := objects.NewDataLayer([]string{"TestSpace"}, "T-1", NewHelmConfig, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -226,7 +226,7 @@ func TestConfigureKubectls(t *testing.T) {
|
|||||||
|
|
||||||
func TestE2E(t *testing.T) {
|
func TestE2E(t *testing.T) {
|
||||||
// Initialize data layer
|
// Initialize data layer
|
||||||
data, err := objects.NewDataLayer([]string{""}, "0.0.0-test", getFakeHelmConfig)
|
data, err := objects.NewDataLayer([]string{""}, "0.0.0-test", getFakeHelmConfig, false)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
||||||
// Create a new router with the function
|
// Create a new router with the function
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/hexops/gotextdiff"
|
"github.com/hexops/gotextdiff"
|
||||||
"github.com/hexops/gotextdiff/myers"
|
"github.com/hexops/gotextdiff/myers"
|
||||||
"github.com/hexops/gotextdiff/span"
|
"github.com/hexops/gotextdiff/span"
|
||||||
"github.com/joomcode/errorx"
|
"github.com/joomcode/errorx"
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||||
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||||
"github.com/rogpeppe/go-internal/semver"
|
"github.com/rogpeppe/go-internal/semver"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -15,12 +25,7 @@ import (
|
|||||||
"helm.sh/helm/v3/pkg/release"
|
"helm.sh/helm/v3/pkg/release"
|
||||||
"helm.sh/helm/v3/pkg/repo"
|
"helm.sh/helm/v3/pkg/repo"
|
||||||
helmtime "helm.sh/helm/v3/pkg/time"
|
helmtime "helm.sh/helm/v3/pkg/time"
|
||||||
"net/http"
|
"k8s.io/utils/strings/slices"
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type HelmHandler struct {
|
type HelmHandler struct {
|
||||||
@@ -120,7 +125,7 @@ func (h *HelmHandler) History(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) Resources(c *gin.Context) {
|
func (h *HelmHandler) Resources(c *gin.Context) {
|
||||||
h.EnableClientCache(c)
|
// can't enable the client cache because resource list changes with time
|
||||||
|
|
||||||
rel := h.getRelease(c)
|
rel := h.getRelease(c)
|
||||||
if rel == nil {
|
if rel == nil {
|
||||||
@@ -129,8 +134,39 @@ func (h *HelmHandler) Resources(c *gin.Context) {
|
|||||||
|
|
||||||
res, err := objects.ParseManifests(rel.Orig.Manifest)
|
res, err := objects.ParseManifests(rel.Orig.Manifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
res = append(res, &v1.Carp{
|
||||||
return
|
TypeMeta: metav1.TypeMeta{Kind: "ManifestParseError"},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: err.Error(),
|
||||||
|
},
|
||||||
|
Spec: v1.CarpSpec{},
|
||||||
|
Status: v1.CarpStatus{
|
||||||
|
Phase: "BrokenManifest",
|
||||||
|
Message: err.Error(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
//_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
//return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Query("health") != "" { // we need to query k8s for health status
|
||||||
|
app := h.GetApp(c)
|
||||||
|
if app == nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, obj := range res {
|
||||||
|
ns := obj.Namespace
|
||||||
|
if ns == "" {
|
||||||
|
ns = c.Param("ns")
|
||||||
|
}
|
||||||
|
info, err := app.K8s.GetResourceInfo(obj.Kind, ns, obj.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to get resource info for %s %s/%s: %+v", obj.Name, ns, obj.Name, err)
|
||||||
|
info = &v1.Carp{}
|
||||||
|
}
|
||||||
|
obj.Status = *EnhanceStatus(info, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
@@ -162,6 +198,7 @@ func (h *HelmHandler) RepoVersions(c *gin.Context) {
|
|||||||
AppVersion: r.AppVersion,
|
AppVersion: r.AppVersion,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
Repository: r.Annotations[objects.AnnRepo],
|
Repository: r.Annotations[objects.AnnRepo],
|
||||||
|
URLs: r.URLs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +231,7 @@ func (h *HelmHandler) RepoLatestVer(c *gin.Context) {
|
|||||||
AppVersion: r.AppVersion,
|
AppVersion: r.AppVersion,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
Repository: r.Annotations[objects.AnnRepo],
|
Repository: r.Annotations[objects.AnnRepo],
|
||||||
|
URLs: r.URLs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +242,21 @@ func (h *HelmHandler) RepoLatestVer(c *gin.Context) {
|
|||||||
if len(res) > 0 {
|
if len(res) > 0 {
|
||||||
c.IndentedJSON(http.StatusOK, res[:1])
|
c.IndentedJSON(http.StatusOK, res[:1])
|
||||||
} else {
|
} else {
|
||||||
c.Status(http.StatusNoContent)
|
// 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +284,6 @@ func (h *HelmHandler) RepoCharts(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: enrich with installed
|
|
||||||
enrichRepoChartsWithInstalled(charts, installed)
|
enrichRepoChartsWithInstalled(charts, installed)
|
||||||
|
|
||||||
sort.Slice(charts, func(i, j int) bool {
|
sort.Slice(charts, func(i, j int) bool {
|
||||||
@@ -288,12 +339,19 @@ func (h *HelmHandler) Install(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repoChart, err := h.checkLocalRepo(c.PostForm("chart"))
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
justTemplate := c.PostForm("preview") == "true"
|
justTemplate := c.PostForm("preview") == "true"
|
||||||
ns := c.Param("ns")
|
ns := c.Param("ns")
|
||||||
if ns == "[empty]" {
|
if ns == "[empty]" {
|
||||||
ns = ""
|
ns = ""
|
||||||
}
|
}
|
||||||
rel, err := app.Releases.Install(ns, c.PostForm("name"), c.PostForm("chart"), c.PostForm("version"), justTemplate, values)
|
|
||||||
|
rel, err := app.Releases.Install(ns, c.PostForm("name"), repoChart, c.PostForm("version"), justTemplate, values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -306,6 +364,16 @@ func (h *HelmHandler) Install(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func (h *HelmHandler) Upgrade(c *gin.Context) {
|
||||||
app := h.GetApp(c)
|
app := h.GetApp(c)
|
||||||
if app == nil {
|
if app == nil {
|
||||||
@@ -325,8 +393,14 @@ func (h *HelmHandler) Upgrade(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repoChart, err := h.checkLocalRepo(c.PostForm("chart"))
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
justTemplate := c.PostForm("preview") == "true"
|
justTemplate := c.PostForm("preview") == "true"
|
||||||
rel, err := existing.Upgrade(c.PostForm("chart"), c.PostForm("version"), justTemplate, values)
|
rel, err := existing.Upgrade(repoChart, c.PostForm("version"), justTemplate, values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -409,7 +483,13 @@ func (h *HelmHandler) RepoValues(c *gin.Context) {
|
|||||||
return // sets error inside
|
return // sets error inside
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := app.Repositories.GetChartValues(c.Query("chart"), c.Query("version"))
|
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 {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -433,8 +513,8 @@ func (h *HelmHandler) RepoList(c *gin.Context) {
|
|||||||
out := []RepositoryElement{}
|
out := []RepositoryElement{}
|
||||||
for _, r := range repos {
|
for _, r := range repos {
|
||||||
out = append(out, RepositoryElement{
|
out = append(out, RepositoryElement{
|
||||||
Name: r.Orig.Name,
|
Name: r.Name(),
|
||||||
URL: r.Orig.URL,
|
URL: r.URL(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +528,7 @@ func (h *HelmHandler) RepoAdd(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: more repo options to accept
|
// TODO: more repo options to accept
|
||||||
err := app.Repositories.Add(c.PostForm("name"), c.PostForm("url"))
|
err := app.Repositories.Add(c.PostForm("name"), c.PostForm("url"), c.PostForm("username"), c.PostForm("password"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -521,41 +601,103 @@ func (h *HelmHandler) handleGetSection(rel *objects.Release, section string, rDi
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type RepoChartElement struct {
|
func (h *HelmHandler) repoFromArtifactHub(name string) (string, error) {
|
||||||
|
results, err := objects.QueryArtifactHub(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to query ArtifactHub: %s", err)
|
||||||
|
return "", nil // swallowing the error to not annoy users
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(results, func(i, j int) bool {
|
||||||
|
ri, rj := results[i], results[j]
|
||||||
|
|
||||||
|
// we prefer official repos
|
||||||
|
if ri.Repository.Official && !rj.Repository.Official {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// more popular
|
||||||
|
if ri.Stars != rj.Stars {
|
||||||
|
return ri.Stars > rj.Stars
|
||||||
|
}
|
||||||
|
|
||||||
|
// or from verified publishers
|
||||||
|
if ri.Repository.VerifiedPublisher && !rj.Repository.VerifiedPublisher {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// or with more recent app version
|
||||||
|
c := semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion)
|
||||||
|
if c != 0 {
|
||||||
|
return c > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// shorter repo name is usually closer to officials
|
||||||
|
return len(ri.Repository.Name) < len(rj.Repository.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
r := results[0]
|
||||||
|
buf, err := json.Marshal([]*RepoChartElement{{
|
||||||
|
Name: r.Name,
|
||||||
|
Version: r.Version,
|
||||||
|
AppVersion: r.AppVersion,
|
||||||
|
Description: r.Description,
|
||||||
|
Repository: r.Repository.Name,
|
||||||
|
URLs: []string{r.Repository.Url},
|
||||||
|
IsSuggestedRepo: true,
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepoChartElement struct { // TODO: do we need it at all? there is existing repo.ChartVersion in Helm
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
AppVersion string `json:"app_version"`
|
AppVersion string `json:"app_version"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
InstalledNamespace string `json:"installed_namespace"` // custom addition on top of Helm
|
InstalledNamespace string `json:"installed_namespace"`
|
||||||
InstalledName string `json:"installed_name"` // custom addition on top of Helm
|
InstalledName string `json:"installed_name"`
|
||||||
Repository string `json:"repository"`
|
Repository string `json:"repository"`
|
||||||
|
URLs []string `json:"urls"`
|
||||||
|
IsSuggestedRepo bool `json:"isSuggestedRepo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func HReleaseToJSON(o *release.Release) *ReleaseElement {
|
func HReleaseToJSON(o *release.Release) *ReleaseElement {
|
||||||
return &ReleaseElement{
|
return &ReleaseElement{
|
||||||
Name: o.Name,
|
Name: o.Name,
|
||||||
Namespace: o.Namespace,
|
Namespace: o.Namespace,
|
||||||
Revision: strconv.Itoa(o.Version),
|
Revision: strconv.Itoa(o.Version),
|
||||||
Updated: o.Info.LastDeployed,
|
Updated: o.Info.LastDeployed,
|
||||||
Status: o.Info.Status,
|
Status: o.Info.Status,
|
||||||
Chart: fmt.Sprintf("%s-%s", o.Chart.Name(), o.Chart.Metadata.Version),
|
Chart: fmt.Sprintf("%s-%s", o.Chart.Name(), o.Chart.Metadata.Version),
|
||||||
AppVersion: o.Chart.AppVersion(),
|
ChartName: o.Chart.Name(),
|
||||||
Icon: o.Chart.Metadata.Icon,
|
ChartVersion: o.Chart.Metadata.Version,
|
||||||
Description: o.Chart.Metadata.Description,
|
AppVersion: o.Chart.AppVersion(),
|
||||||
|
Icon: o.Chart.Metadata.Icon,
|
||||||
|
Description: o.Chart.Metadata.Description,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReleaseElement struct {
|
type ReleaseElement struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Namespace string `json:"namespace"`
|
Namespace string `json:"namespace"`
|
||||||
Revision string `json:"revision"`
|
Revision string `json:"revision"`
|
||||||
Updated helmtime.Time `json:"updated"`
|
Updated helmtime.Time `json:"updated"`
|
||||||
Status release.Status `json:"status"`
|
Status release.Status `json:"status"`
|
||||||
Chart string `json:"chart"`
|
Chart string `json:"chart"`
|
||||||
AppVersion string `json:"app_version"`
|
ChartName string `json:"chartName"`
|
||||||
Icon string `json:"icon"`
|
ChartVersion string `json:"chartVersion"`
|
||||||
Description string `json:"description"`
|
AppVersion string `json:"app_version"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RepositoryElement struct {
|
type RepositoryElement struct {
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/joomcode/errorx"
|
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/joomcode/errorx"
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||||
|
"k8s.io/utils/strings/slices"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const Unknown = "Unknown"
|
||||||
|
const Healthy = "Healthy"
|
||||||
|
const Unhealthy = "Unhealthy"
|
||||||
|
const Progressing = "Progressing"
|
||||||
|
|
||||||
type KubeHandler struct {
|
type KubeHandler struct {
|
||||||
*Contexted
|
*Contexted
|
||||||
}
|
}
|
||||||
@@ -50,25 +56,67 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
EnhanceStatus(res)
|
EnhanceStatus(res, nil)
|
||||||
|
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnhanceStatus(res *v12.Carp) {
|
func EnhanceStatus(res *v12.Carp, err error) *v12.CarpStatus {
|
||||||
// custom logic to provide most meaningful status for the resource
|
s := res.Status
|
||||||
if res.Status.Phase == "Active" || res.Status.Phase == "Error" {
|
if s.Conditions == nil {
|
||||||
_ = res.Name + ""
|
s.Conditions = []v12.CarpCondition{}
|
||||||
} 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 := v12.CarpCondition{
|
||||||
|
Type: "hdHealth",
|
||||||
|
Status: Unknown,
|
||||||
|
Reason: s.Reason,
|
||||||
|
Message: s.Message,
|
||||||
|
}
|
||||||
|
|
||||||
|
// custom logic to provide most meaningful status for the resource
|
||||||
|
if err != nil {
|
||||||
|
c.Reason = "ErrorGettingStatus"
|
||||||
|
c.Message = err.Error()
|
||||||
|
} else if s.Phase == "Error" {
|
||||||
|
c.Status = Unhealthy
|
||||||
|
} else if slices.Contains([]string{"Available", "Active", "Established", "Bound", "Ready"}, string(s.Phase)) {
|
||||||
|
c.Status = Healthy
|
||||||
|
} else if s.Phase == "" && len(s.Conditions) > 0 {
|
||||||
|
for _, cond := range s.Conditions {
|
||||||
|
if cond.Type == "Progressing" { // https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
|
||||||
|
if cond.Status == "False" {
|
||||||
|
c.Status = Unhealthy
|
||||||
|
c.Reason = cond.Reason
|
||||||
|
c.Message = cond.Message
|
||||||
|
} else if cond.Reason != "NewReplicaSetAvailable" {
|
||||||
|
c.Status = Progressing
|
||||||
|
c.Reason = cond.Reason
|
||||||
|
c.Message = cond.Message
|
||||||
|
}
|
||||||
|
} else if cond.Type == "Available" && c.Status == Unknown {
|
||||||
|
if cond.Status == "False" {
|
||||||
|
c.Status = Unhealthy
|
||||||
|
} else {
|
||||||
|
c.Status = Healthy
|
||||||
|
}
|
||||||
|
c.Reason = cond.Reason
|
||||||
|
c.Message = cond.Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if s.Phase == "Pending" {
|
||||||
|
c.Status = Progressing
|
||||||
|
c.Reason = string(s.Phase)
|
||||||
|
} else if s.Phase == "" {
|
||||||
|
c.Status = Healthy
|
||||||
|
c.Reason = "Exists"
|
||||||
|
} else {
|
||||||
|
log.Warnf("Unhandled status: %v", s)
|
||||||
|
c.Reason = string(s.Phase)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Conditions = append(s.Conditions, c)
|
||||||
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *KubeHandler) Describe(c *gin.Context) {
|
func (h *KubeHandler) Describe(c *gin.Context) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/joomcode/errorx"
|
"github.com/joomcode/errorx"
|
||||||
"helm.sh/helm/v3/pkg/action"
|
"helm.sh/helm/v3/pkg/action"
|
||||||
"helm.sh/helm/v3/pkg/cli"
|
"helm.sh/helm/v3/pkg/cli"
|
||||||
|
|
||||||
// Import to initialize client auth plugins.
|
// Import to initialize client auth plugins.
|
||||||
// From https://github.com/kubernetes/client-go/issues/242
|
// From https://github.com/kubernetes/client-go/issues/242
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||||
@@ -22,7 +23,7 @@ type Application struct {
|
|||||||
Repositories *Repositories
|
Repositories *Repositories
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApplication(settings *cli.EnvSettings, helmConfig HelmNSConfigGetter, namespaces []string) (*Application, error) {
|
func NewApplication(settings *cli.EnvSettings, helmConfig HelmNSConfigGetter, namespaces []string, devel bool) (*Application, error) {
|
||||||
hc, err := helmConfig(settings.Namespace())
|
hc, err := helmConfig(settings.Namespace())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
|
return nil, errorx.Decorate(err, "failed to get helm config for namespace '%s'", "")
|
||||||
@@ -33,6 +34,11 @@ func NewApplication(settings *cli.EnvSettings, helmConfig HelmNSConfigGetter, na
|
|||||||
return nil, errorx.Decorate(err, "failed to get k8s client")
|
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{
|
return &Application{
|
||||||
HelmConfig: helmConfig,
|
HelmConfig: helmConfig,
|
||||||
K8s: k8s,
|
K8s: k8s,
|
||||||
@@ -42,8 +48,9 @@ func NewApplication(settings *cli.EnvSettings, helmConfig HelmNSConfigGetter, na
|
|||||||
HelmConfig: helmConfig,
|
HelmConfig: helmConfig,
|
||||||
},
|
},
|
||||||
Repositories: &Repositories{
|
Repositories: &Repositories{
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
HelmConfig: hc,
|
HelmConfig: hc,
|
||||||
|
versionConstraint: semVerConstraint,
|
||||||
},
|
},
|
||||||
}, nil
|
}, 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"`
|
||||||
|
}
|
||||||
@@ -1,23 +1,26 @@
|
|||||||
package objects
|
package objects
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"io"
|
||||||
|
|
||||||
"github.com/joomcode/errorx"
|
"github.com/joomcode/errorx"
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"helm.sh/helm/v3/pkg/action"
|
"helm.sh/helm/v3/pkg/action"
|
||||||
"helm.sh/helm/v3/pkg/cli"
|
"helm.sh/helm/v3/pkg/cli"
|
||||||
"helm.sh/helm/v3/pkg/release"
|
"helm.sh/helm/v3/pkg/release"
|
||||||
"io"
|
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/yaml"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
//"sigs.k8s.io/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DataLayer struct {
|
type DataLayer struct {
|
||||||
@@ -30,6 +33,8 @@ type DataLayer struct {
|
|||||||
ConfGen HelmConfigGetter
|
ConfGen HelmConfigGetter
|
||||||
appPerContext map[string]*Application
|
appPerContext map[string]*Application
|
||||||
appPerContextMx *sync.Mutex
|
appPerContextMx *sync.Mutex
|
||||||
|
devel bool
|
||||||
|
LocalCharts []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusInfo struct {
|
type StatusInfo struct {
|
||||||
@@ -40,7 +45,7 @@ type StatusInfo struct {
|
|||||||
ClusterMode bool
|
ClusterMode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDataLayer(ns []string, ver string, cg HelmConfigGetter) (*DataLayer, error) {
|
func NewDataLayer(ns []string, ver string, cg HelmConfigGetter, devel bool) (*DataLayer, error) {
|
||||||
if cg == nil {
|
if cg == nil {
|
||||||
return nil, errors.New("HelmConfigGetter can't be nil")
|
return nil, errors.New("HelmConfigGetter can't be nil")
|
||||||
}
|
}
|
||||||
@@ -56,6 +61,7 @@ func NewDataLayer(ns []string, ver string, cg HelmConfigGetter) (*DataLayer, err
|
|||||||
ConfGen: cg,
|
ConfGen: cg,
|
||||||
appPerContext: map[string]*Application{},
|
appPerContext: map[string]*Application{},
|
||||||
appPerContextMx: new(sync.Mutex),
|
appPerContextMx: new(sync.Mutex),
|
||||||
|
devel: devel,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +103,7 @@ func (d *DataLayer) GetStatus() *StatusInfo {
|
|||||||
type SectionFn = func(*release.Release, bool) (string, error)
|
type SectionFn = func(*release.Release, bool) (string, error)
|
||||||
|
|
||||||
func ParseManifests(out string) ([]*v1.Carp, error) {
|
func ParseManifests(out string) ([]*v1.Carp, error) {
|
||||||
dec := yaml.NewDecoder(bytes.NewReader([]byte(out)))
|
dec := yaml.NewYAMLOrJSONDecoder(strings.NewReader(out), 4096)
|
||||||
|
|
||||||
res := make([]*v1.Carp, 0)
|
res := make([]*v1.Carp, 0)
|
||||||
var tmp interface{}
|
var tmp interface{}
|
||||||
for {
|
for {
|
||||||
@@ -108,20 +113,20 @@ func ParseManifests(out string) ([]*v1.Carp, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.Decorate(err, "failed to parse manifest document #%d", len(res)+1)
|
return res, errorx.Decorate(err, "failed to parse manifest document #%d", len(res)+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
|
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
|
||||||
// we can juggle it
|
// we can juggle it
|
||||||
jsoned, err := json.Marshal(tmp)
|
jsoned, err := json.Marshal(tmp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var doc v1.Carp
|
var doc v1.Carp
|
||||||
err = json.Unmarshal(jsoned, &doc)
|
err = json.Unmarshal(jsoned, &doc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if doc.Kind == "" {
|
if doc.Kind == "" {
|
||||||
@@ -162,11 +167,13 @@ func (d *DataLayer) AppForCtx(ctx string) (*Application, error) {
|
|||||||
return d.ConfGen(settings, ns)
|
return d.ConfGen(settings, ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := NewApplication(settings, cfgGetter, d.Namespaces)
|
a, err := NewApplication(settings, cfgGetter, d.Namespaces, d.devel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.Decorate(err, "Failed to create application for context '%s'", ctx)
|
return nil, errorx.Decorate(err, "Failed to create application for context '%s'", ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.Repositories.LocalCharts = d.LocalCharts
|
||||||
|
|
||||||
app = a
|
app = a
|
||||||
d.appPerContext[ctx] = app
|
d.appPerContext[ctx] = app
|
||||||
}
|
}
|
||||||
@@ -188,13 +195,12 @@ func (d *DataLayer) nsForCtx(ctx string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DataLayer) PeriodicTasks(ctx context.Context) {
|
func (d *DataLayer) PeriodicTasks(ctx context.Context) {
|
||||||
if !d.StatusInfo.ClusterMode { // TODO: maybe have a separate flag for that?
|
// TODO: separate scanning setup for in-cluster?
|
||||||
log.Debugf("Not in cluster mode, not starting background tasks")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// auto-update repos
|
if os.Getenv("HD_NO_AUTOUPDATE") == "" {
|
||||||
go d.loopUpdateRepos(ctx, 10*time.Minute) // TODO: parameterize interval?
|
// auto-update repos
|
||||||
|
go d.loopUpdateRepos(ctx, 10*time.Minute) // TODO: parameterize interval?
|
||||||
|
}
|
||||||
|
|
||||||
// auto-scan
|
// auto-scan
|
||||||
}
|
}
|
||||||
@@ -215,7 +221,7 @@ func (d *DataLayer) loopUpdateRepos(ctx context.Context, interval time.Duration)
|
|||||||
for _, repo := range repos {
|
for _, repo := range repos {
|
||||||
err := repo.Update()
|
err := repo.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Failed to update repo %s: %v", repo.Orig.Name, err)
|
log.Warnf("Failed to update repo %s: %v", repo.Name(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ func TestNewDataLayer(t *testing.T) {
|
|||||||
namespaces []string
|
namespaces []string
|
||||||
version string
|
version string
|
||||||
helmConfig HelmConfigGetter
|
helmConfig HelmConfigGetter
|
||||||
|
devel bool
|
||||||
errorExpected bool
|
errorExpected bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -22,6 +23,7 @@ func TestNewDataLayer(t *testing.T) {
|
|||||||
namespaces: []string{"namespace1", "namespace2"},
|
namespaces: []string{"namespace1", "namespace2"},
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
helmConfig: nil,
|
helmConfig: nil,
|
||||||
|
devel: false,
|
||||||
errorExpected: true,
|
errorExpected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -34,12 +36,13 @@ func TestNewDataLayer(t *testing.T) {
|
|||||||
helmConfig: func(sett *cli.EnvSettings, ns string) (*action.Configuration, error) {
|
helmConfig: func(sett *cli.EnvSettings, ns string) (*action.Configuration, error) {
|
||||||
return &action.Configuration{}, nil
|
return &action.Configuration{}, nil
|
||||||
},
|
},
|
||||||
|
devel: false,
|
||||||
errorExpected: false,
|
errorExpected: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
dl, err := NewDataLayer(tt.namespaces, tt.version, tt.helmConfig)
|
dl, err := NewDataLayer(tt.namespaces, tt.version, tt.helmConfig, tt.devel)
|
||||||
if tt.errorExpected {
|
if tt.errorExpected {
|
||||||
assert.Error(t, err, "Expected error but got nil")
|
assert.Error(t, err, "Expected error but got nil")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -126,7 +126,14 @@ func (k *K8s) DescribeResource(kind string, ns string, name string) (string, err
|
|||||||
|
|
||||||
func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.Object, error) {
|
func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.Object, error) {
|
||||||
builder := k.Factory.NewBuilder()
|
builder := k.Factory.NewBuilder()
|
||||||
resp := builder.Unstructured().NamespaceParam(namespace).Flatten().ResourceNames(kind, name).Do()
|
builder = builder.Unstructured().SingleResourceType()
|
||||||
|
if namespace != "" {
|
||||||
|
builder = builder.NamespaceParam(namespace)
|
||||||
|
} else {
|
||||||
|
builder = builder.DefaultNamespace()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := builder.Flatten().ResourceNames(kind, name).Do()
|
||||||
if resp.Err() != nil {
|
if resp.Err() != nil {
|
||||||
return nil, errorx.Decorate(resp.Err(), "failed to get k8s resource")
|
return nil, errorx.Decorate(resp.Err(), "failed to get k8s resource")
|
||||||
}
|
}
|
||||||
@@ -139,6 +146,7 @@ func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k *K8s) GetResourceInfo(kind string, namespace string, name string) (*testapiv1.Carp, error) {
|
func (k *K8s) GetResourceInfo(kind string, namespace string, name string) (*testapiv1.Carp, error) {
|
||||||
|
// TODO: mutex to avoid a lot of requests?
|
||||||
obj, err := k.GetResource(kind, namespace, name)
|
obj, err := k.GetResource(kind, namespace, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.Decorate(err, "failed to get k8s object")
|
return nil, errorx.Decorate(err, "failed to get k8s object")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -45,6 +45,7 @@ func (a *Releases) List() ([]*Release, error) {
|
|||||||
client.All = true
|
client.All = true
|
||||||
client.AllNamespaces = true
|
client.AllNamespaces = true
|
||||||
client.Limit = 0
|
client.Limit = 0
|
||||||
|
client.SetStateMask() // required to apply proper filtering
|
||||||
rels, err := client.Run()
|
rels, err := client.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.Decorate(err, "failed to get list of releases")
|
return nil, errorx.Decorate(err, "failed to get list of releases")
|
||||||
@@ -139,7 +140,7 @@ func locateChart(pathOpts action.ChartPathOptions, chart string, settings *cli.E
|
|||||||
err = errorx.Decorate(err, "An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies")
|
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
|
if true { // client.DependencyUpdate
|
||||||
man := &downloader.Manager{
|
man := &downloader.Manager{
|
||||||
Out: ioutil.Discard,
|
Out: io.Discard,
|
||||||
ChartPath: cp,
|
ChartPath: cp,
|
||||||
Keyring: pathOpts.Keyring,
|
Keyring: pathOpts.Keyring,
|
||||||
SkipUpdate: false,
|
SkipUpdate: false,
|
||||||
@@ -319,6 +320,7 @@ func (r *Release) Upgrade(repoChart string, version string, justTemplate bool, v
|
|||||||
cmd.Version = version
|
cmd.Version = version
|
||||||
|
|
||||||
cmd.DryRun = justTemplate
|
cmd.DryRun = justTemplate
|
||||||
|
cmd.ResetValues = true
|
||||||
|
|
||||||
chrt, err := locateChart(cmd.ChartPathOptions, repoChart, r.Settings)
|
chrt, err := locateChart(cmd.ChartPathOptions, repoChart, r.Settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -345,7 +347,7 @@ func (r *Release) restoreChart() (string, error) {
|
|||||||
// we're unlikely to have the original chart, let's try the cheesy thing...
|
// 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)
|
log.Infof("Attempting to restore the chart for %s", r.Orig.Name)
|
||||||
dir, err := ioutil.TempDir("", "khd-*")
|
dir, err := os.MkdirTemp("", "khd-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errorx.Decorate(err, "failed to get temporary directory")
|
return "", errorx.Decorate(err, "failed to get temporary directory")
|
||||||
}
|
}
|
||||||
@@ -355,7 +357,7 @@ func (r *Release) restoreChart() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errorx.Decorate(err, "failed to restore Chart.yaml")
|
return "", errorx.Decorate(err, "failed to restore Chart.yaml")
|
||||||
}
|
}
|
||||||
err = ioutil.WriteFile(path.Join(dir, "Chart.yaml"), cdata, 0644)
|
err = os.WriteFile(path.Join(dir, "Chart.yaml"), cdata, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errorx.Decorate(err, "failed to write file Chart.yaml")
|
return "", errorx.Decorate(err, "failed to write file Chart.yaml")
|
||||||
}
|
}
|
||||||
@@ -365,7 +367,7 @@ func (r *Release) restoreChart() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errorx.Decorate(err, "failed to restore values.yaml")
|
return "", errorx.Decorate(err, "failed to restore values.yaml")
|
||||||
}
|
}
|
||||||
err = ioutil.WriteFile(path.Join(dir, "values.yaml"), vdata, 0644)
|
err = os.WriteFile(path.Join(dir, "values.yaml"), vdata, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errorx.Decorate(err, "failed to write file values.yaml")
|
return "", errorx.Decorate(err, "failed to write file values.yaml")
|
||||||
}
|
}
|
||||||
@@ -379,7 +381,7 @@ func (r *Release) restoreChart() (string, error) {
|
|||||||
return "", errorx.Decorate(err, "failed to create directory for file: %s", fname)
|
return "", errorx.Decorate(err, "failed to create directory for file: %s", fname)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(fname, f.Data, 0644)
|
err = os.WriteFile(fname, f.Data, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errorx.Decorate(err, "failed to write file to restore chart: %s", fname)
|
return "", errorx.Decorate(err, "failed to write file to restore chart: %s", fname)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,34 @@
|
|||||||
package objects
|
package objects
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/joomcode/errorx"
|
"github.com/joomcode/errorx"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"helm.sh/helm/v3/pkg/action"
|
"helm.sh/helm/v3/pkg/action"
|
||||||
"helm.sh/helm/v3/pkg/chart"
|
|
||||||
"helm.sh/helm/v3/pkg/chart/loader"
|
"helm.sh/helm/v3/pkg/chart/loader"
|
||||||
"helm.sh/helm/v3/pkg/cli"
|
"helm.sh/helm/v3/pkg/cli"
|
||||||
"helm.sh/helm/v3/pkg/getter"
|
"helm.sh/helm/v3/pkg/getter"
|
||||||
"helm.sh/helm/v3/pkg/helmpath"
|
"helm.sh/helm/v3/pkg/helmpath"
|
||||||
"helm.sh/helm/v3/pkg/repo"
|
"helm.sh/helm/v3/pkg/repo"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const AnnRepo = "helm-dashboard/repository-name"
|
const AnnRepo = "helm-dashboard/repository-name"
|
||||||
|
|
||||||
type Repositories struct {
|
type Repositories struct {
|
||||||
Settings *cli.EnvSettings
|
Settings *cli.EnvSettings
|
||||||
HelmConfig *action.Configuration
|
HelmConfig *action.Configuration
|
||||||
mx sync.Mutex
|
mx sync.Mutex
|
||||||
|
versionConstraint *semver.Constraints
|
||||||
|
LocalCharts []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repositories) Load() (*repo.File, error) {
|
func (r *Repositories) load() (*repo.File, error) {
|
||||||
r.mx.Lock()
|
r.mx.Lock()
|
||||||
defer r.mx.Unlock()
|
defer r.mx.Unlock()
|
||||||
|
|
||||||
@@ -37,28 +40,40 @@ func (r *Repositories) Load() (*repo.File, error) {
|
|||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repositories) List() ([]*Repository, error) {
|
func (r *Repositories) List() ([]Repository, error) {
|
||||||
f, err := r.Load()
|
f, err := r.load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.Decorate(err, "failed to load repo information")
|
return nil, errorx.Decorate(err, "failed to load repo information")
|
||||||
}
|
}
|
||||||
|
|
||||||
res := []*Repository{}
|
res := []Repository{}
|
||||||
for _, item := range f.Repositories {
|
for _, item := range f.Repositories {
|
||||||
res = append(res, &Repository{
|
res = append(res, &HelmRepo{
|
||||||
Settings: r.Settings,
|
Settings: r.Settings,
|
||||||
Orig: item,
|
Orig: item,
|
||||||
|
versionConstraint: r.versionConstraint,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(r.LocalCharts) > 0 {
|
||||||
|
lc := LocalChart{
|
||||||
|
LocalCharts: r.LocalCharts,
|
||||||
|
}
|
||||||
|
res = append(res, &lc)
|
||||||
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repositories) Add(name string, url string) error {
|
func (r *Repositories) Add(name string, url string, username string, password string) error {
|
||||||
if name == "" || url == "" {
|
if name == "" || url == "" {
|
||||||
return errors.New("Name and URL are required parameters to add the repository")
|
return errors.New("Name and URL are required parameters to add the repository")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (username != "" && password == "") || (username == "" && password != "") {
|
||||||
|
return errors.New("Username and Password, both are required parameters to add the repository with authentication")
|
||||||
|
}
|
||||||
|
|
||||||
// copied from cmd/helm/repo_add.go
|
// copied from cmd/helm/repo_add.go
|
||||||
repoFile := r.Settings.RepositoryConfig
|
repoFile := r.Settings.RepositoryConfig
|
||||||
|
|
||||||
@@ -68,7 +83,7 @@ func (r *Repositories) Add(name string, url string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := r.Load()
|
f, err := r.load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.Decorate(err, "Failed to load repo config")
|
return errorx.Decorate(err, "Failed to load repo config")
|
||||||
}
|
}
|
||||||
@@ -77,10 +92,10 @@ func (r *Repositories) Add(name string, url string) error {
|
|||||||
defer r.mx.Unlock()
|
defer r.mx.Unlock()
|
||||||
|
|
||||||
c := repo.Entry{
|
c := repo.Entry{
|
||||||
Name: name,
|
Name: name,
|
||||||
URL: url,
|
URL: url,
|
||||||
//Username: o.username,
|
Username: username,
|
||||||
//Password: o.password,
|
Password: password,
|
||||||
//PassCredentialsAll: o.passCredentialsAll,
|
//PassCredentialsAll: o.passCredentialsAll,
|
||||||
//CertFile: o.certFile,
|
//CertFile: o.certFile,
|
||||||
//KeyFile: o.keyFile,
|
//KeyFile: o.keyFile,
|
||||||
@@ -111,7 +126,7 @@ func (r *Repositories) Add(name string, url string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repositories) Delete(name string) error {
|
func (r *Repositories) Delete(name string) error {
|
||||||
f, err := r.Load()
|
f, err := r.load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.Decorate(err, "failed to load repo information")
|
return errorx.Decorate(err, "failed to load repo information")
|
||||||
}
|
}
|
||||||
@@ -133,24 +148,22 @@ func (r *Repositories) Delete(name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repositories) Get(name string) (*Repository, error) {
|
func (r *Repositories) Get(name string) (Repository, error) {
|
||||||
f, err := r.Load()
|
l, err := r.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.Decorate(err, "failed to load repo information")
|
return nil, errorx.Decorate(err, "failed to get list of repos")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, entry := range f.Repositories {
|
for _, entry := range l {
|
||||||
if entry.Name == name {
|
if entry.Name() == name {
|
||||||
return &Repository{
|
return entry, nil
|
||||||
Settings: r.Settings,
|
|
||||||
Orig: entry,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errorx.DataUnavailable.New("Could not find reposiroty '%s'", name)
|
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) {
|
func (r *Repositories) Containing(name string) (repo.ChartVersions, error) {
|
||||||
list, err := r.List()
|
list, err := r.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -161,11 +174,12 @@ func (r *Repositories) Containing(name string) (repo.ChartVersions, error) {
|
|||||||
for _, rep := range list {
|
for _, rep := range list {
|
||||||
vers, err := rep.ByName(name)
|
vers, err := rep.ByName(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Failed to get data from repo '%s', updating it might help", rep.Orig.Name)
|
log.Warnf("Failed to get data from repo '%s', updating it might help", rep.Name())
|
||||||
log.Debugf("The error was: %v", err)
|
log.Debugf("The error was: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var updatedChartVersions repo.ChartVersions
|
||||||
for _, v := range vers {
|
for _, v := range vers {
|
||||||
// just using annotations here to attach a bit of information to the object
|
// 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
|
// it has nothing to do with k8s annotations and should not get into manifests
|
||||||
@@ -173,32 +187,27 @@ func (r *Repositories) Containing(name string) (repo.ChartVersions, error) {
|
|||||||
v.Annotations = map[string]string{}
|
v.Annotations = map[string]string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
v.Annotations[AnnRepo] = rep.Orig.Name
|
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, vers...) // TODO filter dev versions here, relates to #139
|
res = append(res, updatedChartVersions...)
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repositories) GetChart(chart string, ver string) (*chart.Chart, error) {
|
|
||||||
// TODO: unused method?
|
|
||||||
client := action.NewShowWithConfig(action.ShowAll, r.HelmConfig)
|
|
||||||
client.Version = ver
|
|
||||||
|
|
||||||
cp, err := client.ChartPathOptions.LocateChart(chart, r.Settings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errorx.Decorate(err, "failed to locate chart '%s'", chart)
|
|
||||||
}
|
|
||||||
|
|
||||||
chrt, err := loader.Load(cp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errorx.Decorate(err, "failed to load chart from '%s'", cp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return chrt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repositories) GetChartValues(chart string, ver string) (string, error) {
|
func (r *Repositories) GetChartValues(chart string, ver string) (string, error) {
|
||||||
// comes from cmd/helm/show.go
|
// comes from cmd/helm/show.go
|
||||||
client := action.NewShowWithConfig(action.ShowValues, r.HelmConfig)
|
client := action.NewShowWithConfig(action.ShowValues, r.HelmConfig)
|
||||||
@@ -216,17 +225,35 @@ func (r *Repositories) GetChartValues(chart string, ver string) (string, error)
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Repository struct {
|
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
|
Settings *cli.EnvSettings
|
||||||
Orig *repo.Entry
|
Orig *repo.Entry
|
||||||
mx sync.Mutex
|
mx sync.Mutex
|
||||||
|
|
||||||
|
versionConstraint *semver.Constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) indexFileName() string {
|
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))
|
return filepath.Join(r.Settings.RepositoryCache, helmpath.CacheIndexFile(r.Orig.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) getIndex() (*repo.IndexFile, error) {
|
func (r *HelmRepo) getIndex() (*repo.IndexFile, error) {
|
||||||
r.mx.Lock()
|
r.mx.Lock()
|
||||||
defer r.mx.Unlock()
|
defer r.mx.Unlock()
|
||||||
|
|
||||||
@@ -240,23 +267,39 @@ func (r *Repository) getIndex() (*repo.IndexFile, error) {
|
|||||||
return ind, nil
|
return ind, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) Charts() ([]*repo.ChartVersion, error) {
|
func (r *HelmRepo) Charts() (repo.ChartVersions, error) {
|
||||||
ind, err := r.getIndex()
|
ind, err := r.getIndex()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.Decorate(err, "failed to get repo index")
|
return nil, errorx.Decorate(err, "failed to get repo index")
|
||||||
}
|
}
|
||||||
|
|
||||||
res := []*repo.ChartVersion{}
|
res := repo.ChartVersions{}
|
||||||
for _, v := range ind.Entries {
|
for _, cv := range ind.Entries {
|
||||||
if len(v) > 0 { // TODO filter dev versions here, relates to #139
|
for _, v := range cv {
|
||||||
res = append(res, v[0])
|
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
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) ByName(name string) (repo.ChartVersions, error) {
|
func (r *HelmRepo) ByName(name string) (repo.ChartVersions, error) {
|
||||||
ind, err := r.getIndex()
|
ind, err := r.getIndex()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.Decorate(err, "failed to get repo index")
|
return nil, errorx.Decorate(err, "failed to get repo index")
|
||||||
@@ -269,7 +312,7 @@ func (r *Repository) ByName(name string) (repo.ChartVersions, error) {
|
|||||||
return repo.ChartVersions{}, nil
|
return repo.ChartVersions{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) Update() error {
|
func (r *HelmRepo) Update() error {
|
||||||
r.mx.Lock()
|
r.mx.Lock()
|
||||||
defer r.mx.Unlock()
|
defer r.mx.Unlock()
|
||||||
log.Infof("Updating repository: %s", r.Orig.Name)
|
log.Infof("Updating repository: %s", r.Orig.Name)
|
||||||
@@ -310,3 +353,79 @@ func removeRepoCache(root, name string) error {
|
|||||||
}
|
}
|
||||||
return os.Remove(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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,149 +1,289 @@
|
|||||||
package objects
|
package objects
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"helm.sh/helm/v3/pkg/action"
|
"os"
|
||||||
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
|
"helm.sh/helm/v3/pkg/action"
|
||||||
"helm.sh/helm/v3/pkg/cli"
|
"helm.sh/helm/v3/pkg/cli"
|
||||||
"helm.sh/helm/v3/pkg/repo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var filePath = "./testdata/repositories.yaml"
|
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) *Repositories {
|
func initRepository(t *testing.T, filePath string, devel bool) *Repositories {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
settings := cli.New()
|
settings := cli.New()
|
||||||
|
|
||||||
|
fname, err := os.CreateTemp("", "repo-*.yaml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(fname.Name(), input, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := os.Remove(fname.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
vc, err := versionConstaint(devel)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Sets the repository file path
|
// Sets the repository file path
|
||||||
settings.RepositoryConfig = filePath
|
settings.RepositoryConfig = fname.Name()
|
||||||
|
settings.RepositoryCache = path.Dir(filePath)
|
||||||
|
|
||||||
testRepository := &Repositories{
|
testRepository := &Repositories{
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
HelmConfig: &action.Configuration{}, // maybe use copy of getFakeHelmConfig from api_test.go
|
HelmConfig: &action.Configuration{}, // maybe use copy of getFakeHelmConfig from api_test.go
|
||||||
|
versionConstraint: vc,
|
||||||
|
LocalCharts: []string{"../../../charts/helm-dashboard"},
|
||||||
}
|
}
|
||||||
|
|
||||||
return testRepository
|
return testRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadRepo(t *testing.T) {
|
func TestFlow(t *testing.T) {
|
||||||
|
testRepository := initRepository(t, validRepositoryConfigPath, false)
|
||||||
res, err := repo.LoadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testRepository := initRepository(t, filePath)
|
|
||||||
|
|
||||||
file, err := testRepository.Load()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, file.Generated, res.Generated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestList(t *testing.T) {
|
|
||||||
res, err := repo.LoadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testRepository := initRepository(t, filePath)
|
|
||||||
|
|
||||||
|
// initial list
|
||||||
repos, err := testRepository.List()
|
repos, err := testRepository.List()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, len(repos), 5)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, len(repos), len(res.Repositories))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAdd(t *testing.T) {
|
|
||||||
testRepoName := "TEST"
|
testRepoName := "TEST"
|
||||||
testRepoUrl := "https://helm.github.io/examples"
|
testRepoUrl := "https://helm.github.io/examples"
|
||||||
|
|
||||||
res, err := repo.LoadFile(filePath)
|
// add repo
|
||||||
if err != nil {
|
err = testRepository.Add(testRepoName, testRepoUrl, "", "")
|
||||||
t.Fatal(err)
|
assert.NilError(t, err)
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the repository if already exist
|
// get repo
|
||||||
res.Remove(testRepoName)
|
r, err := testRepository.Get(testRepoName)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, r.URL(), testRepoUrl)
|
||||||
|
|
||||||
testRepository := initRepository(t, filePath)
|
// update repo
|
||||||
|
err = r.Update()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
err = testRepository.Add(testRepoName, testRepoUrl)
|
// list charts
|
||||||
|
c, err := r.Charts()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
if err != nil {
|
// contains chart
|
||||||
t.Fatal(err, "Failed to add repo")
|
c, err = testRepository.Containing(c[0].Name)
|
||||||
}
|
assert.NilError(t, err)
|
||||||
|
|
||||||
// Reload the file
|
// chart by name from repo
|
||||||
res, err = repo.LoadFile(filePath)
|
c, err = r.ByName(c[0].Name)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, res.Has(testRepoName), true)
|
// get chart values
|
||||||
|
v, err := testRepository.GetChartValues(r.Name()+"/"+c[0].Name, c[0].Version)
|
||||||
// Removes test repository which is added for testing
|
assert.NilError(t, err)
|
||||||
t.Cleanup(func() {
|
assert.Assert(t, v != "")
|
||||||
removed := res.Remove(testRepoName)
|
|
||||||
if removed != true {
|
|
||||||
t.Log("Failed to clean the test repository file")
|
|
||||||
}
|
|
||||||
err = res.WriteFile(filePath, 0644)
|
|
||||||
if err != nil {
|
|
||||||
t.Log("Failed to write the file while cleaning test repo")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDelete(t *testing.T) {
|
|
||||||
testRepoName := "TEST DELETE"
|
|
||||||
testRepoUrl := "https://helm.github.io/examples"
|
|
||||||
|
|
||||||
res, err := repo.LoadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a test entry
|
|
||||||
res.Add(&repo.Entry{Name: testRepoName, URL: testRepoUrl})
|
|
||||||
err = res.WriteFile(filePath, 0644)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Failed to write the file while creating test repo")
|
|
||||||
}
|
|
||||||
|
|
||||||
testRepository := initRepository(t, filePath)
|
|
||||||
|
|
||||||
|
// delete added
|
||||||
err = testRepository.Delete(testRepoName)
|
err = testRepository.Delete(testRepoName)
|
||||||
if err != nil {
|
assert.NilError(t, err)
|
||||||
t.Fatal(err, "Failed to delete the repo")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload the file
|
// final list
|
||||||
res, err = repo.LoadFile(filePath)
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, res.Has(testRepoName), false)
|
charts, err := r.Charts()
|
||||||
}
|
|
||||||
|
|
||||||
func TestGet(t *testing.T) {
|
|
||||||
// Initial repositiry name in test file
|
|
||||||
repoName := "charts"
|
|
||||||
|
|
||||||
testRepository := initRepository(t, filePath)
|
|
||||||
|
|
||||||
repo, err := testRepository.Get(repoName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err, "Failed to get th repo")
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, repo.Orig.Name, repoName)
|
// 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: ""
|
||||||
@@ -28,3 +28,6 @@ repositories:
|
|||||||
password: ""
|
password: ""
|
||||||
url: http://secondexample.com
|
url: http://secondexample.com
|
||||||
username: ""
|
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
|
||||||
@@ -4,16 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/joomcode/errorx"
|
"github.com/joomcode/errorx"
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
||||||
"helm.sh/helm/v3/pkg/action"
|
"helm.sh/helm/v3/pkg/action"
|
||||||
"helm.sh/helm/v3/pkg/cli"
|
"helm.sh/helm/v3/pkg/cli"
|
||||||
"helm.sh/helm/v3/pkg/registry"
|
"helm.sh/helm/v3/pkg/registry"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
@@ -23,25 +24,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
Version string
|
Version string
|
||||||
Namespaces []string
|
Namespaces []string
|
||||||
Address string
|
Address string
|
||||||
Debug bool
|
Debug bool
|
||||||
NoTracking bool
|
NoTracking bool
|
||||||
|
Devel bool
|
||||||
|
LocalCharts []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (string, utils.ControlChan, error) {
|
func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (string, utils.ControlChan, error) {
|
||||||
data, err := objects.NewDataLayer(s.Namespaces, s.Version, NewHelmConfig)
|
data, err := objects.NewDataLayer(s.Namespaces, s.Version, NewHelmConfig, s.Devel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, errorx.Decorate(err, "Failed to create data layer")
|
return "", nil, errorx.Decorate(err, "Failed to create data layer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.LocalCharts = s.LocalCharts
|
||||||
|
|
||||||
isDevModeWithAnalytics := os.Getenv("HD_DEV_ANALYTICS") == "true"
|
isDevModeWithAnalytics := os.Getenv("HD_DEV_ANALYTICS") == "true"
|
||||||
data.StatusInfo.Analytics = (!s.NoTracking && s.Version != "0.0.0") || isDevModeWithAnalytics
|
data.StatusInfo.Analytics = (!s.NoTracking && s.Version != "0.0.0") || isDevModeWithAnalytics
|
||||||
|
|
||||||
err = s.detectClusterMode(data)
|
err = s.detectClusterMode(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, errorx.Decorate(err, "Failed to detect cluster mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
go checkUpgrade(data.StatusInfo)
|
go checkUpgrade(data.StatusInfo)
|
||||||
@@ -75,7 +80,7 @@ func (s *Server) detectClusterMode(data *objects.DataLayer) error {
|
|||||||
}
|
}
|
||||||
ns, err := app.K8s.GetNameSpaces()
|
ns, err := app.K8s.GetNameSpaces()
|
||||||
if err != nil { // no point in continuing without kubectl context and k8s connection
|
if err != nil { // no point in continuing without kubectl context and k8s connection
|
||||||
return err
|
return errorx.InitializationFailed.Wrap(err, "No k8s cluster connection")
|
||||||
}
|
}
|
||||||
log.Debugf("Got %d namespaces listed", len(ns.Items))
|
log.Debugf("Got %d namespaces listed", len(ns.Items))
|
||||||
data.StatusInfo.ClusterMode = true
|
data.StatusInfo.ClusterMode = true
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ function checkUpgradeable(name) {
|
|||||||
if (!data || !data.length) {
|
if (!data || !data.length) {
|
||||||
btnUpgradeCheck.prop("disabled", true)
|
btnUpgradeCheck.prop("disabled", true)
|
||||||
btnUpgradeCheck.text("")
|
btnUpgradeCheck.text("")
|
||||||
$("#btnAddRepository").text("Add repository for it")
|
$("#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 {
|
} else {
|
||||||
$("#btnAddRepository").text("")
|
$("#btnAddRepository").text("")
|
||||||
btnUpgradeCheck.text("Check for new version")
|
btnUpgradeCheck.text("Check for new version")
|
||||||
@@ -56,13 +60,7 @@ function checkUpgradeable(name) {
|
|||||||
function popUpUpgrade(elm, ns, name, verCur, lastRev) {
|
function popUpUpgrade(elm, ns, name, verCur, lastRev) {
|
||||||
$("#upgradeModal .btn-confirm").prop("disabled", true)
|
$("#upgradeModal .btn-confirm").prop("disabled", true)
|
||||||
|
|
||||||
let chart = elm.repository + "/" + elm.name;
|
$('#upgradeModal').data("initial", !verCur)
|
||||||
if (!elm.name) {
|
|
||||||
chart = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#upgradeModal').data("chart", chart).data("initial", !verCur)
|
|
||||||
$('#upgradeModal form .chart-name').val(chart)
|
|
||||||
$('#upgradeModal').data("newManifest", "")
|
$('#upgradeModal').data("newManifest", "")
|
||||||
|
|
||||||
$("#upgradeModalLabel .name").text(elm.name)
|
$("#upgradeModalLabel .name").text(elm.name)
|
||||||
@@ -93,23 +91,26 @@ function popUpUpgrade(elm, ns, name, verCur, lastRev) {
|
|||||||
$.getJSON("/api/helm/repositories/versions?name=" + elm.name).fail(function (xhr) {
|
$.getJSON("/api/helm/repositories/versions?name=" + elm.name).fail(function (xhr) {
|
||||||
reportError("Failed to find chart in repo", xhr)
|
reportError("Failed to find chart in repo", xhr)
|
||||||
}).done(function (vers) {
|
}).done(function (vers) {
|
||||||
|
vers.sort((a, b) => (isNewerVersion(a.version, b.version)?1:-1))
|
||||||
|
|
||||||
// fill versions
|
// fill versions
|
||||||
$('#upgradeModal select').empty()
|
$('#upgradeModal select').empty()
|
||||||
for (let i = 0; i < vers.length; i++) {
|
for (let i = 0; i < vers.length; i++) {
|
||||||
const opt = $("<option value='" + vers[i].version + "'></option>");
|
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) {
|
if (vers[i].version === verCur) {
|
||||||
opt.html(vers[i].version + " ·")
|
opt.html(label + " ✓")
|
||||||
} else {
|
} else {
|
||||||
opt.html(vers[i].version)
|
opt.html(label)
|
||||||
}
|
}
|
||||||
$('#upgradeModal select').append(opt)
|
$('#upgradeModal select').append(opt)
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#upgradeModal select').val(elm.version).trigger("change").parent().show()
|
$('#upgradeModal select').val(elm.version).parent().show()
|
||||||
upgrPopUpCommon(verCur, ns, lastRev, name)
|
upgrPopUpCommon(verCur, ns, lastRev, name)
|
||||||
})
|
})
|
||||||
} else { // chart without repo reconfigure
|
} else { // chart without repo reconfigure
|
||||||
$('#upgradeModal select').empty().trigger("change").parent().hide()
|
$('#upgradeModal select').empty().parent().hide()
|
||||||
upgrPopUpCommon(verCur, ns, lastRev, name)
|
upgrPopUpCommon(verCur, ns, lastRev, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,9 +125,11 @@ function upgrPopUpCommon(verCur, ns, lastRev, name) {
|
|||||||
reportError("Failed to get charts values info", xhr)
|
reportError("Failed to get charts values info", xhr)
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
$("#upgradeModal textarea").val(data).data("dirty", false)
|
$("#upgradeModal textarea").val(data).data("dirty", false)
|
||||||
|
$('#upgradeModal select').trigger("change")
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
$("#upgradeModal textarea").val("").data("dirty", true)
|
$("#upgradeModal textarea").val("").data("dirty", true)
|
||||||
|
$('#upgradeModal select').trigger("change")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,9 +165,7 @@ function changeTimer() {
|
|||||||
if (reconfigTimeout) {
|
if (reconfigTimeout) {
|
||||||
window.clearTimeout(reconfigTimeout)
|
window.clearTimeout(reconfigTimeout)
|
||||||
}
|
}
|
||||||
reconfigTimeout = window.setTimeout(function () {
|
reconfigTimeout = window.setTimeout(requestChangeDiff, 500)
|
||||||
requestChangeDiff()
|
|
||||||
}, 500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#upgradeModal textarea").keyup(changeTimer)
|
$("#upgradeModal textarea").keyup(changeTimer)
|
||||||
@@ -173,12 +174,25 @@ $("#upgradeModal .rel-ns").keyup(changeTimer)
|
|||||||
|
|
||||||
$('#upgradeModal select').change(function () {
|
$('#upgradeModal select').change(function () {
|
||||||
const self = $(this)
|
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()
|
requestChangeDiff()
|
||||||
|
|
||||||
// fill reference values
|
// fill reference values
|
||||||
$("#upgradeModal .ref-vals").html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
$("#upgradeModal .ref-vals").html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||||
const chart = $("#upgradeModal").data("chart");
|
|
||||||
// TODO: if chart is empty, query different URL that will restore values without repo
|
// TODO: if chart is empty, query different URL that will restore values without repo
|
||||||
if (chart) {
|
if (chart) {
|
||||||
$.get("/api/helm/repositories/values?chart=" + chart + "&version=" + self.val()).fail(function (xhr) {
|
$.get("/api/helm/repositories/values?chart=" + chart + "&version=" + self.val()).fail(function (xhr) {
|
||||||
@@ -231,7 +245,6 @@ $('#upgradeModal .btn-scan').click(function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function requestChangeDiff() {
|
function requestChangeDiff() {
|
||||||
const self = $('#upgradeModal select');
|
|
||||||
const diffBody = $("#upgradeModalBody");
|
const diffBody = $("#upgradeModalBody");
|
||||||
diffBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Calculating diff...')
|
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 .btn-confirm").prop("disabled", true)
|
||||||
@@ -390,11 +403,16 @@ $("#btnRollback").click(function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
$("#btnAddRepository").click(function () {
|
$("#btnAddRepository").click(function () {
|
||||||
|
const self = $(this)
|
||||||
setHashParam("section", "repository")
|
setHashParam("section", "repository")
|
||||||
|
if (self.data("suggestRepo")) {
|
||||||
|
setHashParam("suggestRepo", self.data("suggestRepo"))
|
||||||
|
setHashParam("suggestRepoUrl", self.data("suggestRepoUrl"))
|
||||||
|
}
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
$("#btnTest").click(function() {
|
$("#btnTest").click(function () {
|
||||||
const myModal = new bootstrap.Modal(document.getElementById('testModal'), {});
|
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...')
|
$("#testModal .test-result").empty().prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Waiting for completion...')
|
||||||
myModal.show()
|
myModal.show()
|
||||||
@@ -406,7 +424,7 @@ $("#btnTest").click(function() {
|
|||||||
myModal.hide()
|
myModal.hide()
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
var output;
|
var output;
|
||||||
if(data.length == 0 || data == null || data == "") {
|
if (data.length == 0 || data == null || data == "") {
|
||||||
output = "<div>Tests executed successfully<br><br><pre>Empty response from API<pre></div>"
|
output = "<div>Tests executed successfully<br><br><pre>Empty response from API<pre></div>"
|
||||||
} else {
|
} else {
|
||||||
output = data.replaceAll("\n", "<br>")
|
output = data.replaceAll("\n", "<br>")
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
|
const TRACK_EVENT_TYPE = "track"
|
||||||
|
const IDENTIFY_EVENT_TYPE = "identify"
|
||||||
|
const BASE_ANALYTIC_MSG = {
|
||||||
|
method: "POST",
|
||||||
|
mode: "cors",
|
||||||
|
cache: "no-cache",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"api-key": "komodor.analytics@admin.com",
|
||||||
|
},
|
||||||
|
redirect: "follow",
|
||||||
|
referrerPolicy: "no-referrer"
|
||||||
|
}
|
||||||
xhr.onload = function () {
|
xhr.onload = function () {
|
||||||
|
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
const status = JSON.parse(xhr.responseText);
|
const status = JSON.parse(xhr.responseText);
|
||||||
const version = status.CurVer
|
const version = status.CurVer
|
||||||
if (status.Analytics) {
|
if (status.Analytics) {
|
||||||
enableDD(version)
|
enableDD(version)
|
||||||
enableHeap(version, status.ClusterMode)
|
enableHeap(version, status.ClusterMode)
|
||||||
|
enableSegmentBackend(version, status.ClusterMode)
|
||||||
|
} else {
|
||||||
|
console.log("Analytics is disabled in this session")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,12 +75,57 @@ function enableHeap(version, inCluster) {
|
|||||||
heap.load("4249623943");
|
heap.load("4249623943");
|
||||||
window.heap.addEventProperties({
|
window.heap.addEventProperties({
|
||||||
'version': version,
|
'version': version,
|
||||||
'installationMode': inCluster?"cluster":"local"
|
'installationMode': inCluster ? "cluster" : "local"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendStats(name, prop){
|
function sendStats(name, prop) {
|
||||||
if (window.heap) {
|
if (window.heap) {
|
||||||
window.heap.track(name, prop);
|
window.heap.track(name, prop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enableSegmentBackend(version, ClusterMode) {
|
||||||
|
sendToSegmentThroughAPI("helm dashboard loaded", {version, 'installationMode': ClusterMode ? "cluster" : "local"}, TRACK_EVENT_TYPE)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToSegmentThroughAPI(eventName, properties, segmentCallType) {
|
||||||
|
const userId = getUserId();
|
||||||
|
try {
|
||||||
|
sendData(properties, segmentCallType, userId, eventName);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("failed sending data to segment", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendData(data, eventType, userId, eventName) {
|
||||||
|
const body = createBody(eventType, userId, data, eventName);
|
||||||
|
return fetch(`https://api.komodor.com/analytics/segment/${eventType}`, {
|
||||||
|
...BASE_ANALYTIC_MSG,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBody(segmentCallType, userId, params, eventName) {
|
||||||
|
const data = {userId: userId};
|
||||||
|
if (segmentCallType === IDENTIFY_EVENT_TYPE) {
|
||||||
|
data["traits"] = params;
|
||||||
|
} else if (segmentCallType === TRACK_EVENT_TYPE) {
|
||||||
|
if (!eventName) {
|
||||||
|
throw new Error("no eventName parameter on segment track call");
|
||||||
|
}
|
||||||
|
data["properties"] = params;
|
||||||
|
data["eventName"] = eventName;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserId = (() => {
|
||||||
|
let userId = null;
|
||||||
|
return () => {
|
||||||
|
if (!userId) {
|
||||||
|
userId = crypto.randomUUID();
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|||||||
@@ -35,12 +35,12 @@
|
|||||||
const data = JSON.parse(request.responseText);
|
const data = JSON.parse(request.responseText);
|
||||||
display(data);
|
display(data);
|
||||||
} else {
|
} else {
|
||||||
alert("Failed to get "+ swaggerUrl)
|
alert("Failed to get " + swaggerUrl)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = function () {
|
request.onerror = function () {
|
||||||
alert("Failed to get "+ swaggerUrl)
|
alert("Failed to get " + swaggerUrl)
|
||||||
};
|
};
|
||||||
|
|
||||||
request.send();
|
request.send();
|
||||||
@@ -67,4 +67,5 @@
|
|||||||
reqOas();
|
reqOas();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="analytics.js"></script>
|
||||||
</html>
|
</html>
|
||||||
@@ -149,7 +149,7 @@ function showResources(namespace, chart, revision) {
|
|||||||
const resBody = $("#nav-resources .body");
|
const resBody = $("#nav-resources .body");
|
||||||
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
|
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
|
||||||
resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
|
resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
|
||||||
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources"
|
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources?health=true"
|
||||||
$.getJSON(url).fail(function (xhr) {
|
$.getJSON(url).fail(function (xhr) {
|
||||||
reportError("Failed to get list of resources", xhr)
|
reportError("Failed to get list of resources", xhr)
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
@@ -180,23 +180,32 @@ function showResources(namespace, chart, revision) {
|
|||||||
|
|
||||||
resBody.append(resBlock)
|
resBody.append(resBlock)
|
||||||
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
|
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
|
||||||
$.getJSON("/api/k8s/" + res.kind.toLowerCase() + "/get?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
|
for (let k = 0; k < res.status.conditions.length; k++) {
|
||||||
//reportError("Failed to get list of resources")
|
if (res.status.conditions[k].type !== "hdHealth") { // it's our custom condition type
|
||||||
}).done(function (data) {
|
continue
|
||||||
const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase);
|
}
|
||||||
if (["Available", "Active", "Established", "Bound", "Ready"].includes(data.status.phase)) {
|
|
||||||
|
const cond = res.status.conditions[k]
|
||||||
|
|
||||||
|
const badge = $("<span class='badge me-2 fw-normal'></span>").text(cond.reason);
|
||||||
|
if (cond.status === "Unknown") {
|
||||||
|
badge.addClass("bg-secondary text-danger")
|
||||||
|
} else if (cond.status === "Healthy") {
|
||||||
badge.addClass("bg-success text-dark")
|
badge.addClass("bg-success text-dark")
|
||||||
} else if (["Exists"].includes(data.status.phase)) {
|
} else if (cond.status === "Progressing") {
|
||||||
badge.addClass("bg-success text-dark bg-opacity-50")
|
|
||||||
} else if (["Progressing"].includes(data.status.phase)) {
|
|
||||||
badge.addClass("bg-warning")
|
badge.addClass("bg-warning")
|
||||||
} else {
|
} else {
|
||||||
badge.addClass("bg-danger")
|
badge.addClass("bg-danger")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (["Exists"].includes(cond.reason)) {
|
||||||
|
badge.addClass("bg-opacity-50")
|
||||||
|
}
|
||||||
|
|
||||||
const statusBlock = resBlock.find(".res-status");
|
const statusBlock = resBlock.find(".res-status");
|
||||||
statusBlock.empty().append(badge).attr("title", data.status.phase)
|
statusBlock.empty().append(badge).attr("title", cond.reason)
|
||||||
const statusMessage = getStatusMessage(data.status)
|
const statusMessage = cond.message
|
||||||
resBlock.find(".res-statusmsg").html("<span class='text-muted small'>" + (statusMessage ? statusMessage : '') + "</span>")
|
resBlock.find(".res-statusmsg").html("<span class='text-muted small me-2'>" + (statusMessage ? statusMessage : '') + "</span>")
|
||||||
|
|
||||||
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
|
if (badge.text() !== "NotFound" && revision == $("#specRev").data("last-rev")) {
|
||||||
resBlock.find(".res-actions")
|
resBlock.find(".res-actions")
|
||||||
@@ -215,21 +224,15 @@ function showResources(namespace, chart, revision) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
if (badge.hasClass("bg-danger") || badge.hasClass("bg-warning")) {
|
||||||
|
resBlock.find(".res-statusmsg").append("<a href='" + KomodorCTALink + "' class='btn btn-primary btn-sm fw-normal fs-80' target='_blank'>Troubleshoot in Komodor <i class='bi-box-arrow-up-right'></i></a>")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
function showDescribe(ns, kind, name, badge) {
|
||||||
$("#describeModal .offcanvas-header p").text(kind)
|
$("#describeModal .offcanvas-header p").text(kind)
|
||||||
$("#describeModalLabel").text(name).append(badge.addClass("ms-3 small fw-normal"))
|
$("#describeModalLabel").text(name).append(badge.addClass("ms-3 small fw-normal"))
|
||||||
|
|||||||
@@ -64,15 +64,20 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="nav-item mx-2 display-none upgrade-possible">
|
<li class="nav-item mx-2 display-none upgrade-possible">
|
||||||
<a class="nav-link position-relative text-danger"
|
<a class="nav-link position-relative text-danger"
|
||||||
href="https://github.com/komodorio/helm-dashboard#installing" target="_blank">
|
href="https://github.com/komodorio/helm-dashboard/releases" target="_blank">
|
||||||
Upgrade to <span id="toolVersionUpgrade"></span>
|
Upgrade to <span id="toolVersionUpgrade"></span>
|
||||||
</a></li>
|
</a></li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<div>
|
<div>
|
||||||
<a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img
|
<div class="border-muted text-muted border rounded p-1 pe-2 me-3 d-flex">
|
||||||
src="static/komodor-logo.svg" alt="komodor.io"
|
<img alt="Komodor" src="https://raw.githubusercontent.com/komodorio/helm-charts/master/k8s-watcher.svg" class="me-2" style="width: 42px; height: 42px"/>
|
||||||
style="height: 1.2rem; vertical-align: text-bottom; filter: grayscale(00%);"></a>
|
<span class="text-nowrap">
|
||||||
|
<a href="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash"
|
||||||
|
class="link text-primary fw-bold text-decoration-none" target="_blank">Upgrade your HELM experience - Free
|
||||||
|
<i class="bi-box-arrow-up-right ms-1"></i></a><br/>
|
||||||
|
Auth & RBAC, k8s events, troubleshooting and more
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="separator-vertical"><span></span></div>
|
<div class="separator-vertical"><span></span></div>
|
||||||
<i class="btn bi-power text-muted p-2 m-1 mx-2" title="Shut down the Helm Dashboard application"></i>
|
<i class="btn bi-power text-muted p-2 m-1 mx-2" title="Shut down the Helm Dashboard application"></i>
|
||||||
@@ -90,6 +95,9 @@
|
|||||||
<button class="btn btn-sm border-secondary text-muted">
|
<button class="btn btn-sm border-secondary text-muted">
|
||||||
<i class="bi-plus-lg"></i> Add Repository
|
<i class="bi-plus-lg"></i> Add Repository
|
||||||
</button>
|
</button>
|
||||||
|
<div class="mt-2 p-2 small">Charts developers: you can also add local directories as chart source. Use
|
||||||
|
<span class="font-monospace text-success">--local-chart</span> CLI switch to specify it.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-9 repo-details bg-white b-shadow pt-4 px-5 overflow-auto rounded">
|
<div class="col-9 repo-details bg-white b-shadow pt-4 px-5 overflow-auto rounded">
|
||||||
@@ -154,14 +162,6 @@
|
|||||||
placeholder="Filter..."/>
|
placeholder="Filter..."/>
|
||||||
</div>
|
</div>
|
||||||
</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">Chart Status</div>
|
|
||||||
<div class="col-2">Chart</div>
|
|
||||||
<div class="col-1">Revision</div>
|
|
||||||
<div class="col-1">Namespace</div>
|
|
||||||
<div class="col-1">Updated</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="body"></div>
|
<div class="body"></div>
|
||||||
@@ -305,6 +305,8 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<p style="white-space: pre-wrap"></p>
|
<p style="white-space: pre-wrap"></p>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<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>
|
||||||
|
|
||||||
<div class="offcanvas offcanvas-end rounded-start" tabindex="-1" id="describeModal"
|
<div class="offcanvas offcanvas-end rounded-start" tabindex="-1" id="describeModal"
|
||||||
@@ -314,8 +316,12 @@
|
|||||||
<h5 id="describeModalLabel"></h5>
|
<h5 id="describeModalLabel"></h5>
|
||||||
<p class="m-0 mt-4">ResourceType</p>
|
<p class="m-0 mt-4">ResourceType</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
<div>
|
||||||
|
<a href='https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash'
|
||||||
|
class='btn btn-primary btn-sm me-2' target='_blank'>See more details in Komodor <i
|
||||||
|
class='bi-box-arrow-up-right'></i></a>
|
||||||
|
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="offcanvas-body p-2 ps-4" id="describeModalBody">
|
<div class="offcanvas-body p-2 ps-4" id="describeModalBody">
|
||||||
</div>
|
</div>
|
||||||
@@ -348,8 +354,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form enctype="application/x-www-form-urlencoded">
|
<form enctype="application/x-www-form-urlencoded">
|
||||||
<label class="form-label">Name: <input class="form-control" name="name"></label>
|
<div class="row mb-4">
|
||||||
<label class="form-label">URL: <input class="form-control" name="url"></label>
|
<div class="col">
|
||||||
|
<label class="form-label required">Name</label>
|
||||||
|
<input class="form-control" type="text" name="name" placeholder="Komodorio">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label required">URL</label>
|
||||||
|
<input class="form-control" type="text" name="url" placeholder="https://helm-charts.komodor.io">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Username</label>
|
||||||
|
<input class="form-control" type="text" name="username">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label class="form-label">Password</label>
|
||||||
|
<input class="form-control" type="password" name="password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -479,47 +503,5 @@
|
|||||||
<script src="static/actions.js"></script>
|
<script src="static/actions.js"></script>
|
||||||
<script src="static/scripts.js"></script>
|
<script src="static/scripts.js"></script>
|
||||||
|
|
||||||
<!-- BANNER START -->
|
|
||||||
<a id="banner"
|
|
||||||
href="https://helm-dashboard-survey.komodor.com/"
|
|
||||||
class="display-none position-absolute top-0 start-50 translate-middle-x bg-primary text-light rounded px-2 mt-1 text-decoration-none py-1">Help
|
|
||||||
shaping the future by participating in user survey <b class="bi-x-lg"></b></a>
|
|
||||||
<script>
|
|
||||||
function setCookie(name, value, days) {
|
|
||||||
let expires = "";
|
|
||||||
if (days) {
|
|
||||||
const date = new Date();
|
|
||||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
||||||
expires = "; expires=" + date.toUTCString();
|
|
||||||
}
|
|
||||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
const nameEQ = name + "=";
|
|
||||||
const ca = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < ca.length; i++) {
|
|
||||||
const c = ca[i].trim();
|
|
||||||
if (c.indexOf(nameEQ) === 0) {
|
|
||||||
return c.substring(nameEQ.length, c.length)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookie = getCookie("hideBanner");
|
|
||||||
if (cookie == null) {
|
|
||||||
console.log("show")
|
|
||||||
$("#banner").show()
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#banner b").click(function (evt) {
|
|
||||||
evt.preventDefault()
|
|
||||||
setCookie("hideBanner", "1", 365);
|
|
||||||
$("#banner").hide()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<!-- /BANNER END -->
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -79,6 +79,63 @@ function buildChartCard(elm) {
|
|||||||
|
|
||||||
loadChartHistory(chart.namespace, chart.name, elm.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='fw-bold' data-bs-toggle='tooltip' data-bs-placement='bottom'></span>")
|
||||||
|
if (data[0].isSuggestedRepo) {
|
||||||
|
icon.addClass("bi-plus-circle-fill text-primary")
|
||||||
|
icon.text(" ADD REPO")
|
||||||
|
icon.attr("data-bs-title", "Add '" + data[0].repository + "' to list of known repositories")
|
||||||
|
} else {
|
||||||
|
icon.addClass("bi-arrow-up-circle-fill text-primary")
|
||||||
|
icon.text(" UPGRADE")
|
||||||
|
icon.attr("data-bs-title", "Upgrade available: " + data[0].version + " from " + data[0].repository)
|
||||||
|
}
|
||||||
|
card.find(".rel-chart div").append(icon)
|
||||||
|
|
||||||
|
const tooltipTriggerList = card.find('.rel-chart [data-bs-toggle="tooltip"]')
|
||||||
|
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||||
|
sendStats('upgradeIconShown', {'isProbable': data[0].isSuggestedRepo})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// check resource health status
|
||||||
|
$.getJSON("/api/helm/releases/" + elm.namespace + "/" + elm.name + "/resources?health=true").fail(function (xhr) {
|
||||||
|
reportError("Failed to find chart in repo", xhr)
|
||||||
|
}).done(function (data) {
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const res = data[i]
|
||||||
|
for (let k = 0; k < res.status.conditions.length; k++) {
|
||||||
|
if (res.status.conditions[k].type !== "hdHealth") { // it's our custom condition type
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const cond = res.status.conditions[k]
|
||||||
|
const square=$("<span class='me-1 mb-1 square rounded rounded-1' data-bs-toggle='tooltip'> </span>")
|
||||||
|
if (cond.status === "Healthy") {
|
||||||
|
square.addClass("bg-success")
|
||||||
|
} else if (cond.status === "Progressing") {
|
||||||
|
square.addClass("bg-warning")
|
||||||
|
} else {
|
||||||
|
square.addClass("bg-danger")
|
||||||
|
}
|
||||||
|
square.attr("data-bs-title", cond.status+" "+res.kind+" '"+res.metadata.name+"'")
|
||||||
|
card.find(".rel-status div").append(square)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipTriggerList = card.find('.rel-status [data-bs-toggle="tooltip"]')
|
||||||
|
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||||
|
})
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,11 @@
|
|||||||
"name": "name",
|
"name": "name",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"description": "Name of Helm release"
|
"description": "Name of Helm release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "health",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Flag to query k8s health status of resources"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"get": {
|
"get": {
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ function loadRepoView() {
|
|||||||
$("#sectionRepo .repo-details").hide()
|
$("#sectionRepo .repo-details").hide()
|
||||||
$("#sectionRepo").show()
|
$("#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) {
|
$.getJSON("/api/helm/repositories").fail(function (xhr) {
|
||||||
reportError("Failed to get list of repositories", xhr)
|
reportError("Failed to get list of repositories", xhr)
|
||||||
sendStats('Get repo', {'status': 'fail'});
|
sendStats('Get repo', {'status': 'fail'});
|
||||||
@@ -10,7 +17,7 @@ function loadRepoView() {
|
|||||||
data.sort((a, b) => (a.name > b.name) - (a.name < b.name))
|
data.sort((a, b) => (a.name > b.name) - (a.name < b.name))
|
||||||
|
|
||||||
data.forEach(function (elm) {
|
data.forEach(function (elm) {
|
||||||
let opt = $('<li class="mb-2"><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
|
let opt = $('<li class="mb-2"><label><input type="radio" name="repo" class="me-2"/><span></span></label></li>');
|
||||||
opt.attr('title', elm.url)
|
opt.attr('title', elm.url)
|
||||||
opt.find("input").val(elm.name).text(elm.name).data("item", elm)
|
opt.find("input").val(elm.name).text(elm.name).data("item", elm)
|
||||||
opt.find("span").text(elm.name)
|
opt.find("span").text(elm.name)
|
||||||
@@ -30,6 +37,8 @@ function loadRepoView() {
|
|||||||
$("#sectionRepo .repo-details h2").text(elm.name)
|
$("#sectionRepo .repo-details h2").text(elm.name)
|
||||||
$("#sectionRepo .repo-details .url").text(elm.url)
|
$("#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>')
|
$("#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) {
|
$.getJSON("/api/helm/repositories/" + elm.name).fail(function (xhr) {
|
||||||
reportError("Failed to get list of charts in repo", xhr)
|
reportError("Failed to get list of charts in repo", xhr)
|
||||||
@@ -83,6 +92,8 @@ $("#inputSearch").keyup(function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
$("#sectionRepo .repo-list .btn").click(function () {
|
$("#sectionRepo .repo-list .btn").click(function () {
|
||||||
|
setHashParam("suggestRepo", null)
|
||||||
|
setHashParam("suggestRepoUrl", null)
|
||||||
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
|
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
|
||||||
myModal.show()
|
myModal.show()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ $("#topNav ul a").click(function () {
|
|||||||
initView()
|
initView()
|
||||||
})
|
})
|
||||||
|
|
||||||
const myAlert = document.getElementById('errorAlert')
|
const errAlert = document.getElementById('errorAlert')
|
||||||
myAlert.addEventListener('close.bs.alert', event => {
|
errAlert.addEventListener('close.bs.alert', event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
$("#errorAlert").hide()
|
$("#errorAlert").hide()
|
||||||
})
|
})
|
||||||
@@ -129,6 +129,7 @@ function reportError(err, xhr) {
|
|||||||
$("#errorAlert p").text(xhr.responseText)
|
$("#errorAlert p").text(xhr.responseText)
|
||||||
}
|
}
|
||||||
$("#errorAlert").show()
|
$("#errorAlert").show()
|
||||||
|
sendStats("errorReported", {"errMessage": err})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -356,4 +357,6 @@ function setFilteredNamespaces(filteredNamespaces) {
|
|||||||
} else if (filteredNamespaces.length !== 0) {
|
} else if (filteredNamespaces.length !== 0) {
|
||||||
setHashParam("filteredNamespace", filteredNamespaces.join('+'))
|
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"
|
||||||
@@ -79,3 +79,8 @@
|
|||||||
.fs-80 {
|
.fs-80 {
|
||||||
font-size: 0.8rem!important;
|
font-size: 0.8rem!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.required::after {
|
||||||
|
content: " *";
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
@@ -300,4 +300,27 @@ nav .nav-tabs .nav-link.active {
|
|||||||
|
|
||||||
.test-result {
|
.test-result {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square {
|
||||||
|
width: 0.55rem;
|
||||||
|
height: 0.55rem;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 0.1rem!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square.bg-danger {
|
||||||
|
background-color: #ff0072!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square.bg-warning {
|
||||||
|
background-color: #ffa800!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square.bg-success {
|
||||||
|
background-color: #00c2ab!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
overflow-x: inherit;
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,6 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -34,12 +33,12 @@ func ChartAndVersion(x string) (string, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TempFile(txt string) (string, func(), error) {
|
func TempFile(txt string) (string, func(), error) {
|
||||||
file, err := ioutil.TempFile("", "helm_dahsboard_*.yaml")
|
file, err := os.CreateTemp("", "helm_dahsboard_*.yaml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(file.Name(), []byte(txt), 0600)
|
err = os.WriteFile(file.Name(), []byte(txt), 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: "dashboard"
|
name: "dashboard"
|
||||||
version: "0.3.1"
|
version: "1.3.2"
|
||||||
usage: "A simplified way of working with Helm"
|
usage: "A simplified way of working with Helm"
|
||||||
description: "View HELM situation in nice web UI"
|
description: "View HELM situation in nice web UI"
|
||||||
command: "$HELM_PLUGIN_DIR/bin/helm-dashboard"
|
command: "$HELM_PLUGIN_DIR/bin/helm-dashboard"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Copied w/ love from the chartmuseum/helm-push :)
|
# Copied w/ love from the chartmuseum/helm-push :)
|
||||||
|
|
||||||
[ -z "$HELM_DEBUG" ] || set -x
|
[ ! -z "$HELM_DEBUG" ] && set -x
|
||||||
|
|
||||||
name="helm-dashboard"
|
name="helm-dashboard"
|
||||||
repo="https://github.com/komodorio/${name}"
|
repo="https://github.com/komodorio/${name}"
|
||||||
|
|||||||