1 Commits

Author SHA1 Message Date
ronahk
eef3cd94b7 release 0.3.1 2022-12-19 14:23:55 +02:00
72 changed files with 1390 additions and 5776 deletions

View File

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

68
.github/labeler.yml vendored
View File

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

View File

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

View File

@@ -4,7 +4,7 @@ on:
push: push:
branches: main branches: main
pull_request: pull_request:
branches: "*" branches: main
jobs: jobs:
build: build:
@@ -18,7 +18,7 @@ jobs:
go-version: 1.18 go-version: 1.18
- 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 ./... # Run all the tests with the race detector enabled
- 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
@@ -34,7 +34,7 @@ jobs:
- name: Test Binary is Runnable - name: Test Binary is Runnable
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help" run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3.3.1 uses: golangci/golangci-lint-action@v3.2.0
with: with:
# version: latest # version: latest
# skip-go-installation: true # skip-go-installation: true
@@ -47,7 +47,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Helm Template Check For Sanity - name: Helm Template Check For Sanity
@@ -68,6 +68,3 @@ 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
run: |
scripts/install_plugin.sh

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Bump versions - name: Bump versions

View File

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

View File

@@ -18,16 +18,26 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Get plugin version
id: get_plugin_version
run: echo "PLUGIN_VERSION=$(cat plugin.yaml | grep "version" | cut -d '"' -f 2)" >> $GITHUB_OUTPUT
- name: Get tag name - name: Get tag name
id: get_tag_name id: get_tag_name
run: echo "TAG_NAME=$(echo ${{ github.ref_name }} | cut -d 'v' -f2)" >> $GITHUB_OUTPUT run: echo "TAG_NAME=$(echo ${{ github.ref_name }} | cut -d 'v' -f2)" >> $GITHUB_OUTPUT
outputs: outputs:
plugin_version: ${{ steps.get_plugin_version.outputs.PLUGIN_VERSION }}
release_tag: ${{ steps.get_tag_name.outputs.TAG_NAME }} release_tag: ${{ steps.get_tag_name.outputs.TAG_NAME }}
release: release:
needs: pre_release needs: pre_release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Plugin version/Tag name Check
if: needs.pre_release.outputs.release_tag != needs.pre_release.outputs.plugin_version
uses: actions/github-script@v3
with:
script: |
core.setFailed('Plugin version and tag name are not equivalent!')
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
@@ -54,7 +64,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3 uses: actions/checkout@v2
- name: Docker meta - name: Docker meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v3
@@ -85,7 +95,7 @@ jobs:
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Bump versions - name: Bump versions
@@ -96,7 +106,6 @@ jobs:
git checkout main git checkout main
sh ./ci/bump-versions.sh sh ./ci/bump-versions.sh
git add charts/helm-dashboard/Chart.yaml git add charts/helm-dashboard/Chart.yaml
git add plugin.yaml
git commit -m "Increment chart versions [skip ci]" || echo "Already up-to-date" git commit -m "Increment chart versions [skip ci]" || echo "Already up-to-date"
git push -f || echo "Nothing to push!" git push -f || echo "Nothing to push!"
env: env:

2
.gitignore vendored
View File

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

View File

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

View File

@@ -23,11 +23,13 @@ WORKDIR /build/src
RUN make build RUN make build
# Stage - runner # Stage - runner
FROM alpine FROM alpine/helm
EXPOSE 8080
# Python # Python
RUN apk add --update --no-cache python3 curl && python3 -m ensurepip && pip3 install --upgrade pip setuptools RUN apk add --update --no-cache python3 && python3 -m ensurepip && pip3 install --upgrade pip setuptools
# kubectl
RUN curl -o /bin/kubectl -vf -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && chmod +x /bin/kubectl && kubectl --help
# Trivy # Trivy
RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.18.3 RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.18.3
@@ -38,6 +40,6 @@ RUN pip3 install checkov packaging==21.3 && checkov --version
COPY --from=builder /build/src/bin/dashboard /bin/helm-dashboard COPY --from=builder /build/src/bin/dashboard /bin/helm-dashboard
ENTRYPOINT ["/bin/helm-dashboard", "--no-browser", "--bind=0.0.0.0", "--port=8080"] ENTRYPOINT ["/bin/helm-dashboard", "--no-browser", "--bind=0.0.0.0"]
# docker build . -t komodorio/helm-dashboard:0.0.0 && kind load docker-image komodorio/helm-dashboard:0.0.0 # docker build . -t komodorio/helm-dashboard:0.0.0 && kind load docker-image komodorio/helm-dashboard:0.0.0

View File

@@ -4,7 +4,7 @@ VERSION ?= $(git describe --tags --always --dirty --match=v* 2> /dev/null || \
.PHONY: test .PHONY: test
test: ; $(info $(M) start unit testing...) @ test: ; $(info $(M) start unit testing...) @
@go test $$(go list ./... | grep -v /mocks/) --race -v -short -coverpkg=./... -coverprofile=profile.cov @go test $$(go list ./... | grep -v /mocks/) --race -v -short -coverprofile=profile.cov
@echo "\n*****************************" @echo "\n*****************************"
@echo "** TOTAL COVERAGE: $$(go tool cover -func profile.cov | grep total | grep -Eo '[0-9]+\.[0-9]+')% **" @echo "** TOTAL COVERAGE: $$(go tool cover -func profile.cov | grep total | grep -Eo '[0-9]+\.[0-9]+')% **"
@echo "*****************************\n" @echo "*****************************\n"

View File

@@ -1,49 +1,32 @@
<p align="center"> # ![Helm Dashboard](pkg/dashboard/static/logo-header.svg#gh-light-mode-only) ![Helm Dashboard](pkg/dashboard/static/logo-header-inverted.svg#gh-dark-mode-only)
<picture>
<source media="(prefers-color-scheme: dark)" srcset="pkg/dashboard/static/logo-header-inverted.svg">
<source media="(prefers-color-scheme: light)" srcset="pkg/dashboard/static/logo-header.svg#gh-light-mode-only">
<img alt="Helm Dashboard" src="pkg/dashboard/static/logo-header.svg#gh-light-mode-only">
</picture>
</p>
<p align="center">A simplified way of working with Helm.</p> A simplified way of working with Helm.
<kbd>[<img src="screenshot.png" style="width: 100%; border: 1px solid silver;" border="1" alt="Screenshot">](screenshot.png)</kbd>
![GitHub contributors](https://img.shields.io/github/contributors/komodorio/helm-dashboard) [![GitHub issues](https://img.shields.io/github/issues-raw/komodorio/helm-dashboard)](https://github.com/komodorio/helm-dashboard/issues) ![GitHub stars](https://img.shields.io/github/stars/komodorio/helm-dashboard?style=social) ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/komodorio/helm-dashboard) ![GitHub pull requests](https://img.shields.io/github/issues-pr/komodorio/helm-dashboard) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/komodorio/helm-dashboard)](https://github.com/komodorio/helm-dashboard/releases) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/komodorio/helm-dashboard) [![GitHub license](https://img.shields.io/github/license/komodorio/helm-dashboard)](https://github.com/komodorio/helm-dashboard) ## What it Does?
<kbd>[<img src="images/screenshot.png" style="width: 100%; border: 1px solid silver;" border="1" alt="Screenshot">](images/screenshot.png)</kbd> _Helm Dashboard_ offers a UI-driven way to view the installed Helm charts, see their revision history and
## Description
_Helm Dashboard_ is an **open-source project** which offers a UI-driven way to view the installed Helm charts, see their revision history and
corresponding k8s resources. Also, you can perform simple actions like roll back to a revision or upgrade to newer 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 offical project by the [helm team](https://helm.sh/).
Key capabilities of the tool: Some of the key capabilities of the tool:
- See all installed charts and their revision history - See all installed charts and their revision history
- See manifest diff of the past revisions - See manifest diff of the past revisions
- Browse k8s resources resulting from the chart - Browse k8s resources resulting from the chart
- Easy rollback or upgrade version with a clear and easy manifest diff - Easy rollback or upgrade version with a clear and easy manifest diff
- Integration with popular problem scanners - Integration with popular problem scanners
- Easy switch between multiple clusters - Easy switch between multiple clusters
- Can be used locally, or installed into Kubernetes cluster
- Does not require Helm or Kubectl installed
## Setup ## Setup
### Standalone Binary
Since version 1.0, the recommended install method is to just use standalone binary. It does not require Helm or kubectl to be installed.
Download the appropriate [release package](https://github.com/komodorio/helm-dashboard/releases) for your platform, unpack it and just run `dashboard` binary from it.
### Using Helm plugin manager ### Using Helm plugin manager
To install dashboard as Helm plugin, simply run Helm command: To install the plugin, simply run Helm command:
```shell ```shell
helm plugin install https://github.com/komodorio/helm-dashboard.git helm plugin install https://github.com/komodorio/helm-dashboard.git
@@ -82,7 +65,7 @@ This can also be specified using flag `--bind <host>`, for example `--bind=0.0.0
If your port 8080 is busy, you can specify a different port to use via `--port <number>` command-line flag. If your port 8080 is busy, you can specify a different port to use via `--port <number>` command-line flag.
If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line. You can specify multiple namespaces, separated by commas. If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line.
If you don't want browser tab to automatically open, add `--no-browser` flag in your command line. If you don't want browser tab to automatically open, add `--no-browser` flag in your command line.
@@ -94,82 +77,40 @@ 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 ### Manual Installation
### Support for Local Charts Download the appropriate [release package](https://github.com/komodorio/helm-dashboard/releases) for your platform, unpack it and just run `dashboard` binary from it.
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. ## Scanner Integrations
For all the above use-cases, you may use Helm Dashboard UI, spcifying location of your local chart folders via special `--local-chart` command-line parameter. The parameter might be specified multiple times, for example:
```shell
helm-dashboard --local-chart=/opt/charts/my-private-app --local-chart=/home/dev/sources/app/chart
```
When _valid_ local chart sources specified, the repository list would contain a surrogate `[local]` entry, with those charts listed inside. All the chart operations are normal: installing, reconfiguring and upgrading.
![](images/screenshot_local_charts.png)
### Execute Helm tests
For all the release(s) (installed helm charts), you can execute helm tests for that release. For the tests to execute successfully, you need to have existing tests for that helm chart.
You can execute `helm test` for the specific release as below:
![](images/screenshot_run_test.png)
The result of executed `helm test` for the release will be displayed as below:
![](images/screenshot_run_test_result.png)
### Scanner Integrations
Upon startup, Helm Dashboard detects the presence of [Trivy](https://github.com/aquasecurity/trivy) 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:
![](images/screenshot_scan_resource.png) ![](screenshot_scan_resource.png)
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:
![](images/screenshot_scan_manifest.png) ![](screenshot_scan_manifest.png)
## Support Channels ## Support Channels
We have two main channels for supporting the Helm Dashboard We have two main channels for supporting the Helm Dashboard
users: [Slack community](https://komodorkommunity.slack.com) for general conversations users: [Slack community](https://join.slack.com/t/komodorkommunity/shared_invite/zt-1dm3cnkue-ov1Yh~_95teA35QNx5yuMg) for general conversations
and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs. and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
## Contributing
Kindly read our [Contributing Guide](CONTRIBUTING.md) to learn and understand about our development process, how to propose bug fixes and improvements, and how to build and test your changes to Helm Dashboard. <br>
## Contributors
<a href="https://github.com/komodorio/helm-dashboard/graphs/contributors">
<img src="https://contrib.rocks/image?repo=komodorio/helm-dashboard" />
</a>
## Local Dev Testing ## Local Dev Testing
Prerequisites, binaries installed and operational: Prerequisites: `helm` and `kubectl` 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 .
``` ```
### Windows You can just run the `bin/dashboard` binary directly, it will just work.
```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:

View File

@@ -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.7 version: 0.1.1
appVersion: "1.3.0" appVersion: "0.3.0"

View File

@@ -5,25 +5,24 @@
```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 helm-dashboard komodorio/helm-dashboard helm upgrade --install my-release komodorio/helm-dashboard
``` ```
## Introduction ## Introduction
This chart bootstraps a Helm Dashboard deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. This chart bootstraps a Helm Dashboard deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
While installed inside cluster, Helm Dashboard will run some additional backgroud actions, for example, will automatically update Helm repositories. To enable that behavior locally, set `HD_CLUSTER_MODE` env variable.
## Prerequisites ## Prerequisites
- Kubernetes 1.16+ - Kubernetes 1.16+
- Helm 3+
## Installing the Chart ## Installing the Chart
To install the chart with the release name `helm-dashboard`: To install the chart with the release name `my-release`:
```bash ```bash
helm install helm-dashboard . helm install my-release .
``` ```
The command deploys Helm Dashboard on the Kubernetes cluster in the default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. 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.
@@ -32,20 +31,14 @@ The command deploys Helm Dashboard on the Kubernetes cluster in the default conf
## Uninstalling the Chart ## Uninstalling the Chart
To uninstall/delete the `helm-dashboard` deployment: To uninstall/delete the `my-release` deployment:
```bash ```bash
helm uninstall helm-dashboard helm uninstall my-release
``` ```
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.
## Adding Authentication
The task of authentication and user control is out of scope for Helm Dashboard. Luckily, there are third-party solutions which are dedicated to provide that functionality.
For instance, you can place authentication proxy in front of Helm Dashboard, like this one: https://github.com/oauth2-proxy/oauth2-proxy
## Parameters ## Parameters
The following table lists the configurable parameters of the chart and their default values. The following table lists the configurable parameters of the chart and their default values.
@@ -72,14 +65,12 @@ The following table lists the configurable parameters of the chart and their def
| `dashboard.persistence.accessModes` | Persistent Volume access modes | `["ReadWriteOnce"]` | | `dashboard.persistence.accessModes` | Persistent Volume access modes | `["ReadWriteOnce"]` |
| `dashboard.persistence.storageClass` | Persistent Volume storage class | `""` | | `dashboard.persistence.storageClass` | Persistent Volume storage class | `""` |
| `dashboard.persistence.size` | Persistent Volume size | `100M` | | `dashboard.persistence.size` | Persistent Volume size | `100M` |
| `dashboard.persistence.hostPath` | Set path in case you want to use local host path volumes (not recommended in production) | `""` | `dashboard.persistence.hostPath` | Set path in case you want to use local host path volumes (not recommended in production) | `""` |
| `updateStrategy.type` | Set up update strategy for helm-dashboard installation. | `RollingUpdate` |
| `extraArgs` | Set the arguments to be supplied to the helm-dashboard binary | `[--no-browser, --bind=0.0.0.0]`
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`.
```bash ```bash
helm upgrade --install helm-dashboard komodorio/helm-dashboard --set dashboard.allowWriteActions=true --set service.port=9090 helm upgrade --install my-release komodorio/helm-dashboard --set dashboard.allowWriteActions=true --set service.port=9090
``` ```
> **Tip**: You can use the default [values.yaml](values.yaml) > **Tip**: You can use the default [values.yaml](values.yaml)

View File

@@ -11,7 +11,6 @@ spec:
selector: selector:
matchLabels: matchLabels:
{{- include "helm-dashboard.selectorLabels" . | nindent 6 }} {{- include "helm-dashboard.selectorLabels" . | nindent 6 }}
strategy: {{- toYaml .Values.updateStrategy | nindent 4 }}
template: template:
metadata: metadata:
{{- with .Values.podAnnotations }} {{- with .Values.podAnnotations }}
@@ -30,29 +29,17 @@ spec:
{{- toYaml .Values.podSecurityContext | nindent 8 }} {{- toYaml .Values.podSecurityContext | nindent 8 }}
containers: containers:
- name: {{ .Chart.Name }} - name: {{ .Chart.Name }}
command:
- /bin/helm-dashboard
args:
{{- with .Values.extraArgs }}
{{- toYaml . | nindent 12 }}
{{- end }}
securityContext: securityContext:
{{- toYaml .Values.securityContext | nindent 12 }} {{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
env: env:
- name: HELM_CACHE_HOME - name: HELM_CACHE_HOME
value: /opt/dashboard/helm/cache value: /opt/dashboard/helm/cache
- name: HELM_CONFIG_HOME - name: HELM_CONFIG_HOME
value: /opt/dashboard/helm/config value: /opt/dashboard/helm/config
- name: HELM_DATA_HOME - name: HELM_DATA_HOME
value: /opt/dashboard/helm/data value: /opt/dashboard/helm/data
- name: DEBUG
value: {{- ternary " 1" "" .Values.debug }}
{{- if .Values.dashboard.namespace }}
- name: HELM_NAMESPACE
value: {{ .Values.dashboard.namespace }}
{{end}}
ports: ports:
- name: http - name: http
containerPort: 8080 containerPort: 8080
@@ -68,8 +55,8 @@ spec:
resources: resources:
{{- toYaml .Values.resources | nindent 12 }} {{- toYaml .Values.resources | nindent 12 }}
volumeMounts: volumeMounts:
- name: data - name: data
mountPath: /opt/dashboard/helm mountPath: /opt/dashboard/helm
{{- with .Values.nodeSelector }} {{- with .Values.nodeSelector }}
nodeSelector: nodeSelector:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
@@ -88,6 +75,5 @@ spec:
persistentVolumeClaim: persistentVolumeClaim:
claimName: {{ include "helm-dashboard.fullname" . }} claimName: {{ include "helm-dashboard.fullname" . }}
{{- else }} {{- else }}
emptyDir: { } emptyDir: {}
{{- end }} {{- end }}

View File

@@ -13,10 +13,6 @@ metadata:
spec: spec:
{{- if .Values.dashboard.persistence.hostPath }} {{- if .Values.dashboard.persistence.hostPath }}
storageClassName: "" storageClassName: ""
{{- else }}
{{- if kindIs "string" .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) }}
@@ -44,13 +40,9 @@ metadata:
{{- end }} {{- end }}
spec: spec:
accessModes: accessModes:
{{- if not (empty .Values.dashboard.persistence.accessModes) }} - {{ .Values.dashboard.persistence.accessMode | quote }}
{{- range .Values.dashboard.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
{{- end }}
capacity: capacity:
storage: {{ .Values.dashboard.persistence.size | quote }} storage: {{ .Values.dashboard.persistence.size | quote }}
hostPath: hostPath:
path: {{ .Values.dashboard.persistence.hostPath | quote }} path: {{ .Values.dashboard.persistence.hostPath | quote }}
{{- end -}} {{- end -}}

View File

@@ -19,10 +19,11 @@ metadata:
rules: rules:
- apiGroups: ["*"] - apiGroups: ["*"]
resources: ["*"] resources: ["*"]
{{- if .Values.dashboard.allowWriteActions }}
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
{{- else }}
verbs: ["get", "list", "watch"] verbs: ["get", "list", "watch"]
{{- if .Values.dashboard.allowWriteActions }}
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
{{- end }} {{- end }}
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1

View File

@@ -1,8 +1,5 @@
replicaCount: 1 replicaCount: 1
# Flag for setting environment to debug mode
debug: false
image: image:
repository: komodorio/helm-dashboard repository: komodorio/helm-dashboard
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
@@ -30,10 +27,6 @@ resources:
dashboard: dashboard:
allowWriteActions: true allowWriteActions: true
# default namespace for Helm operations
namespace: ""
persistence: persistence:
enabled: true enabled: true
@@ -43,7 +36,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: null storageClass: ""
## 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
@@ -67,20 +60,6 @@ dashboard:
## Helm Dashboard data Persistent Volume size ## Helm Dashboard data Persistent Volume size
## ##
size: 100M size: 100M
## @param.updateStrategy.type Set up update strategy for helm-dashboard installation.
## Set to Recreate if you use persistent volume that cannot be mounted by more than one pods to make sure the pods is destroyed first.
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
## Example:
## updateStrategy:
## type: RollingUpdate
## rollingUpdate:
## maxSurge: 25%
## maxUnavailable: 25%
##
updateStrategy:
type: RollingUpdate
podAnnotations: {} podAnnotations: {}
@@ -111,11 +90,6 @@ autoscaling:
nodeSelector: {} nodeSelector: {}
extraArgs:
- --no-browser
- --bind=0.0.0.0
tolerations: [] tolerations: []
affinity: {} affinity: {}

View File

@@ -1,4 +1,4 @@
#!/bin/bash -e #!/bin/bash
WORKING_DIRECTORY="$PWD" WORKING_DIRECTORY="$PWD"
@@ -9,7 +9,6 @@ WORKING_DIRECTORY="$PWD"
} }
sed -i -e "s/appVersion.*/appVersion: \"${APP_VERSION}\" /g" ${HELM_CHARTS_SOURCE}/Chart.yaml sed -i -e "s/appVersion.*/appVersion: \"${APP_VERSION}\" /g" ${HELM_CHARTS_SOURCE}/Chart.yaml
sed -i -e "s/version.*/version: \"${APP_VERSION}\" /g" plugin.yaml
CURRENT_VERSION=$(cat ${HELM_CHARTS_SOURCE}/Chart.yaml | grep 'version:' | awk '{print $2}') CURRENT_VERSION=$(cat ${HELM_CHARTS_SOURCE}/Chart.yaml | grep 'version:' | awk '{print $2}')
NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g') NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g')
sed -i -e "s/${CURRENT_VERSION}/${NEW_VERSION}/g" ${HELM_CHARTS_SOURCE}/Chart.yaml sed -i -e "s/${CURRENT_VERSION}/${NEW_VERSION}/g" ${HELM_CHARTS_SOURCE}/Chart.yaml

131
go.mod
View File

@@ -3,163 +3,68 @@ module github.com/komodorio/helm-dashboard
go 1.18 go 1.18
require ( require (
github.com/Masterminds/semver/v3 v3.2.0 github.com/eko/gocache/v3 v3.1.1
github.com/eko/gocache/v3 v3.1.2
github.com/gin-gonic/gin v1.8.1 github.com/gin-gonic/gin v1.8.1
github.com/hashicorp/go-version v1.6.0 github.com/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
github.com/joomcode/errorx v1.1.0
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
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/rogpeppe/go-internal v1.8.0
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.4.0 helm.sh/helm/v3 v3.10.3
helm.sh/helm/v3 v3.11.1 k8s.io/apimachinery v0.25.2
k8s.io/api v0.26.0
k8s.io/apimachinery v0.26.0
k8s.io/cli-runtime v0.26.0
k8s.io/client-go v0.26.0
k8s.io/kubectl v0.26.0
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d
) )
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Masterminds/squirrel v1.5.3 // indirect
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 // indirect github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/containerd/containerd v1.6.18 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/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.21+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v20.10.24+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/camelcase v1.0.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // 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-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/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/goccy/go-json v0.9.11 // indirect github.com/goccy/go-json v0.9.11 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.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/uuid v1.2.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.6 // 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.16 // 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.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-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/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // 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.3 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/prometheus/client_golang v1.12.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/common v0.33.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rubenv/sql-migrate v1.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // 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 golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
github.com/xlab/treeprint v1.1.0 // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
golang.org/x/crypto v0.5.0 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/exp v0.0.0-20221110155412-d0897a79cd37 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/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
k8s.io/apiextensions-apiserver v0.26.0 // indirect k8s.io/klog/v2 v2.70.1 // indirect
k8s.io/apiserver v0.26.0 // indirect k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
k8s.io/component-base v0.26.0 // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
oras.land/oras-go v1.2.2 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/kustomize/api v0.12.1 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
) )

564
go.sum

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

65
main.go
View File

@@ -1,13 +1,8 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"github.com/joomcode/errorx"
"os" "os"
"os/signal"
"strings"
"syscall"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
@@ -23,23 +18,16 @@ 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 (Heap, DataDog etc.)"` NoTracking bool `long:"no-analytics" description:"Disable user analytics (GA, DataDog etc.)"`
BindHost string `long:"bind" description:"Host binding to start server (default: localhost)"` // default should be printed but not assigned as the precedence: flag > env > default 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"` // TODO: better default port to clash less?
Namespace string `short:"n" long:"namespace" description:"Namespace for HELM operations"` Namespace string `short:"n" long:"namespace" description:"Limit operations to a specific namespace"`
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")
@@ -53,35 +41,13 @@ func main() {
setupLogging(opts.Verbose) setupLogging(opts.Verbose)
server := dashboard.Server{ server := dashboard.Server{
Version: version, Version: version,
Namespaces: strings.Split(opts.Namespace, ","), Namespace: 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())
osSignal := make(chan os.Signal, 1)
signal.Notify(osSignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
oscall := <-osSignal
log.Warnf("Stopping on signal: %s\n", oscall)
cancel()
}()
address, webServerDone, err := server.StartServer(ctx, cancel)
if err != nil {
if errorx.IsOfType(err, errorx.InitializationFailed) {
log.Debugf("Full error: %+v", err)
log.Errorf("No Kubernetes cluster connection possible. Make sure you have valid kubeconfig file or run dashboard from inside cluster. See https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/")
os.Exit(1)
} else {
log.Fatalf("Failed to start Helm Dashboard: %+v", err)
}
} }
address, webServerDone := server.StartServer()
if !opts.NoTracking { if !opts.NoTracking {
log.Infof("User analytics is collected to improve the quality, disable it with --no-analytics") log.Infof("User analytics is collected to improve the quality, disable it with --no-analytics")
@@ -103,7 +69,7 @@ func main() {
func parseFlags() options { func parseFlags() options {
ns := os.Getenv("HELM_NAMESPACE") ns := os.Getenv("HELM_NAMESPACE")
if ns == "default" { // it's how Helm passes to plugin the empty NS, we have to reset it back if ns == "default" {
ns = "" ns = ""
} }
@@ -126,8 +92,7 @@ func parseFlags() options {
} }
if len(args) > 0 { if len(args) > 0 {
fmt.Println("The program does not take arguments, see --help for usage") panic("The program does not take argumants, see --help for usage")
os.Exit(1)
} }
return opts return opts
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import (
) )
type ScannersHandler struct { type ScannersHandler struct {
*Contexted Data *subproc.DataLayer
} }
func (h *ScannersHandler) List(c *gin.Context) { func (h *ScannersHandler) List(c *gin.Context) {
@@ -26,10 +26,23 @@ func (h *ScannersHandler) List(c *gin.Context) {
c.IndentedJSON(http.StatusOK, res) c.IndentedJSON(http.StatusOK, res)
} }
func (h *ScannersHandler) ScanManifest(c *gin.Context) { func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) {
qp, err := utils.GetQueryProps(c, false)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}
reuseVals := c.Query("initial") != "true"
mnf, err := h.Data.ChartInstall(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), true, c.PostForm("values"), reuseVals)
if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}
reps := map[string]*subproc.ScanResults{} reps := map[string]*subproc.ScanResults{}
for _, scanner := range h.Data.Scanners { for _, scanner := range h.Data.Scanners {
sr, err := scanner.ScanManifests(c.PostForm("manifest")) sr, err := scanner.ScanManifests(mnf)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusInternalServerError, err) _ = c.AbortWithError(http.StatusInternalServerError, err)
return return
@@ -42,7 +55,7 @@ func (h *ScannersHandler) ScanManifest(c *gin.Context) {
} }
func (h *ScannersHandler) ScanResource(c *gin.Context) { func (h *ScannersHandler) ScanResource(c *gin.Context) {
qp, err := utils.GetQueryProps(c) qp, err := utils.GetQueryProps(c, false)
if err != nil { if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err) _ = c.AbortWithError(http.StatusBadRequest, err)
return return

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,6 @@ package scanners
import ( import (
"encoding/json" "encoding/json"
"github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc" "github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils" "github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
@@ -13,7 +11,7 @@ import (
) )
type Checkov struct { type Checkov struct {
Data *objects.DataLayer Data *subproc.DataLayer
} }
func (c *Checkov) ManifestIsScannable() bool { func (c *Checkov) ManifestIsScannable() bool {
@@ -79,7 +77,7 @@ func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) {
res := &subproc.ScanResults{} res := &subproc.ScanResults{}
err = json.Unmarshal([]byte(out), &res.OrigReport) err = json.Unmarshal([]byte(out), res.OrigReport)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -91,19 +89,14 @@ func (c *Checkov) ScanResource(ns string, kind string, name string) (*subproc.Sc
carp := v1.Carp{} carp := v1.Carp{}
carp.Kind = kind carp.Kind = kind
carp.Name = name carp.Name = name
app, err := c.Data.AppForCtx(c.Data.KubeContext) mnf, err := c.Data.GetResourceYAML(ns, &carp)
if err != nil { if err != nil {
return nil, errorx.Decorate(err, "failed to get app for context") return nil, err
}
mnf, err := app.K8s.GetResourceYAML(kind, ns, name)
if err != nil {
return nil, errorx.Decorate(err, "failed to get YAML for resource")
} }
fname, fclose, err := utils.TempFile(mnf) fname, fclose, err := utils.TempFile(mnf)
if err != nil { if err != nil {
return nil, errorx.Decorate(err, "failed to create temporary file") return nil, err
} }
defer fclose() defer fclose()

View File

@@ -1,7 +1,6 @@
package scanners package scanners
import ( import (
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc" "github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils" "github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -10,7 +9,7 @@ import (
) )
type Trivy struct { type Trivy struct {
Data *objects.DataLayer Data *subproc.DataLayer
} }
func (c *Trivy) ManifestIsScannable() bool { func (c *Trivy) ManifestIsScannable() bool {

View File

@@ -9,101 +9,57 @@ import (
"strings" "strings"
"time" "time"
"github.com/joomcode/errorx"
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/registry"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
"github.com/komodorio/helm-dashboard/pkg/dashboard/scanners" "github.com/komodorio/helm-dashboard/pkg/dashboard/scanners"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils" "github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type Server struct { type Server struct {
Version string Version string
Namespaces []string Namespace 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() (string, utils.ControlChan) {
data, err := objects.NewDataLayer(s.Namespaces, s.Version, NewHelmConfig, s.Devel) data := subproc.DataLayer{
if err != nil { Namespace: s.Namespace,
return "", nil, errorx.Decorate(err, "Failed to create data layer") Cache: subproc.NewCache(),
StatusInfo: &subproc.StatusInfo{
CurVer: s.Version,
Analytics: false,
LimitedToNamespace: s.Namespace,
},
}
err := data.CheckConnectivity()
if err != nil {
log.Errorf("Failed to check that Helm is operational, cannot continue. The error was: %s", err)
os.Exit(1) // TODO: propagate error instead?
} }
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)
if err != nil {
return "", nil, errorx.Decorate(err, "Failed to detect cluster mode")
}
go checkUpgrade(data.StatusInfo) go checkUpgrade(data.StatusInfo)
discoverScanners(data) discoverScanners(&data)
go data.PeriodicTasks(ctx) abort := make(utils.ControlChan)
api := NewRouter(abort, &data, s.Debug)
done := s.startBackgroundServer(api, abort)
api := NewRouter(cancel, data, s.Debug) return "http://" + s.Address, done
done := s.startBackgroundServer(api, ctx)
return "http://" + s.Address, done, nil
} }
func (s *Server) detectClusterMode(data *objects.DataLayer) error { func (s Server) startBackgroundServer(routes *gin.Engine, abort utils.ControlChan) utils.ControlChan {
data.StatusInfo.ClusterMode = os.Getenv("HD_CLUSTER_MODE") != ""
if data.StatusInfo.ClusterMode {
return nil
}
ctxs, err := data.ListContexts()
if err != nil {
return err
}
if len(ctxs) == 0 {
log.Infof("Got no kubectl config contexts, will attempt to detect if we're inside cluster...")
app, err := data.AppForCtx("")
if err != nil {
return err
}
ns, err := app.K8s.GetNameSpaces()
if err != nil { // no point in continuing without kubectl context and k8s connection
return errorx.InitializationFailed.Wrap(err, "No k8s cluster connection")
}
log.Debugf("Got %d namespaces listed", len(ns.Items))
data.StatusInfo.ClusterMode = true
}
return err
}
func (s *Server) startBackgroundServer(routes *gin.Engine, ctx context.Context) utils.ControlChan {
done := make(utils.ControlChan) done := make(utils.ControlChan)
server := &http.Server{ server := &http.Server{
Addr: s.Address, Addr: s.Address,
Handler: routes, Handler: routes,
} }
go func() {
<-ctx.Done()
err := server.Shutdown(context.Background())
if err != nil {
log.Warnf("Had problems shutting down the server: %s", err)
}
log.Infof("Web server has been shut down.")
}()
go func() { go func() {
err := server.ListenAndServe() err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed { if err != nil && err != http.ErrServerClosed {
@@ -117,10 +73,18 @@ func (s *Server) startBackgroundServer(routes *gin.Engine, ctx context.Context)
done <- struct{}{} done <- struct{}{}
}() }()
go func() {
<-abort
err := server.Shutdown(context.Background())
if err != nil {
log.Warnf("Had problems shutting down the server: %s", err)
}
}()
return done return done
} }
func (s *Server) itIsUs() bool { func (s Server) itIsUs() bool {
url := fmt.Sprintf("http://%s/status", s.Address) url := fmt.Sprintf("http://%s/status", s.Address)
var myClient = &http.Client{ var myClient = &http.Client{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
@@ -135,7 +99,7 @@ func (s *Server) itIsUs() bool {
return strings.HasPrefix(r.Header.Get("X-Application-Name"), "Helm Dashboard") return strings.HasPrefix(r.Header.Get("X-Application-Name"), "Helm Dashboard")
} }
func discoverScanners(data *objects.DataLayer) { func discoverScanners(data *subproc.DataLayer) {
potential := []subproc.Scanner{ potential := []subproc.Scanner{
&scanners.Checkov{Data: data}, &scanners.Checkov{Data: data},
&scanners.Trivy{Data: data}, &scanners.Trivy{Data: data},
@@ -149,7 +113,7 @@ func discoverScanners(data *objects.DataLayer) {
} }
} }
func checkUpgrade(d *objects.StatusInfo) { // TODO: check it once an hour func checkUpgrade(d *subproc.StatusInfo) { // TODO: check it once an hour
url := "https://api.github.com/repos/komodorio/helm-dashboard/releases/latest" url := "https://api.github.com/repos/komodorio/helm-dashboard/releases/latest"
type GHRelease struct { type GHRelease struct {
Name string `json:"name"` Name string `json:"name"`
@@ -179,7 +143,7 @@ func checkUpgrade(d *objects.StatusInfo) { // TODO: check it once an hour
v2, err := version.NewVersion(d.LatestVer) v2, err := version.NewVersion(d.LatestVer)
if err != nil { if err != nil {
log.Warnf("Failed to parse RepoLatestVer: %s", err) log.Warnf("Failed to parse LatestVer: %s", err)
} else { } else {
if v1.LessThan(v2) { if v1.LessThan(v2) {
log.Warnf("Newer Helm Dashboard version is available: %s", d.LatestVer) log.Warnf("Newer Helm Dashboard version is available: %s", d.LatestVer)
@@ -189,34 +153,3 @@ func checkUpgrade(d *objects.StatusInfo) { // TODO: check it once an hour
} }
} }
} }
func NewHelmConfig(origSettings *cli.EnvSettings, ns string) (*action.Configuration, error) {
// TODO: cache it into map
// TODO: I feel there should be more elegant way to organize this code
actionConfig := new(action.Configuration)
settings := cli.New()
settings.KubeContext = origSettings.KubeContext
settings.SetNamespace(ns) // important for RESTClientGetter to have correct namespace
registryClient, err := registry.NewClient(
registry.ClientOptDebug(false),
registry.ClientOptEnableCache(true),
//registry.ClientOptWriter(out),
registry.ClientOptCredentialsFile(settings.RegistryConfig),
)
if err != nil {
return nil, errorx.Decorate(err, "failed to crete helm config object")
}
actionConfig.RegistryClient = registryClient
helmDriver := os.Getenv("HELM_DRIVER")
if err := actionConfig.Init(
settings.RESTClientGetter(),
ns,
helmDriver, log.Debugf); err != nil {
return nil, errorx.Decorate(err, "failed to init Helm action config")
}
return actionConfig, nil
}

View File

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

View File

@@ -1,28 +1,11 @@
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")
} }
} }
} }
@@ -75,57 +58,12 @@ 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;
};
})();

View File

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

View File

@@ -55,17 +55,17 @@ function loadContentWrapper() {
loadContent(getHashParam("tab"), getHashParam("namespace"), getHashParam("chart"), revision, revDiff, flag) loadContent(getHashParam("tab"), getHashParam("namespace"), getHashParam("chart"), revision, revDiff, flag)
} }
function loadContent(mode, namespace, name, revision, revDiff, userDefined) { function loadContent(mode, namespace, name, revision, revDiff, flag) {
let qstr = "revision=" + revision let qstr = "name=" + name + "&namespace=" + namespace + "&revision=" + revision
if (revDiff) { if (revDiff) {
qstr += "&revisionDiff=" + revDiff qstr += "&revisionDiff=" + revDiff
} }
if (userDefined) { if (flag) {
qstr += "&userDefined=" + userDefined qstr += "&flag=" + flag
} }
let url = "/api/helm/releases/" + namespace + "/" + name + "/" + mode let url = "/api/helm/charts/" + mode
url += "?" + qstr url += "?" + qstr
const diffDisplay = $("#manifestText"); const diffDisplay = $("#manifestText");
diffDisplay.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>') diffDisplay.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
@@ -149,7 +149,9 @@ 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?health=true" let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
let url = "/api/helm/charts/resources"
url += "?" + qstr
$.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,32 +182,23 @@ 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
for (let k = 0; k < res.status.conditions.length; k++) { $.getJSON("/api/kube/resources/" + res.kind.toLowerCase() + "?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
if (res.status.conditions[k].type !== "hdHealth") { // it's our custom condition type //reportError("Failed to get list of resources")
continue }).done(function (data) {
} const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase);
if (["Available", "Active", "Established", "Bound", "Ready"].includes(data.status.phase)) {
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 (cond.status === "Progressing") { } else if (["Exists"].includes(data.status.phase)) {
badge.addClass("bg-success text-dark bg-opacity-50")
} else if (["Progressing"].includes(data.status.phase)) {
badge.addClass("bg-warning") 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", cond.reason) statusBlock.empty().append(badge).attr("title", data.status.phase)
const statusMessage = cond.message const statusMessage = getStatusMessage(data.status)
resBlock.find(".res-statusmsg").html("<span class='text-muted small me-2'>" + (statusMessage ? statusMessage : '') + "</span>") resBlock.find(".res-statusmsg").html("<span class='text-muted small'>" + (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")
@@ -224,15 +217,21 @@ 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"))
@@ -240,7 +239,7 @@ function showDescribe(ns, kind, name, badge) {
const myModal = new bootstrap.Offcanvas(document.getElementById('describeModal')); const myModal = new bootstrap.Offcanvas(document.getElementById('describeModal'));
myModal.show() myModal.show()
$.get("/api/k8s/" + kind.toLowerCase() + "/describe?name=" + name + "&namespace=" + ns).fail(function (xhr) { $.get("/api/kube/describe/" + kind.toLowerCase() + "?name=" + name + "&namespace=" + ns).fail(function (xhr) {
reportError("Failed to describe resource", xhr) reportError("Failed to describe resource", xhr)
}).done(function (data) { }).done(function (data) {
data = hljs.highlight(data, {language: 'yaml'}).value data = hljs.highlight(data, {language: 'yaml'}).value

View File

@@ -42,7 +42,7 @@
</a> </a>
<ul class="dropdown-menu fs-80"> <ul class="dropdown-menu fs-80">
<li><a class="dropdown-item" <li><a class="dropdown-item"
href="https://komodorkommunity.slack.com" href="https://join.slack.com/t/komodorkommunity/shared_invite/zt-1dm3cnkue-ov1Yh~_95teA35QNx5yuMg"
target="_blank"><i class="bi-slack"></i> Support Chat</a></li> target="_blank"><i class="bi-slack"></i> Support Chat</a></li>
<li><a class="dropdown-item" href="https://github.com/komodorio/helm-dashboard" target="_blank"><i <li><a class="dropdown-item" href="https://github.com/komodorio/helm-dashboard" target="_blank"><i
class="bi-github"></i> Project Page</a></li> class="bi-github"></i> Project Page</a></li>
@@ -54,8 +54,7 @@
<button class="dropdown-item" id="cacheClear"><i <button class="dropdown-item" id="cacheClear"><i
class="bi-arrow-repeat"></i> Reset Cache class="bi-arrow-repeat"></i> Reset Cache
</button> </button>
<li><a class="dropdown-item" href="api-docs" target="_blank"><i </li>
class="bi-braces"></i> REST API</a></li>
<li> <li>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
</li> </li>
@@ -64,20 +63,15 @@
</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/releases" target="_blank"> href="https://github.com/komodorio/helm-dashboard#installing" target="_blank">
Upgrade to <span id="toolVersionUpgrade"></span> Upgrade to <span id="toolVersionUpgrade"></span>
</a></li> </a></li>
</ul> </ul>
<div> <div>
<div class="border-muted text-muted border rounded p-1 pe-2 me-3 d-flex"> <a class="btn" href="https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash"><img
<img alt="Komodor" src="https://raw.githubusercontent.com/komodorio/helm-charts/master/k8s-watcher.svg" class="me-2" style="width: 42px; height: 42px"/> src="static/komodor-logo.svg" alt="komodor.io"
<span class="text-nowrap"> style="height: 1.2rem; vertical-align: text-bottom; filter: grayscale(00%);"></a>
<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>
@@ -95,9 +89,6 @@
<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">
@@ -162,6 +153,14 @@
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>
@@ -195,10 +194,6 @@
<button id="btnRollback" class="btn btn-sm btn-light bg-white border border-secondary me-2" <button id="btnRollback" class="btn btn-sm btn-light bg-white border border-secondary me-2"
title="Rollback to this revision"><i class="bi-arrow-repeat"></i> <span>Rollback</span> title="Rollback to this revision"><i class="bi-arrow-repeat"></i> <span>Rollback</span>
</button> </button>
<button id="btnTest"
class="btn btn-sm btn-light bg-white border border-secondary me-2 display-none"
title="Run tests for this chart"><i class="bi-check-circle"></i> <span>Run tests</span>
</button>
<button id="btnUninstall" class="btn btn-sm btn-light bg-white border border-secondary" <button id="btnUninstall" class="btn btn-sm btn-light bg-white border border-secondary"
title="Uninstall the chart"><i class="bi-trash3"></i> Uninstall title="Uninstall the chart"><i class="bi-trash3"></i> Uninstall
</button> </button>
@@ -305,8 +300,6 @@
<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"
@@ -316,12 +309,8 @@
<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>
<div> <button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
<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>
@@ -354,26 +343,8 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form enctype="application/x-www-form-urlencoded"> <form enctype="application/x-www-form-urlencoded">
<div class="row mb-4"> <label class="form-label">Name: <input class="form-control" name="name"></label>
<div class="col"> <label class="form-label">URL: <input class="form-control" name="url"></label>
<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">
@@ -393,16 +364,14 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form class="modal-body border-bottom fs-5" enctype="multipart/form-data"> <form class="modal-body border-bottom fs-5" enctype="multipart/form-data">
<input name="preview" type="hidden" class="preview-mode"/>
<input name="chart" type="hidden" class="chart-name"/>
<div class="input-group mb-3 text-muted"> <div class="input-group mb-3 text-muted">
<label class="form-label me-4 text-dark">Version to install: <select <label class="form-label me-4 text-dark">Version to install: <select
class='fw-bold text-success ver-new' name="version"></select></label> <span class="ver-old">(current version is <span class='fw-bold text-success ver-new'></select></label> <span class="ver-old">(current version is <span
class='text-success ms-1'>0.0.0</span>)</span> class='text-success ms-1'>0.0.0</span>)</span>
</div> </div>
<div class="input-group mb-3 text-muted"> <div class="input-group mb-3 text-muted">
<label class="form-label me-4 text-dark"> <label class="form-label me-4 text-dark">
Release Name: <input class="form-control rel-name" name="name"> Release Name: <input class="form-control rel-name">
</label> </label>
<label class="form-label me-4 text-dark"> <label class="form-label me-4 text-dark">
Namespace (optional): Namespace (optional):
@@ -448,26 +417,6 @@
</div> </div>
</div> </div>
<div class="modal" id="testModal" tabindex="-1">
<div class="modal-dialog modal-dialog modal-dialog-scrollable modal-xl">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="testModalLabel">
<span class="type">Test results</span>
</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body border-bottom fs-5">
<div class="row">
<div class="col">
<span class="test-result"></span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="PowerOffModal" tabindex="-1" aria-labelledby="ModalLabel" aria-hidden="true"> <div class="modal fade" id="PowerOffModal" tabindex="-1" aria-labelledby="ModalLabel" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">

View File

@@ -3,7 +3,7 @@ function loadChartsList() {
$("#sectionList").show() $("#sectionList").show()
const chartsCards = $("#installedList .body") const chartsCards = $("#installedList .body")
chartsCards.empty().append("<div><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span> Loading...</div>") chartsCards.empty().append("<div><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span> Loading...</div>")
$.getJSON("/api/helm/releases").fail(function (xhr) { $.getJSON("/api/helm/charts").fail(function (xhr) {
sendStats('Get releases', {'status': 'failed'}); sendStats('Get releases', {'status': 'failed'});
reportError("Failed to get list of charts", xhr) reportError("Failed to get list of charts", xhr)
chartsCards.empty().append("<div class=\"row m-0 py-4 bg-white rounded-1 b-shadow border-4 border-start\"><div class=\"col\">Failed to get list of charts</div></div>") chartsCards.empty().append("<div class=\"row m-0 py-4 bg-white rounded-1 b-shadow border-4 border-start\"><div class=\"col\">Failed to get list of charts</div></div>")
@@ -44,13 +44,37 @@ function buildChartCard(elm) {
<div class="col-1 rel-date text-nowrap"><span>today</span><div>Updated</div></div> <div class="col-1 rel-date text-nowrap"><span>today</span><div>Updated</div></div>
</div>`) </div>`)
if (elm.icon) { let chartName = elm.chart
card.find(".rel-name").attr("style", "background-image: url(" + elm.icon + ")") // semver2 regex , add optional v prefix
const chartNameRegex = 'v?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?'
const match = elm.chart.match(chartNameRegex);
if (match) {
chartName = elm.chart.substring(0, match.index - 1)
} else {
// fall back to simple substr
chartName = elm.chart.substring(0, elm.chart.lastIndexOf("-"))
} }
$.getJSON("/api/helm/repo/search?name=" + chartName).fail(function (xhr) {
// we're ok if we can't show icon and description
console.log("Failed to get repo name for charts", xhr)
}).done(function (data) {
if (data.length > 0) {
$.getJSON("/api/helm/charts/show?name=" + data[0].name).fail(function (xhr) {
console.log("Failed to get chart", xhr)
}).done(function (data) {
if (data) {
const res = data[0];
if (res.icon) {
card.find(".rel-name").attr("style", "background-image: url(" + res.icon + ")")
}
if (res.description) {
card.find(".rel-name div").text(res.description)
}
}
if (elm.description) { })
card.find(".rel-name div").text(elm.description) }
} })
card.find(".rel-name span").text(elm.name) card.find(".rel-name span").text(elm.name)
card.find(".rel-rev span").text("#" + elm.revision) card.find(".rel-rev span").text("#" + elm.revision)
@@ -79,63 +103,6 @@ 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'>&nbsp;</span>")
if (cond.status === "Healthy") {
square.addClass("bg-success")
} else if (cond.status === "Progressing") {
square.addClass("bg-warning")
} else {
square.addClass("bg-danger")
}
square.attr("data-bs-title", cond.status+" "+res.kind+" '"+res.metadata.name+"'")
card.find(".rel-status div").append(square)
}
}
const tooltipTriggerList = card.find('.rel-status [data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
})
return card; return card;
} }

View File

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

View File

@@ -2,22 +2,15 @@ function loadRepoView() {
$("#sectionRepo .repo-details").hide() $("#sectionRepo .repo-details").hide()
$("#sectionRepo").show() $("#sectionRepo").show()
$("#repoAddModal input[name=name]").val(getHashParam("suggestRepo")) $.getJSON("/api/helm/repo").fail(function (xhr) {
$("#repoAddModal input[name=url]").val(getHashParam("suggestRepoUrl"))
if (getHashParam("suggestRepo")) {
$("#sectionRepo .repo-list .btn").click()
}
$.getJSON("/api/helm/repositories").fail(function (xhr) {
reportError("Failed to get list of repositories", xhr) reportError("Failed to get list of repositories", xhr)
sendStats('Get repo', {'status': 'fail'}); sendStats('Get repo', {'status': 'fail'});
}).done(function (data) { }).done(function (data) {
const items = $("#sectionRepo .repo-list ul").empty() const items = $("#sectionRepo .repo-list ul").empty()
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="repo" class="me-2"/><span></span></label></li>'); let opt = $('<li class="mb-2"><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
opt.attr('title', elm.url) opt.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)
@@ -27,7 +20,7 @@ function loadRepoView() {
if (!data.length) { if (!data.length) {
items.text("No repositories found, try adding one") items.text("No repositories found, try adding one")
} }
sendStats('Get repo', {'status': 'success', length: data.length}); sendStats('Get repo', {'status': 'success', length:data.length});
items.find("input").click(function () { items.find("input").click(function () {
$("#inputSearch").val('') $("#inputSearch").val('')
const self = $(this) const self = $(this)
@@ -37,10 +30,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/repo/charts?name=" + 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)
}).done(function (data) { }).done(function (data) {
$("#sectionRepo .repo-details ul").empty() $("#sectionRepo .repo-details ul").empty()
@@ -51,11 +42,6 @@ function loadRepoView() {
<div class="col-1 py-2">` + elm.version + `</div> <div class="col-1 py-2">` + elm.version + `</div>
<div class="col-1 action text-nowrap"><button class="btn btn-sm border-secondary bg-white">Install</button></div> <div class="col-1 action text-nowrap"><button class="btn btn-sm border-secondary bg-white">Install</button></div>
</li>`) </li>`)
if (elm.icon) {
li.find("h6").prepend('<img src="' + elm.icon + '" class="me-1" style="height: 1rem"/>')
}
li.data("item", elm) li.data("item", elm)
if (elm.installed_namespace) { if (elm.installed_namespace) {
@@ -92,8 +78,6 @@ $("#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()
}) })
@@ -102,7 +86,7 @@ $("#repoAddModal .btn-confirm").click(function () {
$("#repoAddModal .btn-confirm").prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>') $("#repoAddModal .btn-confirm").prop("disabled", true).prepend('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>')
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: "/api/helm/repositories", url: "/api/helm/repo",
data: $("#repoAddModal form").serialize(), data: $("#repoAddModal form").serialize(),
}).fail(function (xhr) { }).fail(function (xhr) {
reportError("Failed to add repo", xhr) reportError("Failed to add repo", xhr)
@@ -116,7 +100,7 @@ $("#sectionRepo .btn-remove").click(function () {
if (confirm("Confirm removing repository?")) { if (confirm("Confirm removing repository?")) {
$.ajax({ $.ajax({
type: 'DELETE', type: 'DELETE',
url: "/api/helm/repositories/" + $("#sectionRepo .repo-details h2").text(), url: "/api/helm/repo?name=" + $("#sectionRepo .repo-details h2").text(),
}).fail(function (xhr) { }).fail(function (xhr) {
reportError("Failed to add repo", xhr) reportError("Failed to add repo", xhr)
}).done(function () { }).done(function () {
@@ -130,7 +114,7 @@ $("#sectionRepo .btn-update").click(function () {
$("#sectionRepo .btn-update i").removeClass("bi-arrow-repeat").append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>') $("#sectionRepo .btn-update i").removeClass("bi-arrow-repeat").append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: "/api/helm/repositories/" + $("#sectionRepo .repo-details h2").text(), url: "/api/helm/repo/update?name=" + $("#sectionRepo .repo-details h2").text(),
}).fail(function (xhr) { }).fail(function (xhr) {
reportError("Failed to add repo", xhr) reportError("Failed to add repo", xhr)
}).done(function () { }).done(function () {
@@ -148,11 +132,8 @@ function repoChartClicked() {
window.location.reload() window.location.reload()
} else { } else {
const contexts = $("body").data("contexts") const contexts = $("body").data("contexts")
const ctxFiltered = contexts.filter(obj => { const ctxFiltered = contexts.filter(obj => {return obj.Name === getHashParam("context")});
return obj.Name === getHashParam("context") const contextNamespace = ctxFiltered.length?ctxFiltered[0].Namespace:""
});
const contextNamespace = ctxFiltered.length ? ctxFiltered[0].Namespace : ""
elm.repository = $("#sectionRepo .repo-details h2").text()
popUpUpgrade(elm, contextNamespace) popUpUpgrade(elm, contextNamespace)
} }
} }

View File

@@ -5,14 +5,12 @@ function loadChartHistory(namespace, name) {
$("#sectionDetails").show() $("#sectionDetails").show()
$("#sectionDetails .name").text(name) $("#sectionDetails .name").text(name)
revRow.empty().append("<li><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span></li>") revRow.empty().append("<li><span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span></li>")
$.getJSON("/api/helm/releases/" + namespace + "/" + name + "/history").fail(function (xhr) { $.getJSON("/api/helm/charts/history?name=" + name + "&namespace=" + namespace).fail(function (xhr) {
reportError("Failed to get chart details", xhr) reportError("Failed to get chart details", xhr)
}).done(function (data) { }).done(function (data) {
fillChartHistory(data, namespace, name); fillChartHistory(data, namespace, name);
checkUpgradeable(data[0].chart_name) checkUpgradeable(data[data.length - 1].chart_name)
$("#btnTest").toggle(data[0].has_tests)
const rev = getHashParam("revision") const rev = getHashParam("revision")
if (rev) { if (rev) {

View File

@@ -45,7 +45,7 @@ function fillClusters(limNS) {
filterInstalledList($("#installedList .body .row")) filterInstalledList($("#installedList .body .row"))
}) })
$.getJSON("/api/k8s/contexts").fail(function (xhr) { $.getJSON("/api/kube/contexts").fail(function (xhr) {
sendStats('contexts', {'status': 'fail'}); sendStats('contexts', {'status': 'fail'});
reportError("Failed to get list of clusters", xhr) reportError("Failed to get list of clusters", xhr)
}).done(function (data) { }).done(function (data) {
@@ -54,7 +54,7 @@ function fillClusters(limNS) {
data.sort((a, b) => (getCleanClusterName(a.Name) > getCleanClusterName(b.Name)) - (getCleanClusterName(a.Name) < getCleanClusterName(b.Name))) data.sort((a, b) => (getCleanClusterName(a.Name) > getCleanClusterName(b.Name)) - (getCleanClusterName(a.Name) < getCleanClusterName(b.Name)))
fillClusterList(data, context); fillClusterList(data, context);
sendStats('contexts', {'status': 'success', length: data.length}); sendStats('contexts', {'status': 'success', length: data.length});
$.getJSON("/api/k8s/namespaces/list").fail(function (xhr) { $.getJSON("/api/kube/namespaces").fail(function (xhr) {
reportError("Failed to get namespaces", xhr) reportError("Failed to get namespaces", xhr)
}).done(function (res) { }).done(function (res) {
const ns = res.items.map(i => i.metadata.name) const ns = res.items.map(i => i.metadata.name)
@@ -117,8 +117,8 @@ $("#topNav ul a").click(function () {
initView() initView()
}) })
const errAlert = document.getElementById('errorAlert') const myAlert = document.getElementById('errorAlert')
errAlert.addEventListener('close.bs.alert', event => { myAlert.addEventListener('close.bs.alert', event => {
event.preventDefault() event.preventDefault()
$("#errorAlert").hide() $("#errorAlert").hide()
}) })
@@ -129,7 +129,6 @@ function reportError(err, xhr) {
$("#errorAlert p").text(xhr.responseText) $("#errorAlert p").text(xhr.responseText)
} }
$("#errorAlert").show() $("#errorAlert").show()
sendStats("errorReported", {"errMessage": err})
} }
@@ -357,6 +356,4 @@ 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"

View File

@@ -79,8 +79,3 @@
.fs-80 { .fs-80 {
font-size: 0.8rem!important; font-size: 0.8rem!important;
} }
.required::after {
content: " *";
color: red;
}

View File

@@ -89,11 +89,10 @@ body > .container-fluid {
#filters { #filters {
overflow: hidden; overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: 0.8rem; font-size: 0.8rem;
line-height: 175%; line-height: 175%;
inline-size: auto;
overflow-wrap: break-word;
} }
#cluster input, #cluster span { #cluster input, #cluster span {
@@ -296,31 +295,4 @@ nav .nav-tabs .nav-link.active {
#sectionRepo .repo-details ul .row:hover .btn { #sectionRepo .repo-details ul .row:hover .btn {
visibility: visible; visibility: visible;
}
.test-result {
font-size: 1rem;
}
.square {
width: 0.55rem;
height: 0.55rem;
display: inline-block;
border-radius: 0.1rem!important;
}
.square.bg-danger {
background-color: #ff0072!important;
}
.square.bg-warning {
background-color: #ffa800!important;
}
.square.bg-success {
background-color: #00c2ab!important;
}
.hljs {
overflow-x: inherit;
} }

View File

@@ -1,4 +1,4 @@
package objects package subproc
import ( import (
"context" "context"
@@ -12,6 +12,15 @@ import (
type CacheKey = string type CacheKey = string
const CacheKeyRelList CacheKey = "installed-releases-list"
const CacheKeyShowChart CacheKey = "show-chart"
const CacheKeyRelHistory CacheKey = "release-history"
const CacheKeyRevManifests CacheKey = "rev-manifests"
const CacheKeyRevNotes CacheKey = "rev-notes"
const CacheKeyRevValues CacheKey = "rev-values"
const CacheKeyRepoChartValues CacheKey = "chart-values"
const CacheKeyAllRepos CacheKey = "all-repos"
type Cache struct { type Cache struct {
Marshaler *marshaler.Marshaler `json:"-"` Marshaler *marshaler.Marshaler `json:"-"`
HitCount int HitCount int
@@ -74,3 +83,18 @@ func (c *Cache) Clear() error {
c.MissCount = 0 c.MissCount = 0
return c.Marshaler.Clear(context.Background()) return c.Marshaler.Clear(context.Background())
} }
func cacheTagRelease(namespace string, name string) CacheKey {
return "release" + "\v" + namespace + "\v" + name
}
func cacheTagRepoVers(chartName string) CacheKey {
return "repo-versions" + "\v" + chartName
}
func cacheTagRepoCharts(name string) CacheKey {
return "repo-charts" + "\v" + name
}
func cacheTagRepoName(name string) CacheKey {
return "repo-name" + "\v" + name
}

View File

@@ -0,0 +1,307 @@
package subproc
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"helm.sh/helm/v3/pkg/release"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
)
type DataLayer struct {
KubeContext string
Helm string
Kubectl string
Scanners []Scanner
StatusInfo *StatusInfo
Namespace string
Cache *Cache
}
type StatusInfo struct {
CurVer string
LatestVer string
Analytics bool
LimitedToNamespace string
CacheHitRatio float64
ClusterMode bool
}
func (d *DataLayer) runCommand(cmd ...string) (string, error) {
for i, c := range cmd {
// TODO: remove namespace parameter if it's empty
if c == "--namespace" && i < len(cmd) { // TODO: in case it's not found - add it?
d.forceNamespace(&cmd[i+1])
}
}
return utils.RunCommand(cmd, map[string]string{"HELM_KUBECONTEXT": d.KubeContext})
}
func (d *DataLayer) runCommandHelm(cmd ...string) (string, error) {
if d.Helm == "" {
d.Helm = "helm"
}
cmd = append([]string{d.Helm}, cmd...)
if d.KubeContext != "" {
cmd = append(cmd, "--kube-context", d.KubeContext)
}
return d.runCommand(cmd...)
}
func (d *DataLayer) forceNamespace(s *string) {
if d.Namespace != "" {
*s = d.Namespace
}
}
func (d *DataLayer) CheckConnectivity() error {
contexts, err := d.ListContexts()
if err != nil {
return err
}
if len(contexts) < 1 {
log.Debugf("Did not find any contexts, will try checking k8s")
_, err := d.runCommandKubectl("get", "pods")
if err != nil {
log.Debugf("The error were: %s", err)
return errors.New("did not find any kubectl contexts configured")
}
log.Infof("Assuming k8s environment")
d.StatusInfo.ClusterMode = true
}
_, err = d.runCommandHelm("--help")
if err != nil {
return err
}
return nil
}
func (d *DataLayer) GetStatus() *StatusInfo {
sum := float64(d.Cache.HitCount + d.Cache.MissCount)
if sum > 0 {
d.StatusInfo.CacheHitRatio = float64(d.Cache.HitCount) / sum
} else {
d.StatusInfo.CacheHitRatio = 0
}
return d.StatusInfo
}
func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) {
cmd := []string{"ls", "--all", "--output", "json", "--time-format", time.RFC3339}
// TODO: filter by namespace
if d.Namespace == "" {
cmd = append(cmd, "--all-namespaces")
} else {
cmd = append(cmd, "--namespace", d.Namespace)
}
out, err := d.Cache.String(CacheKeyRelList, nil, func() (string, error) {
return d.runCommandHelm(cmd...)
})
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
return res, nil
}
func (d *DataLayer) ReleaseHistory(namespace string, releaseName string) (res []*HistoryElement, err error) {
// TODO: there is `max` but there is no `offset`
ct := cacheTagRelease(namespace, releaseName)
out, err := d.Cache.String(CacheKeyRelHistory+ct, []string{ct}, func() (string, error) {
return d.runCommandHelm("history", releaseName, "--namespace", namespace, "--output", "json")
})
if err != nil {
return nil, err
}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return nil, err
}
for _, elm := range res {
chartRepoName, curVer, err := utils.ChartAndVersion(elm.Chart)
if err != nil {
return nil, err
}
elm.ChartName = chartRepoName // TODO: move it to frontend?
elm.ChartVer = curVer
elm.Updated.Time = elm.Updated.Time.Round(time.Second)
}
return res, nil
}
type SectionFn = func(string, string, int, bool) (string, error) // TODO: rework it into struct-based argument?
func (d *DataLayer) RevisionManifests(namespace string, chartName string, revision int, _ bool) (res string, err error) {
cmd := []string{"get", "manifest", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision)}
key := CacheKeyRevManifests + "\v" + namespace + "\v" + chartName + "\v" + strconv.Itoa(revision)
return d.Cache.String(key, nil, func() (string, error) {
return d.runCommandHelm(cmd...)
})
}
func (d *DataLayer) RevisionManifestsParsed(namespace string, chartName string, revision int) ([]*v1.Carp, error) {
out, err := d.RevisionManifests(namespace, chartName, revision, false)
if err != nil {
return nil, err
}
dec := yaml.NewDecoder(bytes.NewReader([]byte(out)))
res := make([]*v1.Carp, 0)
var tmp interface{}
for dec.Decode(&tmp) == nil {
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
// we can juggle it
jsoned, err := json.Marshal(tmp)
if err != nil {
return nil, err
}
var doc v1.Carp
err = json.Unmarshal(jsoned, &doc)
if err != nil {
return nil, err
}
if doc.Kind == "" {
log.Warnf("Manifest piece is not k8s resource: %s", jsoned)
continue
}
res = append(res, &doc)
}
return res, nil
}
func (d *DataLayer) RevisionNotes(namespace string, chartName string, revision int, _ bool) (res string, err error) {
cmd := []string{"get", "notes", chartName, "--namespace", namespace, "--revision", strconv.Itoa(revision)}
key := CacheKeyRevNotes + "\v" + namespace + "\v" + chartName + "\v" + strconv.Itoa(revision)
return d.Cache.String(key, nil, func() (string, error) {
return d.runCommandHelm(cmd...)
})
}
func (d *DataLayer) RevisionValues(namespace string, chartName string, revision int, onlyUserDefined bool) (res string, err error) {
cmd := []string{"get", "values", chartName, "--namespace", namespace, "--output", "yaml", "--revision", strconv.Itoa(revision)}
if !onlyUserDefined {
cmd = append(cmd, "--all")
}
key := CacheKeyRevValues + "\v" + namespace + "\v" + chartName + "\v" + strconv.Itoa(revision) + "\v" + fmt.Sprintf("%v", onlyUserDefined)
return d.Cache.String(key, nil, func() (string, error) {
return d.runCommandHelm(cmd...)
})
}
func (d *DataLayer) ReleaseUninstall(namespace string, name string) error {
d.Cache.Invalidate(CacheKeyRelList, cacheTagRelease(namespace, name))
_, err := d.runCommandHelm("uninstall", name, "--namespace", namespace)
return err
}
func (d *DataLayer) Rollback(namespace string, name string, rev int) error {
d.Cache.Invalidate(CacheKeyRelList, cacheTagRelease(namespace, name))
_, err := d.runCommandHelm("rollback", name, strconv.Itoa(rev), "--namespace", namespace)
return err
}
func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string, version string, justTemplate bool, values string, reuseVals bool) (string, error) {
if values == "" && reuseVals {
oldVals, err := d.RevisionValues(namespace, name, 0, true)
if err != nil {
return "", err
}
values = oldVals
}
valsFile, close1, err := utils.TempFile(values)
defer close1()
if err != nil {
return "", err
}
cmd := []string{"upgrade", "--install", "--create-namespace", name, repoChart, "--version", version, "--namespace", namespace, "--values", valsFile, "--output", "json"}
if justTemplate {
cmd = append(cmd, "--dry-run")
}
out, err := d.runCommandHelm(cmd...)
if err != nil {
return "", err
}
if !justTemplate {
d.Cache.Invalidate(CacheKeyRelList, cacheTagRelease(namespace, name))
}
res := release.Release{}
err = json.Unmarshal([]byte(out), &res)
if err != nil {
return "", err
}
if justTemplate {
out = strings.TrimSpace(res.Manifest)
}
return out, nil
}
func RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) {
if revision1 == 0 || revision2 == 0 {
log.Debugf("One of revisions is zero: %d %d", revision1, revision2)
return "", nil
}
manifest1, err := functor(namespace, name, revision1, flag)
if err != nil {
return "", err
}
manifest2, err := functor(namespace, name, revision2, flag)
if err != nil {
return "", err
}
diff := GetDiff(manifest1, manifest2, strconv.Itoa(revision1)+ext, strconv.Itoa(revision2)+ext)
return diff, nil
}
func GetDiff(text1 string, text2 string, name1 string, name2 string) string {
edits := myers.ComputeEdits(span.URIFromPath(""), text1, text2)
unified := gotextdiff.ToUnified(name1, name2, text1, edits)
diff := fmt.Sprint(unified)
log.Debugf("The diff is: %s", diff)
return diff
}

View File

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

View File

@@ -0,0 +1,45 @@
package subproc
import (
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
)
// unpleasant copy from Helm sources, where they have it non-public
type ReleaseElement struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Revision string `json:"revision"`
Updated helmtime.Time `json:"updated"`
Status release.Status `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
}
type HistoryElement struct {
Revision int `json:"revision"`
Updated helmtime.Time `json:"updated"`
Status release.Status `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
ChartName string `json:"chart_name"` // custom addition on top of Helm
ChartVer string `json:"chart_ver"` // custom addition on top of Helm
}
type RepoChartElement struct {
Name string `json:"name"`
Version string `json:"version"`
AppVersion string `json:"app_version"`
Description string `json:"description"`
InstalledNamespace string `json:"installed_namespace"` // custom addition on top of Helm
InstalledName string `json:"installed_name"` // custom addition on top of Helm
}
type RepositoryElement struct {
Name string `json:"name"`
URL string `json:"url"`
}

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
"strconv"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -104,9 +105,10 @@ func RunCommand(cmd []string, env map[string]string) (string, error) {
type QueryProps struct { type QueryProps struct {
Namespace string Namespace string
Name string Name string
Revision int
} }
func GetQueryProps(c *gin.Context) (*QueryProps, error) { func GetQueryProps(c *gin.Context, revRequired bool) (*QueryProps, error) {
qp := QueryProps{} qp := QueryProps{}
qp.Namespace = c.Query("namespace") qp.Namespace = c.Query("namespace")
@@ -115,5 +117,11 @@ func GetQueryProps(c *gin.Context) (*QueryProps, error) {
return nil, errors.New("missing required query string parameter: name") return nil, errors.New("missing required query string parameter: name")
} }
cRev, err := strconv.Atoi(c.Query("revision"))
if err != nil && revRequired {
return nil, err
}
qp.Revision = cRev
return &qp, nil return &qp, nil
} }

View File

@@ -10,24 +10,34 @@ import (
func TestGetQueryProps(t *testing.T) { func TestGetQueryProps(t *testing.T) {
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
tests := []struct { tests := []struct {
name string name string
endpoint string endpoint string
wantErr bool revRequired bool
wantErr bool
}{ }{
{ {
name: "Get query props - all set with revRequired true", name: "Get query props - all set with revRequired true",
wantErr: false, wantErr: false,
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing&revision=1", revRequired: true,
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing&revision=1",
}, },
{ {
name: "Get query props - no namespace with revRequired true", name: "Get query props - no revision with revRequired true",
wantErr: false, wantErr: true,
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&revision=1", revRequired: true,
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing",
}, },
{ {
name: "Get query props - no name with revRequired true", name: "Get query props - no namespace with revRequired true",
wantErr: true, wantErr: false,
endpoint: "/api/v1/namespaces/komodorio/charts?namespace=testing&revision=1", revRequired: true,
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&revision=1",
},
{
name: "Get query props - no name with revRequired true",
wantErr: true,
revRequired: true,
endpoint: "/api/v1/namespaces/komodorio/charts?namespace=testing&revision=1",
}, },
} }
@@ -36,7 +46,7 @@ func TestGetQueryProps(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w) c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", tt.endpoint, nil) c.Request = httptest.NewRequest("GET", tt.endpoint, nil)
_, err := GetQueryProps(c) _, err := GetQueryProps(c, tt.revRequired)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("GetQueryProps() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("GetQueryProps() error = %v, wantErr %v", err, tt.wantErr)
return return

View File

@@ -1,5 +1,5 @@
name: "dashboard" name: "dashboard"
version: "1.3.0" version: "0.3.1"
usage: "A simplified way of working with Helm" usage: "A simplified way of working with Helm"
description: "View HELM situation in nice web UI" description: "View HELM situation in nice web UI"
command: "$HELM_PLUGIN_DIR/bin/helm-dashboard" command: "$HELM_PLUGIN_DIR/bin/helm-dashboard"

View File

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 270 KiB

View File

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -2,23 +2,16 @@
# Copied w/ love from the chartmuseum/helm-push :) # Copied w/ love from the chartmuseum/helm-push :)
[ ! -z "$HELM_DEBUG" ] && set -x
name="helm-dashboard" name="helm-dashboard"
repo="https://github.com/komodorio/${name}" repo="https://github.com/komodorio/${name}"
api_repo="https://api.github.com/repos/komodorio/${name}/releases/latest"
if [ -n "${HELM_PUSH_PLUGIN_NO_INSTALL_HOOK}" ]; then if [ -n "${HELM_PUSH_PLUGIN_NO_INSTALL_HOOK}" ]; then
echo "Development mode: not downloading versioned release." echo "Development mode: not downloading versioned release."
exit 0 exit 0
fi fi
version="$(curl -s ${api_repo} | grep '\"name\": "v.*\"' | cut -d 'v' -f 2 | cut -d '"' -f 1)" version="$(cat plugin.yaml | grep "version" | cut -d '"' -f 2)"
echo Tried to autodetect latest version: $version # TODO: if no version provided, get it from https://api.github.com/repos/komodorio/helm-dashboard/releases/latest
[ -z "$version" ] && {
version="$(cat plugin.yaml | grep "version" | cut -d '"' -f 2)"
echo Defaulted to version: $version
}
echo "Downloading and installing ${name} v${version} ..." echo "Downloading and installing ${name} v${version} ..."
url="" url=""
@@ -74,4 +67,4 @@ mv "releases/v${version}/${name}" "bin/${name}" || \
echo echo
echo "Helm Dashboard is installed, to start it, run in your terminal:" echo "Helm Dashboard is installed, to start it, run in your terminal:"
echo " helm dashboard" echo " helm dashboard"
echo echo