mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-24 11:48:04 +00:00
[WIP] Major release 1.0 (#147)
* Object model with self-sufficient binary (#131) * Code cosmetics * Experimenting with object model and direct HELM usage * Experiment with object model * replacing the kubectl * Progressing * Save the progress * Able to start with migration in mind * Migrated two pieces * List releases via Helm * Forgotten field * Cristallized the problem of ctx switcher * Reworked to multi-context * Rollback is also new style * More migration * Refactoring * Describe via code * Bye-bye kubectl binary * Eliminate more old code * Refactor a bit * Merges * No binaries in dockerfile * Commit * Progress with getting the data * Learned the thing about get * One field less * Sstart with repos * Repo add * repo remove * Repos! Icons! * Simplified access to data * Ver listing works * Ver check works * Caching and values * fixup * Done with repos * Working on install * Install work-ish * Fix UI failing on install * Upgrade flow works * Fix image building * Remove outdated test file * Move files around * REfactorings * Cosmetics * Test for cache control (#151) * Files import formatted * Added go-test tools * Added test for no-cache header * added changes * test for cache behaviour of app * test for static route (#153) * Tests: route configuration & context setter (#154) * Test for route configuration * Test for context setter middleware * implemented changes * Restore coverage profile Fixes #156 * Cosmetics * Test for `NewRouter` function (#157) * Test for `configureScanners` (#158) * Test for `configureKubectls` (#163) * Test for repository loading (#169) - Created `repos_test.go` - Test: `Load()` of Repositories * Build all PRs * Fixes failing test (#171) * Fixes failing test - Fixes failing test of repo loading * handles error for * Did some changes * Test for listing of repos (#173) - and did some code formatting Signed-off-by: OmAxiani0 <aximaniom@gmail.com> Signed-off-by: OmAxiani0 <aximaniom@gmail.com> * Test for adding repo (#175) - Modified the `repositories.yml` file Signed-off-by: OmAxiani0 <aximaniom@gmail.com> Signed-off-by: OmAxiani0 <aximaniom@gmail.com> * Test for deleting the repository (#176) * Test for deleting the repository - Also added cleanup function for `TestAdd` * Fixes failing test * Add auto labeler for PR's (#174) * Add auto labeler for PR's * Add all file under .github/workflow to 'ci' label Co-authored-by: Harshit Mehta <harshitm@nvidia.com> * Test for getting repository (#177) * Add github workflow for auto PR labeling (#181) Co-authored-by: Harshit Mehta <harshitm@nvidia.com> * Stub compilation * Fixes around installing * More complex test * Using object model to execute helm test (#191) * Expand test * More test * Coverage * Add mutex for operations * Rectore cluster detection code * Change receiver to pointer * Support multiple namespaces * Cosmetics * Update repos periodically * fix tests * Fix error display * Allow reconfiguring chart without repo * mute linter * Cosmetics * Failing approach to parse manifests Relates to #30 * Report the error properly * ✅ Add test for dashboard/objects/data.go NewDataLayer (#199) * Fix problem of wrong namespace * Added unit tests for releases (#204) * Rework API routes (#197) * Bootstrap OpenAPI doc * Renaming some routes * Listing namespaces * k8s part of things * Repositories section * Document scanners API * One more API call * Progress * Reworked install flow * History endpoint * Textual info section * Resources endpoint * Rollback endpoint * Rollback endpoint * Unit tests * Cleanup * Forgotten tags * Fix tests * TODOs * Rework manifest scanning * add hasTests flag * Adding more information on UI for helm test API response (#195) * Hide test button when no tests Fixes #115 Improves #195 --------- Signed-off-by: OmAxiani0 <aximaniom@gmail.com> Co-authored-by: Om Aximani <75031769+OmAximani0@users.noreply.github.com> Co-authored-by: Harshit Mehta <hdm23061993@gmail.com> Co-authored-by: Harshit Mehta <harshitm@nvidia.com> Co-authored-by: Todd Turner <todd@toddtee.sh> Co-authored-by: arvindsundararajan98 <109727359+arvindsundararajan98@users.noreply.github.com>
This commit is contained in:
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
@@ -60,5 +60,9 @@ release:
|
|||||||
scanners:
|
scanners:
|
||||||
- pkg/dashboard/scanners/*
|
- pkg/dashboard/scanners/*
|
||||||
|
|
||||||
|
tests:
|
||||||
|
- pkg/dashboard/**/*_test.go
|
||||||
|
- pkg/dashboard/objects/testdata/*
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
- pkg/dashboard/static/*
|
- pkg/dashboard/static/*
|
||||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: main
|
branches: main
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: main
|
branches: "*"
|
||||||
|
|
||||||
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 ./... # Run all the tests with the race detector enabled
|
go test -v -race ./... -covermode=atomic # Run all the tests with the race detector enabled
|
||||||
- name: Static analysis
|
- 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.2.0
|
uses: golangci/golangci-lint-action@v3.3.1
|
||||||
with:
|
with:
|
||||||
# version: latest
|
# version: latest
|
||||||
# skip-go-installation: true
|
# skip-go-installation: true
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@@ -23,13 +23,11 @@ WORKDIR /build/src
|
|||||||
RUN make build
|
RUN make build
|
||||||
|
|
||||||
# Stage - runner
|
# Stage - runner
|
||||||
FROM alpine/helm
|
FROM alpine
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
RUN apk add --update --no-cache python3 && python3 -m ensurepip && pip3 install --upgrade pip setuptools
|
RUN apk add --update --no-cache python3 curl && 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
|
||||||
@@ -40,6 +38,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"]
|
ENTRYPOINT ["/bin/helm-dashboard", "--no-browser", "--bind=0.0.0.0", "--port=8080"]
|
||||||
|
|
||||||
# docker build . -t komodorio/helm-dashboard:0.0.0 && kind load docker-image komodorio/helm-dashboard:0.0.0
|
# docker build . -t komodorio/helm-dashboard:0.0.0 && kind load docker-image komodorio/helm-dashboard:0.0.0
|
||||||
2
Makefile
2
Makefile
@@ -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 -coverprofile=profile.cov
|
@go test $$(go list ./... | grep -v /mocks/) --race -v -short -coverpkg=./... -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"
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -31,9 +31,15 @@ Some of the key capabilities of the tool:
|
|||||||
|
|
||||||
## 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 the plugin, simply run Helm command:
|
To install dashboard as Helm 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
|
||||||
@@ -72,7 +78,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.
|
If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line. You can specify multiple namespaces, separated by commas.
|
||||||
|
|
||||||
If you don't want browser tab to automatically open, add `--no-browser` flag in your command line.
|
If you don't want browser tab to automatically open, add `--no-browser` flag in your command line.
|
||||||
|
|
||||||
@@ -84,9 +90,6 @@ 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)
|
||||||
|
|
||||||
### Manual Installation
|
|
||||||
|
|
||||||
Download the appropriate [release package](https://github.com/komodorio/helm-dashboard/releases) for your platform, unpack it and just run `dashboard` binary from it.
|
|
||||||
|
|
||||||
## Execute Helm tests
|
## Execute Helm tests
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ helm upgrade --install my-release komodorio/helm-dashboard
|
|||||||
|
|
||||||
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+
|
||||||
|
|||||||
131
go.mod
131
go.mod
@@ -3,68 +3,169 @@ module github.com/komodorio/helm-dashboard
|
|||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
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
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
helm.sh/helm/v3 v3.10.3
|
helm.sh/helm/v3 v3.10.3
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
|
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||||
|
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||||
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||||
|
github.com/Masterminds/sprig/v3 v3.2.2 // 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-20220106215444-fb4bf637b56d // indirect
|
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 // 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.6 // 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.17+incompatible // indirect
|
||||||
|
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||||
|
github.com/docker/docker v20.10.17+incompatible // indirect
|
||||||
|
github.com/docker/docker-credential-helpers v0.6.4 // 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/fsnotify/fsnotify v1.5.4 // 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.0.2 // 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.2 // 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.6 // 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.12 // 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.2-0.20181231171920-c182affec369 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
|
||||||
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/moby/locker v1.0.1 // indirect
|
||||||
|
github.com/moby/spdystream v0.2.0 // indirect
|
||||||
|
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // 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.0.3-0.20211202183452-c5a74bcca799 // 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/prometheus/client_golang v1.12.2 // indirect
|
github.com/peterbourgon/diskv v2.0.1+incompatible // 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.1.2 // 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.0 // 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
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
|
github.com/xlab/treeprint v1.1.0 // indirect
|
||||||
|
go.etcd.io/etcd/api/v3 v3.5.4 // indirect
|
||||||
|
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
|
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect
|
||||||
golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf // indirect
|
golang.org/x/exp v0.0.0-20221110155412-d0897a79cd37 // indirect
|
||||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
|
golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
|
||||||
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/sys v0.3.0 // indirect
|
||||||
|
golang.org/x/term v0.3.0 // indirect
|
||||||
|
golang.org/x/text v0.5.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/klog/v2 v2.70.1 // indirect
|
gotest.tools/v3 v3.4.0
|
||||||
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
|
k8s.io/apiextensions-apiserver v0.25.2 // indirect
|
||||||
|
k8s.io/apiserver v0.25.2 // 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
|
||||||
|
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
|
||||||
|
oras.land/oras-go v1.2.0 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
31
main.go
31
main.go
@@ -1,8 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"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,8 +27,8 @@ type options struct {
|
|||||||
NoBrowser bool `short:"b" long:"no-browser" description:"Do not attempt to open Web browser upon start"`
|
NoBrowser bool `short:"b" long:"no-browser" description:"Do not attempt to open Web browser upon start"`
|
||||||
NoTracking bool `long:"no-analytics" description:"Disable user analytics (GA, DataDog etc.)"`
|
NoTracking bool `long:"no-analytics" description:"Disable user analytics (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"` // TODO: better default port to clash less?
|
Port uint `short:"p" long:"port" description:"Port to start server on" default:"8080"`
|
||||||
Namespace string `short:"n" long:"namespace" description:"Limit operations to a specific namespace"`
|
Namespace string `short:"n" long:"namespace" description:"Namespace for HELM operations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -42,12 +46,26 @@ func main() {
|
|||||||
|
|
||||||
server := dashboard.Server{
|
server := dashboard.Server{
|
||||||
Version: version,
|
Version: version,
|
||||||
Namespace: opts.Namespace,
|
Namespaces: strings.Split(opts.Namespace, ","),
|
||||||
Address: fmt.Sprintf("%s:%d", opts.BindHost, opts.Port),
|
Address: fmt.Sprintf("%s:%d", opts.BindHost, opts.Port),
|
||||||
Debug: opts.Verbose,
|
Debug: opts.Verbose,
|
||||||
NoTracking: opts.NoTracking,
|
NoTracking: opts.NoTracking,
|
||||||
}
|
}
|
||||||
address, webServerDone := server.StartServer()
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
osSignal := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(osSignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
oscall := <-osSignal
|
||||||
|
log.Warnf("Stopping on signal: %s\n", oscall)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
address, webServerDone, err := server.StartServer(ctx, cancel)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to start Helm Dashboard: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if !opts.NoTracking {
|
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")
|
||||||
@@ -69,7 +87,7 @@ func main() {
|
|||||||
|
|
||||||
func parseFlags() options {
|
func parseFlags() options {
|
||||||
ns := os.Getenv("HELM_NAMESPACE")
|
ns := os.Getenv("HELM_NAMESPACE")
|
||||||
if ns == "default" {
|
if ns == "default" { // it's how Helm passes to plugin the empty NS, we have to reset it back
|
||||||
ns = ""
|
ns = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +110,8 @@ func parseFlags() options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
panic("The program does not take argumants, see --help for usage")
|
fmt.Println("The program does not take arguments, see --help for usage")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
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) {
|
||||||
c.Header("Cache-Control", "no-cache")
|
if c.GetHeader("Cache-Control") == "" { // default policy is not to cache
|
||||||
|
c.Header("Cache-Control", "no-cache")
|
||||||
|
}
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,33 +28,39 @@ func errorHandler(c *gin.Context) {
|
|||||||
|
|
||||||
errs := ""
|
errs := ""
|
||||||
for _, err := range c.Errors {
|
for _, err := range c.Errors {
|
||||||
log.Debugf("Error: %s", err)
|
log.Debugf("Error: %+v", err)
|
||||||
errs += err.Error() + "\n"
|
errs += err.Error() + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
if errs != "" {
|
if errs != "" {
|
||||||
c.String(http.StatusInternalServerError, errs)
|
c.String(http.StatusInternalServerError, html.EscapeString(errs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextSetter(data *subproc.DataLayer) gin.HandlerFunc {
|
func contextSetter(data *objects.DataLayer) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
ctxName := ""
|
||||||
if ctx, ok := c.Request.Header["X-Kubecontext"]; ok {
|
if ctx, ok := c.Request.Header["X-Kubecontext"]; ok {
|
||||||
log.Debugf("Setting current context to: %s", ctx)
|
ctxName = ctx[0]
|
||||||
if data.KubeContext != ctx[0] {
|
if err := data.SetContext(ctxName); err != nil {
|
||||||
err := data.Cache.Clear()
|
c.String(http.StatusInternalServerError, err.Error())
|
||||||
if err != nil {
|
return
|
||||||
_ = 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 utils.ControlChan, data *subproc.DataLayer, debug bool) *gin.Engine {
|
func NewRouter(abortWeb context.CancelFunc, data *objects.DataLayer, debug bool) *gin.Engine {
|
||||||
var api *gin.Engine
|
var api *gin.Engine
|
||||||
if debug {
|
if debug {
|
||||||
api = gin.New()
|
api = gin.New()
|
||||||
@@ -71,10 +79,10 @@ func NewRouter(abortWeb utils.ControlChan, data *subproc.DataLayer, debug bool)
|
|||||||
return api
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *gin.Engine) {
|
func configureRoutes(abortWeb context.CancelFunc, data *objects.DataLayer, api *gin.Engine) {
|
||||||
// server shutdown handler
|
// server shutdown handler
|
||||||
api.DELETE("/", func(c *gin.Context) {
|
api.DELETE("/", func(c *gin.Context) {
|
||||||
abortWeb <- struct{}{}
|
abortWeb()
|
||||||
c.Status(http.StatusAccepted)
|
c.Status(http.StatusAccepted)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -83,11 +91,11 @@ func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *g
|
|||||||
c.IndentedJSON(http.StatusOK, data.GetStatus())
|
c.IndentedJSON(http.StatusOK, data.GetStatus())
|
||||||
})
|
})
|
||||||
|
|
||||||
api.GET("/api/cache", func(c *gin.Context) {
|
api.GET("/api/cache", func(c *gin.Context) { // TODO: included into OpenAPI or not?
|
||||||
c.IndentedJSON(http.StatusOK, data.Cache)
|
c.IndentedJSON(http.StatusOK, data.Cache)
|
||||||
})
|
})
|
||||||
|
|
||||||
api.DELETE("/api/cache", func(c *gin.Context) {
|
api.DELETE("/api/cache", func(c *gin.Context) { // TODO: included into OpenAPI or not?
|
||||||
err := data.Cache.Clear()
|
err := data.Cache.Clear()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
@@ -96,40 +104,63 @@ func configureRoutes(abortWeb utils.ControlChan, data *subproc.DataLayer, api *g
|
|||||||
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/kube"), data)
|
configureKubectls(api.Group("/api/k8s"), data)
|
||||||
configureScanners(api.Group("/api/scanners"), data)
|
configureScanners(api.Group("/api/scanners"), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureHelms(api *gin.RouterGroup, data *subproc.DataLayer) {
|
func configureHelms(api *gin.RouterGroup, data *objects.DataLayer) {
|
||||||
h := handlers.HelmHandler{Data: data}
|
h := handlers.HelmHandler{
|
||||||
|
Contexted: &handlers.Contexted{
|
||||||
|
Data: data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
api.GET("/charts", h.GetCharts)
|
rels := api.Group("/releases")
|
||||||
api.DELETE("/charts", h.Uninstall)
|
rels.GET("", h.GetReleases)
|
||||||
|
rels.POST(":ns", h.Install)
|
||||||
|
rels.POST(":ns/:name", h.Upgrade)
|
||||||
|
rels.DELETE(":ns/:name", h.Uninstall)
|
||||||
|
rels.GET(":ns/:name/history", h.History)
|
||||||
|
rels.GET(":ns/:name/:section", h.GetInfoSection)
|
||||||
|
rels.GET(":ns/:name/resources", h.Resources)
|
||||||
|
rels.POST(":ns/:name/rollback", h.Rollback)
|
||||||
|
rels.POST(":ns/:name/test", h.RunTests)
|
||||||
|
|
||||||
api.GET("/charts/history", h.History)
|
repos := api.Group("/repositories")
|
||||||
api.GET("/charts/resources", h.Resources)
|
repos.GET("", h.RepoList)
|
||||||
api.GET("/charts/:section", h.GetInfoSection)
|
repos.POST("", h.RepoAdd)
|
||||||
api.GET("/charts/show", h.Show)
|
repos.GET("/:name", h.RepoCharts)
|
||||||
api.POST("/charts/install", h.Install)
|
repos.POST("/:name", h.RepoUpdate)
|
||||||
api.POST("/charts/tests", h.Tests)
|
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 *subproc.DataLayer) {
|
func configureKubectls(api *gin.RouterGroup, data *objects.DataLayer) {
|
||||||
h := handlers.KubeHandler{Data: data}
|
h := handlers.KubeHandler{
|
||||||
|
Contexted: &handlers.Contexted{
|
||||||
|
Data: data,
|
||||||
|
},
|
||||||
|
}
|
||||||
api.GET("/contexts", h.GetContexts)
|
api.GET("/contexts", h.GetContexts)
|
||||||
api.GET("/resources/:kind", h.GetResourceInfo)
|
api.GET("/:kind/get", h.GetResourceInfo)
|
||||||
api.GET("/describe/:kind", h.Describe)
|
api.GET("/:kind/describe", h.Describe)
|
||||||
api.GET("/namespaces", h.GetNameSpaces)
|
api.GET("/:kind/list", h.GetNameSpaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureStatic(api *gin.Engine) {
|
func configureStatic(api *gin.Engine) {
|
||||||
@@ -162,9 +193,13 @@ func configureStatic(api *gin.Engine) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureScanners(api *gin.RouterGroup, data *subproc.DataLayer) {
|
func configureScanners(api *gin.RouterGroup, data *objects.DataLayer) {
|
||||||
h := handlers.ScannersHandler{Data: data}
|
h := handlers.ScannersHandler{
|
||||||
|
Contexted: &handlers.Contexted{
|
||||||
|
Data: data,
|
||||||
|
},
|
||||||
|
}
|
||||||
api.GET("", h.List)
|
api.GET("", h.List)
|
||||||
api.POST("/manifests", h.ScanDraftManifest)
|
api.POST("/manifests", h.ScanManifest)
|
||||||
api.GET("/resource/:kind", h.ScanResource)
|
api.GET("/resource/:kind", h.ScanResource)
|
||||||
}
|
}
|
||||||
|
|||||||
412
pkg/dashboard/api_test.go
Normal file
412
pkg/dashboard/api_test.go
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
package dashboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
31
pkg/dashboard/handlers/common.go
Normal file
31
pkg/dashboard/handlers/common.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/joomcode/errorx"
|
||||||
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const APP = "app"
|
||||||
|
|
||||||
|
type Contexted struct {
|
||||||
|
Data *objects.DataLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Contexted) GetApp(c *gin.Context) *objects.Application {
|
||||||
|
var app *objects.Application
|
||||||
|
if a, ok := c.Get(APP); ok {
|
||||||
|
app = a.(*objects.Application)
|
||||||
|
} else {
|
||||||
|
err := errorx.IllegalState.New("No application context found")
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Contexted) EnableClientCache(c *gin.Context) {
|
||||||
|
c.Header("Cache-Control", "max-age=43200")
|
||||||
|
}
|
||||||
@@ -2,35 +2,72 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/hexops/gotextdiff"
|
||||||
|
"github.com/hexops/gotextdiff/myers"
|
||||||
|
"github.com/hexops/gotextdiff/span"
|
||||||
|
"github.com/joomcode/errorx"
|
||||||
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||||
|
"github.com/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"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HelmHandler struct {
|
type HelmHandler struct {
|
||||||
Data *subproc.DataLayer
|
*Contexted
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) GetCharts(c *gin.Context) {
|
func (h *HelmHandler) getRelease(c *gin.Context) *objects.Release {
|
||||||
res, err := h.Data.ListInstalled()
|
app := h.GetApp(c)
|
||||||
|
if app == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, err := app.Releases.ByName(c.Param("ns"), c.Param("name"))
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return rel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) GetReleases(c *gin.Context) {
|
||||||
|
app := h.GetApp(c)
|
||||||
|
if app == nil {
|
||||||
|
return // sets error inside
|
||||||
|
}
|
||||||
|
|
||||||
|
rels, err := app.Releases.List()
|
||||||
if err != nil {
|
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) {
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
rel := h.getRelease(c)
|
||||||
if err != nil {
|
if rel == nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
return // error state is set inside
|
||||||
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
|
||||||
@@ -39,13 +76,18 @@ func (h *HelmHandler) Uninstall(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) Rollback(c *gin.Context) {
|
func (h *HelmHandler) Rollback(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, true)
|
rel := h.getRelease(c)
|
||||||
|
if rel == nil {
|
||||||
|
return // error state is set inside
|
||||||
|
}
|
||||||
|
|
||||||
|
revn, err := strconv.Atoi(c.PostForm("revision"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.Data.Rollback(qp.Namespace, qp.Name, qp.Revision)
|
err = rel.Rollback(revn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -54,73 +96,178 @@ func (h *HelmHandler) Rollback(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) History(c *gin.Context) {
|
func (h *HelmHandler) History(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
rel := h.getRelease(c)
|
||||||
if err != nil {
|
if rel == nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
return // error state is set inside
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := h.Data.ReleaseHistory(qp.Namespace, qp.Name)
|
revs, err := rel.History()
|
||||||
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) {
|
||||||
qp, err := utils.GetQueryProps(c, true)
|
h.EnableClientCache(c)
|
||||||
if err != nil {
|
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
rel := h.getRelease(c)
|
||||||
return
|
if rel == nil {
|
||||||
|
return // error state is set inside
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := h.Data.RevisionManifestsParsed(qp.Namespace, qp.Name, qp.Revision)
|
res, err := objects.ParseManifests(rel.Orig.Manifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) RepoSearch(c *gin.Context) {
|
func (h *HelmHandler) RepoVersions(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
qp, err := utils.GetQueryProps(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := h.Data.ChartRepoVersions(qp.Name)
|
app := h.GetApp(c)
|
||||||
|
if app == nil {
|
||||||
|
return // sets error inside
|
||||||
|
}
|
||||||
|
|
||||||
|
repos, err := app.Repositories.Containing(qp.Name)
|
||||||
if err != nil {
|
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],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) RepoLatestVer(c *gin.Context) {
|
||||||
|
qp, err := utils.GetQueryProps(c)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app := h.GetApp(c)
|
||||||
|
if app == nil {
|
||||||
|
return // sets error inside
|
||||||
|
}
|
||||||
|
|
||||||
|
rep, err := app.Repositories.Containing(qp.Name)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := []*RepoChartElement{}
|
||||||
|
for _, r := range rep {
|
||||||
|
res = append(res, &RepoChartElement{
|
||||||
|
Name: r.Name,
|
||||||
|
Version: r.Version,
|
||||||
|
AppVersion: r.AppVersion,
|
||||||
|
Description: r.Description,
|
||||||
|
Repository: r.Annotations[objects.AnnRepo],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) RepoCharts(c *gin.Context) {
|
func (h *HelmHandler) RepoCharts(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
app := h.GetApp(c)
|
||||||
if err != nil {
|
if app == nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
return // sets error inside
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := h.Data.ChartRepoCharts(qp.Name)
|
rep, err := app.Repositories.Get(c.Param("name"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HelmHandler) RepoUpdate(c *gin.Context) {
|
charts, err := rep.Charts()
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.Data.ChartRepoUpdate(qp.Name)
|
installed, err := app.Releases.List()
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: enrich with installed
|
||||||
|
enrichRepoChartsWithInstalled(charts, installed)
|
||||||
|
|
||||||
|
sort.Slice(charts, func(i, j int) bool {
|
||||||
|
return charts[i].Name < charts[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
|
c.IndentedJSON(http.StatusOK, charts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func enrichRepoChartsWithInstalled(charts []*repo.ChartVersion, installed []*objects.Release) {
|
||||||
|
for _, rchart := range charts {
|
||||||
|
for _, rel := range installed {
|
||||||
|
if rchart.Metadata.Name == rel.Orig.Chart.Name() {
|
||||||
|
log.Debugf("Matched") // TODO: restore implementation
|
||||||
|
// TODO: there can be more than one
|
||||||
|
//rchart.InstalledNamespace = rel.Orig.Namespace
|
||||||
|
//rchart.InstalledName = rel.Orig.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) RepoUpdate(c *gin.Context) {
|
||||||
|
app := h.GetApp(c)
|
||||||
|
if app == nil {
|
||||||
|
return // sets error inside
|
||||||
|
}
|
||||||
|
|
||||||
|
rep, err := app.Repositories.Get(c.Param("name"))
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rep.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -128,80 +275,125 @@ func (h *HelmHandler) RepoUpdate(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) Show(c *gin.Context) {
|
func (h *HelmHandler) Install(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
app := h.GetApp(c)
|
||||||
if err != nil {
|
if app == nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
return // sets error inside
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := h.Data.ShowChart(qp.Name)
|
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.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
justTemplate := c.PostForm("preview") == "true"
|
||||||
}
|
ns := c.Param("ns")
|
||||||
|
if ns == "[empty]" {
|
||||||
func (h *HelmHandler) Install(c *gin.Context) {
|
ns = ""
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
rel, err := app.Releases.Install(ns, c.PostForm("name"), c.PostForm("chart"), c.PostForm("version"), justTemplate, values)
|
||||||
justTemplate := c.Query("flag") != "true"
|
|
||||||
isInitial := c.Query("initial") != "true"
|
|
||||||
out, err := h.Data.ChartInstall(qp.Namespace, qp.Name, c.Query("chart"), c.Query("version"), justTemplate, c.PostForm("values"), isInitial)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if justTemplate {
|
if justTemplate {
|
||||||
manifests := ""
|
c.IndentedJSON(http.StatusOK, rel)
|
||||||
if isInitial {
|
|
||||||
manifests, err = h.Data.RevisionManifests(qp.Namespace, qp.Name, 0, false)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out = subproc.GetDiff(strings.TrimSpace(manifests), out, "current.yaml", "upgraded.yaml")
|
|
||||||
} else {
|
} else {
|
||||||
c.Header("Content-Type", "application/json")
|
c.IndentedJSON(http.StatusAccepted, rel)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.String(http.StatusAccepted, out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) Tests(c *gin.Context) {
|
func (h *HelmHandler) Upgrade(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
app := h.GetApp(c)
|
||||||
if err != nil {
|
if app == nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
return // sets error inside
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := h.Data.RunTests(qp.Namespace, qp.Name)
|
existing, err := app.Releases.ByName(c.Param("ns"), c.Param("name"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
values := map[string]interface{}{}
|
||||||
|
err = yaml.Unmarshal([]byte(c.PostForm("values")), &values)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
justTemplate := c.PostForm("preview") == "true"
|
||||||
|
rel, err := existing.Upgrade(c.PostForm("chart"), c.PostForm("version"), justTemplate, values)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if justTemplate {
|
||||||
|
c.IndentedJSON(http.StatusOK, rel)
|
||||||
|
} else {
|
||||||
|
c.IndentedJSON(http.StatusAccepted, rel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HelmHandler) RunTests(c *gin.Context) {
|
||||||
|
rel := h.getRelease(c)
|
||||||
|
if rel == nil {
|
||||||
|
return // error state is set inside
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := rel.RunTests()
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
c.String(http.StatusOK, out)
|
c.String(http.StatusOK, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) GetInfoSection(c *gin.Context) {
|
func (h *HelmHandler) GetInfoSection(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, true)
|
if c.Query("revision") != "" { // don't cache if latest is requested
|
||||||
if err != nil {
|
h.EnableClientCache(c)
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
}
|
||||||
|
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
flag := c.Query("flag") == "true"
|
rev, err := rel.GetRev(revn)
|
||||||
rDiff := c.Query("revisionDiff")
|
if err != nil {
|
||||||
res, err := handleGetSection(h.Data, c.Param("section"), rDiff, qp, flag)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var revDiff *objects.Release
|
||||||
|
revS := c.Query("revisionDiff")
|
||||||
|
if revS != "" {
|
||||||
|
revN, err := strconv.Atoi(revS)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
revDiff, err = rel.GetRev(revN)
|
||||||
|
if err != nil {
|
||||||
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flag := c.Query("userDefined") == "true"
|
||||||
|
|
||||||
|
res, err := h.handleGetSection(rev, c.Param("section"), revDiff, flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -210,25 +402,53 @@ func (h *HelmHandler) GetInfoSection(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) RepoValues(c *gin.Context) {
|
func (h *HelmHandler) RepoValues(c *gin.Context) {
|
||||||
out, err := h.Data.ShowValues(c.Query("chart"), c.Query("version"))
|
h.EnableClientCache(c)
|
||||||
|
|
||||||
|
app := h.GetApp(c)
|
||||||
|
if app == nil {
|
||||||
|
return // sets error inside
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := app.Repositories.GetChartValues(c.Query("chart"), c.Query("version"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
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) {
|
||||||
out, err := h.Data.ChartRepoList()
|
app := h.GetApp(c)
|
||||||
|
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.Orig.Name,
|
||||||
|
URL: r.Orig.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) {
|
||||||
_, err := h.Data.ChartRepoAdd(c.PostForm("name"), c.PostForm("url"))
|
app := h.GetApp(c)
|
||||||
|
if app == nil {
|
||||||
|
return // sets error inside
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: more repo options to accept
|
||||||
|
err := app.Repositories.Add(c.PostForm("name"), c.PostForm("url"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -237,13 +457,12 @@ func (h *HelmHandler) RepoAdd(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *HelmHandler) RepoDelete(c *gin.Context) {
|
func (h *HelmHandler) RepoDelete(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
app := h.GetApp(c)
|
||||||
if err != nil {
|
if app == nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
return // sets error inside
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = h.Data.ChartRepoDelete(qp.Name)
|
err := app.Repositories.Delete(c.Param("name"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -251,11 +470,30 @@ func (h *HelmHandler) RepoDelete(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetSection(data *subproc.DataLayer, section string, rDiff string, qp *utils.QueryProps, flag bool) (string, error) {
|
func (h *HelmHandler) handleGetSection(rel *objects.Release, section string, rDiff *objects.Release, flag bool) (string, error) {
|
||||||
sections := map[string]subproc.SectionFn{
|
sections := map[string]objects.SectionFn{
|
||||||
"manifests": data.RevisionManifests,
|
"manifests": func(qp *release.Release, b bool) (string, error) { return qp.Manifest, nil },
|
||||||
"values": data.RevisionValues,
|
"notes": func(qp *release.Release, b bool) (string, error) { return qp.Info.Notes, nil },
|
||||||
"notes": data.RevisionNotes,
|
"values": func(qp *release.Release, b bool) (string, error) {
|
||||||
|
allVals := qp.Config
|
||||||
|
|
||||||
|
if !b {
|
||||||
|
merged, err := chartutil.CoalesceValues(qp.Chart, qp.Config)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to merge chart vals with user defined")
|
||||||
|
}
|
||||||
|
allVals = merged
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allVals) > 0 {
|
||||||
|
data, err := yaml.Marshal(allVals)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to serialize values into YAML")
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
functor, found := sections[section]
|
functor, found := sections[section]
|
||||||
@@ -263,27 +501,130 @@ func handleGetSection(data *subproc.DataLayer, section string, rDiff string, qp
|
|||||||
return "", errors.New("unsupported section: " + section)
|
return "", errors.New("unsupported section: " + section)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rDiff != "" {
|
if rDiff != nil {
|
||||||
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 := subproc.RevisionDiff(functor, ext, qp.Namespace, qp.Name, cRevDiff, qp.Revision, flag)
|
res, err := RevisionDiff(functor, ext, rDiff.Orig, rel.Orig, flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := functor(qp.Namespace, qp.Name, qp.Revision, flag)
|
res, err := functor(rel.Orig, flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", errorx.Decorate(err, "failed to get section info")
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
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"`
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/joomcode/errorx"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
|
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
v12 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type KubeHandler struct {
|
type KubeHandler struct {
|
||||||
Data *subproc.DataLayer
|
*Contexted
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -24,21 +29,33 @@ 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, false)
|
qp, err := utils.GetQueryProps(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := h.Data.GetResource(qp.Namespace, &v12.Carp{
|
app := h.GetApp(c)
|
||||||
TypeMeta: v1.TypeMeta{Kind: c.Param("kind")},
|
if app == nil {
|
||||||
ObjectMeta: v1.ObjectMeta{Name: qp.Name},
|
return // sets error inside
|
||||||
})
|
}
|
||||||
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)
|
||||||
|
|
||||||
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnhanceStatus(res *v12.Carp) {
|
||||||
// custom logic to provide most meaningful status for the resource
|
// custom logic to provide most meaningful status for the resource
|
||||||
if res.Status.Phase == "Active" || res.Status.Phase == "Error" {
|
if res.Status.Phase == "Active" || res.Status.Phase == "Error" {
|
||||||
_ = res.Name + ""
|
_ = res.Name + ""
|
||||||
@@ -52,18 +69,21 @@ func (h *KubeHandler) GetResourceInfo(c *gin.Context) {
|
|||||||
} else if res.Status.Phase == "" {
|
} else if res.Status.Phase == "" {
|
||||||
res.Status.Phase = "Exists"
|
res.Status.Phase = "Exists"
|
||||||
}
|
}
|
||||||
|
|
||||||
c.IndentedJSON(http.StatusOK, res)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *KubeHandler) Describe(c *gin.Context) {
|
func (h *KubeHandler) Describe(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
qp, err := utils.GetQueryProps(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := h.Data.DescribeResource(qp.Namespace, c.Param("kind"), qp.Name)
|
app := h.GetApp(c)
|
||||||
|
if app == nil {
|
||||||
|
return // sets error inside
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := app.K8s.DescribeResource(c.Param("kind"), qp.Namespace, qp.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -73,11 +93,21 @@ func (h *KubeHandler) Describe(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *KubeHandler) GetNameSpaces(c *gin.Context) {
|
func (h *KubeHandler) GetNameSpaces(c *gin.Context) {
|
||||||
res, err := h.Data.GetNameSpaces()
|
if c.Param("kind") != "namespaces" {
|
||||||
|
_ = c.AbortWithError(http.StatusBadRequest, errorx.AssertionFailed.New("Only 'namespaces' kind is allowed for listing"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app := h.GetApp(c)
|
||||||
|
if app == nil {
|
||||||
|
return // sets error inside
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := app.K8s.GetNameSpaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, res)
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ScannersHandler struct {
|
type ScannersHandler struct {
|
||||||
Data *subproc.DataLayer
|
*Contexted
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScannersHandler) List(c *gin.Context) {
|
func (h *ScannersHandler) List(c *gin.Context) {
|
||||||
@@ -26,23 +26,10 @@ func (h *ScannersHandler) List(c *gin.Context) {
|
|||||||
c.IndentedJSON(http.StatusOK, res)
|
c.IndentedJSON(http.StatusOK, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) {
|
func (h *ScannersHandler) ScanManifest(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(mnf)
|
sr, err := scanner.ScanManifests(c.PostForm("manifest"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@@ -55,7 +42,7 @@ func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScannersHandler) ScanResource(c *gin.Context) {
|
func (h *ScannersHandler) ScanResource(c *gin.Context) {
|
||||||
qp, err := utils.GetQueryProps(c, false)
|
qp, err := utils.GetQueryProps(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, err)
|
_ = c.AbortWithError(http.StatusBadRequest, err)
|
||||||
return
|
return
|
||||||
|
|||||||
49
pkg/dashboard/objects/app.go
Normal file
49
pkg/dashboard/objects/app.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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) (*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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Application{
|
||||||
|
HelmConfig: helmConfig,
|
||||||
|
K8s: k8s,
|
||||||
|
Releases: &Releases{
|
||||||
|
Namespaces: namespaces,
|
||||||
|
Settings: settings,
|
||||||
|
HelmConfig: helmConfig,
|
||||||
|
},
|
||||||
|
Repositories: &Repositories{
|
||||||
|
Settings: settings,
|
||||||
|
HelmConfig: hc,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package subproc
|
package objects
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -12,15 +12,6 @@ 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
|
||||||
@@ -83,18 +74,3 @@ 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
|
|
||||||
}
|
|
||||||
232
pkg/dashboard/objects/data.go
Normal file
232
pkg/dashboard/objects/data.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"io"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataLayer struct {
|
||||||
|
KubeContext string
|
||||||
|
Scanners []subproc.Scanner
|
||||||
|
StatusInfo *StatusInfo
|
||||||
|
Namespaces []string
|
||||||
|
Cache *Cache
|
||||||
|
|
||||||
|
ConfGen HelmConfigGetter
|
||||||
|
appPerContext map[string]*Application
|
||||||
|
appPerContextMx *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusInfo struct {
|
||||||
|
CurVer string
|
||||||
|
LatestVer string
|
||||||
|
Analytics bool
|
||||||
|
CacheHitRatio float64
|
||||||
|
ClusterMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDataLayer(ns []string, ver string, cg HelmConfigGetter) (*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),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) ListContexts() ([]KubeContext, error) {
|
||||||
|
res := []KubeContext{}
|
||||||
|
|
||||||
|
if d.StatusInfo.ClusterMode {
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := clientcmd.NewDefaultPathOptions().GetStartingConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to get kubectl config")
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, ctx := range cfg.Contexts {
|
||||||
|
res = append(res, KubeContext{
|
||||||
|
IsCurrent: cfg.CurrentContext == name,
|
||||||
|
Name: name,
|
||||||
|
Cluster: ctx.Cluster,
|
||||||
|
AuthInfo: ctx.AuthInfo,
|
||||||
|
Namespace: ctx.Namespace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) GetStatus() *StatusInfo {
|
||||||
|
sum := float64(d.Cache.HitCount + d.Cache.MissCount)
|
||||||
|
if sum > 0 {
|
||||||
|
d.StatusInfo.CacheHitRatio = float64(d.Cache.HitCount) / sum
|
||||||
|
} else {
|
||||||
|
d.StatusInfo.CacheHitRatio = 0
|
||||||
|
}
|
||||||
|
return d.StatusInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type SectionFn = func(*release.Release, bool) (string, error)
|
||||||
|
|
||||||
|
func ParseManifests(out string) ([]*v1.Carp, error) {
|
||||||
|
dec := yaml.NewDecoder(bytes.NewReader([]byte(out)))
|
||||||
|
|
||||||
|
res := make([]*v1.Carp, 0)
|
||||||
|
var tmp interface{}
|
||||||
|
for {
|
||||||
|
err := dec.Decode(&tmp)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to parse manifest document #%d", len(res)+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// k8s libs uses only JSON tags defined, say hello to https://github.com/go-yaml/yaml/issues/424
|
||||||
|
// we can juggle it
|
||||||
|
jsoned, err := json.Marshal(tmp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var doc v1.Carp
|
||||||
|
err = json.Unmarshal(jsoned, &doc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if doc.Kind == "" {
|
||||||
|
log.Warnf("Manifest piece is not k8s resource: %s", jsoned)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, &doc)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) SetContext(ctx string) error {
|
||||||
|
if d.KubeContext != ctx {
|
||||||
|
err := d.Cache.Clear()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to set context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.KubeContext = ctx
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DataLayer) AppForCtx(ctx string) (*Application, error) {
|
||||||
|
d.appPerContextMx.Lock()
|
||||||
|
defer d.appPerContextMx.Unlock()
|
||||||
|
|
||||||
|
app, ok := d.appPerContext[ctx]
|
||||||
|
if !ok {
|
||||||
|
settings := cli.New()
|
||||||
|
settings.KubeContext = ctx
|
||||||
|
|
||||||
|
settings.SetNamespace(d.nsForCtx(ctx))
|
||||||
|
|
||||||
|
cfgGetter := func(ns string) (*action.Configuration, error) {
|
||||||
|
return d.ConfGen(settings, ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err := NewApplication(settings, cfgGetter, d.Namespaces)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "Failed to create application for context '%s'", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if !d.StatusInfo.ClusterMode { // TODO: maybe have a separate flag for that?
|
||||||
|
log.Debugf("Not in cluster mode, not starting background tasks")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Orig.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("Update repo loop done.")
|
||||||
|
}
|
||||||
58
pkg/dashboard/objects/data_test.go
Normal file
58
pkg/dashboard/objects/data_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
|
errorExpected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "should return error when helm config is nil",
|
||||||
|
namespaces: []string{"namespace1", "namespace2"},
|
||||||
|
version: "1.0.0",
|
||||||
|
helmConfig: nil,
|
||||||
|
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
|
||||||
|
},
|
||||||
|
errorExpected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dl, err := NewDataLayer(tt.namespaces, tt.version, tt.helmConfig)
|
||||||
|
if tt.errorExpected {
|
||||||
|
assert.Error(t, err, "Expected error but got nil")
|
||||||
|
} else {
|
||||||
|
assert.Nil(t, err, "NewDataLayer returned an error: %v", err)
|
||||||
|
assert.NotNil(t, dl, "NewDataLayer returned nil")
|
||||||
|
assert.Equal(t, tt.namespaces, dl.Namespaces, "NewDataLayer returned incorrect namespaces: %v", dl.Namespaces)
|
||||||
|
assert.NotNil(t, dl.Cache, "NewDataLayer returned nil cache")
|
||||||
|
assert.Equal(t, tt.version, dl.StatusInfo.CurVer, "NewDataLayer returned incorrect version: %v", dl.StatusInfo.CurVer)
|
||||||
|
assert.False(t, dl.StatusInfo.Analytics, "NewDataLayer returned incorrect version: %v", dl.StatusInfo.CurVer)
|
||||||
|
assert.NotNil(t, dl.appPerContext, "NewDataLayer returned nil appPerContext")
|
||||||
|
assert.NotNil(t, dl.ConfGen, "NewDataLayer returned nil ConfGen")
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
198
pkg/dashboard/objects/kubectl.go
Normal file
198
pkg/dashboard/objects/kubectl.go
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/joomcode/errorx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"helm.sh/helm/v3/pkg/action"
|
||||||
|
"helm.sh/helm/v3/pkg/kube"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
testapiv1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
"k8s.io/client-go/discovery"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
describecmd "k8s.io/kubectl/pkg/cmd/describe"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/describe"
|
||||||
|
"k8s.io/utils/strings/slices"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KubeContext struct {
|
||||||
|
IsCurrent bool
|
||||||
|
Name string
|
||||||
|
Cluster string
|
||||||
|
AuthInfo string
|
||||||
|
Namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps action.RESTClientGetter into genericclioptions.RESTClientGetter
|
||||||
|
type cfgProxyObject struct {
|
||||||
|
Impl action.RESTClientGetter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cfgProxyObject) ToRESTConfig() (*rest.Config, error) {
|
||||||
|
return p.Impl.ToRESTConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cfgProxyObject) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
|
||||||
|
return p.Impl.ToDiscoveryClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cfgProxyObject) ToRESTMapper() (meta.RESTMapper, error) {
|
||||||
|
return p.Impl.ToRESTMapper()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *cfgProxyObject) ToRawKubeConfigLoader() clientcmd.ClientConfig {
|
||||||
|
panic("Not implemented, stub")
|
||||||
|
}
|
||||||
|
|
||||||
|
type K8s struct {
|
||||||
|
Namespaces []string
|
||||||
|
Factory kube.Factory
|
||||||
|
RestClientGetter genericclioptions.RESTClientGetter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewK8s(helmConfig *action.Configuration, namespaces []string) (*K8s, error) {
|
||||||
|
factory := cmdutil.NewFactory(&cfgProxyObject{Impl: helmConfig.RESTClientGetter})
|
||||||
|
|
||||||
|
return &K8s{
|
||||||
|
Namespaces: namespaces,
|
||||||
|
Factory: factory,
|
||||||
|
RestClientGetter: factory,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *K8s) GetNameSpaces() (res *corev1.NamespaceList, err error) {
|
||||||
|
clientset, err := k.Factory.KubernetesClientSet()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get KubernetesClientSet")
|
||||||
|
}
|
||||||
|
|
||||||
|
lst, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get list of namespaces")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.Contains(k.Namespaces, "") {
|
||||||
|
filtered := []corev1.Namespace{}
|
||||||
|
for _, ns := range lst.Items {
|
||||||
|
if slices.Contains(k.Namespaces, ns.Name) {
|
||||||
|
filtered = append(filtered, ns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lst.Items = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
return lst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *K8s) DescribeResource(kind string, ns string, name string) (string, error) {
|
||||||
|
log.Debugf("Describing resource: %s %s in %s", kind, name, ns)
|
||||||
|
streams, _, out, errout := genericclioptions.NewTestIOStreams()
|
||||||
|
o := &describecmd.DescribeOptions{
|
||||||
|
Describer: func(mapping *meta.RESTMapping) (describe.ResourceDescriber, error) {
|
||||||
|
return describe.DescriberFn(k.RestClientGetter, mapping)
|
||||||
|
},
|
||||||
|
FilenameOptions: &resource.FilenameOptions{},
|
||||||
|
DescriberSettings: &describe.DescriberSettings{
|
||||||
|
ShowEvents: true,
|
||||||
|
ChunkSize: cmdutil.DefaultChunkSize,
|
||||||
|
},
|
||||||
|
|
||||||
|
IOStreams: streams,
|
||||||
|
|
||||||
|
NewBuilder: k.Factory.NewBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Namespace = ns
|
||||||
|
o.BuilderArgs = []string{kind, name}
|
||||||
|
|
||||||
|
err := o.Run()
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "Failed to run describe command: %s", errout.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *K8s) GetResource(kind string, namespace string, name string) (*runtime.Object, error) {
|
||||||
|
builder := k.Factory.NewBuilder()
|
||||||
|
resp := builder.Unstructured().NamespaceParam(namespace).Flatten().ResourceNames(kind, name).Do()
|
||||||
|
if resp.Err() != nil {
|
||||||
|
return nil, errorx.Decorate(resp.Err(), "failed to get k8s resource")
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := resp.Object()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to get k8s resulting object")
|
||||||
|
}
|
||||||
|
return &obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *K8s) GetResourceInfo(kind string, namespace string, name string) (*testapiv1.Carp, error) {
|
||||||
|
obj, err := k.GetResource(kind, namespace, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to get k8s object")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to marshal k8s object into JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := new(testapiv1.Carp)
|
||||||
|
err = json.Unmarshal(data, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to decode k8s object from JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(res.Status.Conditions, func(i, j int) bool {
|
||||||
|
// some condition types always bubble up
|
||||||
|
if res.Status.Conditions[i].Type == "Available" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Status.Conditions[j].Type == "Available" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
t1 := res.Status.Conditions[i].LastTransitionTime
|
||||||
|
t2 := res.Status.Conditions[j].LastTransitionTime
|
||||||
|
return t1.Time.Before(t2.Time)
|
||||||
|
})
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *K8s) GetResourceYAML(kind string, namespace string, name string) (string, error) {
|
||||||
|
obj, err := k.GetResource(kind, namespace, name)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to get k8s object")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to marshal k8s object into JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := map[string]interface{}{}
|
||||||
|
err = json.Unmarshal(data, &res)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to decode k8s object from JSON")
|
||||||
|
}
|
||||||
|
|
||||||
|
ydata, err := yaml.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to marshal k8s object into JSON")
|
||||||
|
}
|
||||||
|
return string(ydata), nil
|
||||||
|
}
|
||||||
399
pkg/dashboard/objects/releases.go
Normal file
399
pkg/dashboard/objects/releases.go
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
chrt, err := locateChart(cmd.ChartPathOptions, repoChart, r.Settings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := cmd.Run(r.Orig.Name, chrt, values)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !justTemplate {
|
||||||
|
log.Infof("Upgraded release: %s/%s#%d", res.Namespace, res.Name, res.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Release) restoreChart() (string, error) {
|
||||||
|
if r.restoredChartPath != "" {
|
||||||
|
return r.restoredChartPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're unlikely to have the original chart, let's try the cheesy thing...
|
||||||
|
|
||||||
|
log.Infof("Attempting to restore the chart for %s", r.Orig.Name)
|
||||||
|
dir, err := ioutil.TempDir("", "khd-*")
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to get temporary directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
//restore Chart.yaml
|
||||||
|
cdata, err := yaml.Marshal(r.Orig.Chart.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to restore Chart.yaml")
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(path.Join(dir, "Chart.yaml"), cdata, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to write file Chart.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
//restore known values
|
||||||
|
vdata, err := yaml.Marshal(r.Orig.Chart.Values)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to restore values.yaml")
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(path.Join(dir, "values.yaml"), vdata, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to write file values.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if possible, overwrite files with better alternatives
|
||||||
|
for _, f := range append(r.Orig.Chart.Raw, r.Orig.Chart.Templates...) {
|
||||||
|
fname := path.Join(dir, f.Name)
|
||||||
|
log.Debugf("Restoring file: %s", fname)
|
||||||
|
err := os.MkdirAll(path.Dir(fname), 0755)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to create directory for file: %s", fname)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(fname, f.Data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return "", errorx.Decorate(err, "failed to write file to restore chart: %s", fname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.restoredChartPath = dir
|
||||||
|
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkIfInstallable(ch *chart.Chart) error {
|
||||||
|
switch ch.Metadata.Type {
|
||||||
|
case "", "application":
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.Errorf("%s charts are not installable", ch.Metadata.Type)
|
||||||
|
}
|
||||||
80
pkg/dashboard/objects/releases_test.go
Normal file
80
pkg/dashboard/objects/releases_test.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
"helm.sh/helm/v3/pkg/action"
|
||||||
|
kubefake "helm.sh/helm/v3/pkg/kube/fake"
|
||||||
|
"helm.sh/helm/v3/pkg/release"
|
||||||
|
"helm.sh/helm/v3/pkg/storage"
|
||||||
|
"helm.sh/helm/v3/pkg/storage/driver"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fakeKubeClient *kubefake.PrintingKubeClient
|
||||||
|
fakeStorage *storage.Storage
|
||||||
|
)
|
||||||
|
|
||||||
|
func fakeHelmNSConfigGetter(ns string) (*action.Configuration, error) {
|
||||||
|
return &action.Configuration{
|
||||||
|
KubeClient: fakeKubeClient,
|
||||||
|
Releases: fakeStorage,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListReleases(t *testing.T) {
|
||||||
|
fakeStorage = storage.Init(driver.NewMemory())
|
||||||
|
err := fakeStorage.Create(&release.Release{
|
||||||
|
Name: "release1",
|
||||||
|
Info: &release.Info{
|
||||||
|
Status: release.StatusDeployed,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
err = fakeStorage.Create(&release.Release{
|
||||||
|
Name: "release2",
|
||||||
|
Info: &release.Info{
|
||||||
|
Status: release.StatusDeployed,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
err = fakeStorage.Create(&release.Release{
|
||||||
|
Name: "release3",
|
||||||
|
Info: &release.Info{
|
||||||
|
Status: release.StatusDeployed,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
err = fakeStorage.Create(&release.Release{
|
||||||
|
Name: "release4",
|
||||||
|
Info: &release.Info{
|
||||||
|
Status: release.StatusDeployed,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
err = fakeStorage.Create(&release.Release{
|
||||||
|
Name: "release5",
|
||||||
|
Info: &release.Info{
|
||||||
|
Status: release.StatusDeployed,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
releases := &Releases{
|
||||||
|
Namespaces: []string{"testNamespace"},
|
||||||
|
HelmConfig: fakeHelmNSConfigGetter,
|
||||||
|
mx: sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := releases.List()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, len(res), 5)
|
||||||
|
assert.Equal(t, res[0].Orig.Name, "release1")
|
||||||
|
assert.Equal(t, res[1].Orig.Name, "release2")
|
||||||
|
assert.Equal(t, res[2].Orig.Name, "release3")
|
||||||
|
assert.Equal(t, res[3].Orig.Name, "release4")
|
||||||
|
assert.Equal(t, res[4].Orig.Name, "release5")
|
||||||
|
}
|
||||||
312
pkg/dashboard/objects/repos.go
Normal file
312
pkg/dashboard/objects/repos.go
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/getter"
|
||||||
|
"helm.sh/helm/v3/pkg/helmpath"
|
||||||
|
"helm.sh/helm/v3/pkg/repo"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const AnnRepo = "helm-dashboard/repository-name"
|
||||||
|
|
||||||
|
type Repositories struct {
|
||||||
|
Settings *cli.EnvSettings
|
||||||
|
HelmConfig *action.Configuration
|
||||||
|
mx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
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, &Repository{
|
||||||
|
Settings: r.Settings,
|
||||||
|
Orig: item,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repositories) Add(name string, url string) error {
|
||||||
|
if name == "" || url == "" {
|
||||||
|
return errors.New("Name and URL are required parameters to add the repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied from cmd/helm/repo_add.go
|
||||||
|
repoFile := r.Settings.RepositoryConfig
|
||||||
|
|
||||||
|
// Ensure the file directory exists as it is required for file locking
|
||||||
|
err := os.MkdirAll(filepath.Dir(repoFile), os.ModePerm)
|
||||||
|
if err != nil && !os.IsExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := r.Load()
|
||||||
|
if err != nil {
|
||||||
|
return errorx.Decorate(err, "Failed to load repo config")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mx.Lock()
|
||||||
|
defer r.mx.Unlock()
|
||||||
|
|
||||||
|
c := repo.Entry{
|
||||||
|
Name: name,
|
||||||
|
URL: url,
|
||||||
|
//Username: o.username,
|
||||||
|
//Password: o.password,
|
||||||
|
//PassCredentialsAll: o.passCredentialsAll,
|
||||||
|
//CertFile: o.certFile,
|
||||||
|
//KeyFile: o.keyFile,
|
||||||
|
//CAFile: o.caFile,
|
||||||
|
//InsecureSkipTLSverify: o.insecureSkipTLSverify,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the repo name is legal
|
||||||
|
if strings.Contains(c.Name, "/") {
|
||||||
|
return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
rep, err := repo.NewChartRepository(&c, getter.All(r.Settings))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := rep.DownloadIndexFile(); err != nil {
|
||||||
|
return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Update(&c)
|
||||||
|
|
||||||
|
if err := f.WriteFile(repoFile, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repositories) Delete(name string) error {
|
||||||
|
f, err := r.Load()
|
||||||
|
if err != nil {
|
||||||
|
return errorx.Decorate(err, "failed to load repo information")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mx.Lock()
|
||||||
|
defer r.mx.Unlock()
|
||||||
|
|
||||||
|
// copied from cmd/helm/repo_remove.go
|
||||||
|
if !f.Remove(name) {
|
||||||
|
return errors.Errorf("no repo named %q found", name)
|
||||||
|
}
|
||||||
|
if err := f.WriteFile(r.Settings.RepositoryConfig, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := removeRepoCache(r.Settings.RepositoryCache, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repositories) Get(name string) (*Repository, error) {
|
||||||
|
f, err := r.Load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to load repo information")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range f.Repositories {
|
||||||
|
if entry.Name == name {
|
||||||
|
return &Repository{
|
||||||
|
Settings: r.Settings,
|
||||||
|
Orig: entry,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errorx.DataUnavailable.New("Could not find reposiroty '%s'", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Orig.Name)
|
||||||
|
log.Debugf("The error was: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Orig.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, vers...) // TODO filter dev versions here, relates to #139
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repositories) GetChart(chart string, ver string) (*chart.Chart, error) {
|
||||||
|
// TODO: unused method?
|
||||||
|
client := action.NewShowWithConfig(action.ShowAll, r.HelmConfig)
|
||||||
|
client.Version = ver
|
||||||
|
|
||||||
|
cp, err := client.ChartPathOptions.LocateChart(chart, r.Settings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to locate chart '%s'", chart)
|
||||||
|
}
|
||||||
|
|
||||||
|
chrt, err := loader.Load(cp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to load chart from '%s'", cp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chrt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repositories) GetChartValues(chart string, ver string) (string, error) {
|
||||||
|
// 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 struct {
|
||||||
|
Settings *cli.EnvSettings
|
||||||
|
Orig *repo.Entry
|
||||||
|
mx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) indexFileName() string {
|
||||||
|
return filepath.Join(r.Settings.RepositoryCache, helmpath.CacheIndexFile(r.Orig.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) 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 *Repository) Charts() ([]*repo.ChartVersion, error) {
|
||||||
|
ind, err := r.getIndex()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to get repo index")
|
||||||
|
}
|
||||||
|
|
||||||
|
res := []*repo.ChartVersion{}
|
||||||
|
for _, v := range ind.Entries {
|
||||||
|
if len(v) > 0 { // TODO filter dev versions here, relates to #139
|
||||||
|
res = append(res, v[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) 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 *Repository) 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)
|
||||||
|
}
|
||||||
149
pkg/dashboard/objects/repos_test.go
Normal file
149
pkg/dashboard/objects/repos_test.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package objects
|
||||||
|
|
||||||
|
import (
|
||||||
|
"helm.sh/helm/v3/pkg/action"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
"helm.sh/helm/v3/pkg/cli"
|
||||||
|
"helm.sh/helm/v3/pkg/repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
var filePath = "./testdata/repositories.yaml"
|
||||||
|
|
||||||
|
func initRepository(t *testing.T, filePath string) *Repositories {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
settings := cli.New()
|
||||||
|
|
||||||
|
// Sets the repository file path
|
||||||
|
settings.RepositoryConfig = filePath
|
||||||
|
|
||||||
|
testRepository := &Repositories{
|
||||||
|
Settings: settings,
|
||||||
|
HelmConfig: &action.Configuration{}, // maybe use copy of getFakeHelmConfig from api_test.go
|
||||||
|
}
|
||||||
|
|
||||||
|
return testRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadRepo(t *testing.T) {
|
||||||
|
|
||||||
|
res, err := repo.LoadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testRepository := initRepository(t, filePath)
|
||||||
|
|
||||||
|
file, err := testRepository.Load()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, file.Generated, res.Generated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
res, err := repo.LoadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testRepository := initRepository(t, filePath)
|
||||||
|
|
||||||
|
repos, err := testRepository.List()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, len(repos), len(res.Repositories))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdd(t *testing.T) {
|
||||||
|
testRepoName := "TEST"
|
||||||
|
testRepoUrl := "https://helm.github.io/examples"
|
||||||
|
|
||||||
|
res, err := repo.LoadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the repository if already exist
|
||||||
|
res.Remove(testRepoName)
|
||||||
|
|
||||||
|
testRepository := initRepository(t, filePath)
|
||||||
|
|
||||||
|
err = testRepository.Add(testRepoName, testRepoUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err, "Failed to add repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the file
|
||||||
|
res, err = repo.LoadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, res.Has(testRepoName), true)
|
||||||
|
|
||||||
|
// Removes test repository which is added for testing
|
||||||
|
t.Cleanup(func() {
|
||||||
|
removed := res.Remove(testRepoName)
|
||||||
|
if removed != true {
|
||||||
|
t.Log("Failed to clean the test repository file")
|
||||||
|
}
|
||||||
|
err = res.WriteFile(filePath, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Log("Failed to write the file while cleaning test repo")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete(t *testing.T) {
|
||||||
|
testRepoName := "TEST DELETE"
|
||||||
|
testRepoUrl := "https://helm.github.io/examples"
|
||||||
|
|
||||||
|
res, err := repo.LoadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a test entry
|
||||||
|
res.Add(&repo.Entry{Name: testRepoName, URL: testRepoUrl})
|
||||||
|
err = res.WriteFile(filePath, 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to write the file while creating test repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
testRepository := initRepository(t, filePath)
|
||||||
|
|
||||||
|
err = testRepository.Delete(testRepoName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err, "Failed to delete the repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the file
|
||||||
|
res, err = repo.LoadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, res.Has(testRepoName), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
// Initial repositiry name in test file
|
||||||
|
repoName := "charts"
|
||||||
|
|
||||||
|
testRepository := initRepository(t, filePath)
|
||||||
|
|
||||||
|
repo, err := testRepository.Get(repoName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err, "Failed to get th repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, repo.Orig.Name, repoName)
|
||||||
|
}
|
||||||
30
pkg/dashboard/objects/testdata/repositories.yaml
vendored
Normal file
30
pkg/dashboard/objects/testdata/repositories.yaml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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: ""
|
||||||
@@ -2,6 +2,8 @@ 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"
|
||||||
@@ -11,7 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Checkov struct {
|
type Checkov struct {
|
||||||
Data *subproc.DataLayer
|
Data *objects.DataLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Checkov) ManifestIsScannable() bool {
|
func (c *Checkov) ManifestIsScannable() bool {
|
||||||
@@ -77,7 +79,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
|
||||||
}
|
}
|
||||||
@@ -89,14 +91,19 @@ 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
|
||||||
mnf, err := c.Data.GetResourceYAML(ns, &carp)
|
app, err := c.Data.AppForCtx(c.Data.KubeContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errorx.Decorate(err, "failed to get app for context")
|
||||||
|
}
|
||||||
|
|
||||||
|
mnf, err := app.K8s.GetResourceYAML(kind, ns, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.Decorate(err, "failed to get YAML for resource")
|
||||||
}
|
}
|
||||||
|
|
||||||
fname, fclose, err := utils.TempFile(mnf)
|
fname, fclose, err := utils.TempFile(mnf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errorx.Decorate(err, "failed to create temporary file")
|
||||||
}
|
}
|
||||||
defer fclose()
|
defer fclose()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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"
|
||||||
@@ -9,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Trivy struct {
|
type Trivy struct {
|
||||||
Data *subproc.DataLayer
|
Data *objects.DataLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Trivy) ManifestIsScannable() bool {
|
func (c *Trivy) ManifestIsScannable() bool {
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"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"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,54 +18,87 @@ import (
|
|||||||
"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
|
||||||
Namespace string
|
Namespaces []string
|
||||||
Address string
|
Address string
|
||||||
Debug bool
|
Debug bool
|
||||||
NoTracking bool
|
NoTracking bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Server) StartServer() (string, utils.ControlChan) {
|
func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (string, utils.ControlChan, error) {
|
||||||
data := subproc.DataLayer{
|
data, err := objects.NewDataLayer(s.Namespaces, s.Version, NewHelmConfig)
|
||||||
Namespace: s.Namespace,
|
|
||||||
Cache: subproc.NewCache(),
|
|
||||||
StatusInfo: &subproc.StatusInfo{
|
|
||||||
CurVer: s.Version,
|
|
||||||
Analytics: false,
|
|
||||||
LimitedToNamespace: s.Namespace,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := data.CheckConnectivity()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to check that Helm is operational, cannot continue. The error was: %s", err)
|
return "", nil, errorx.Decorate(err, "Failed to create data layer")
|
||||||
os.Exit(1) // TODO: propagate error instead?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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, err
|
||||||
|
}
|
||||||
|
|
||||||
go checkUpgrade(data.StatusInfo)
|
go checkUpgrade(data.StatusInfo)
|
||||||
|
|
||||||
discoverScanners(&data)
|
discoverScanners(data)
|
||||||
|
|
||||||
abort := make(utils.ControlChan)
|
go data.PeriodicTasks(ctx)
|
||||||
api := NewRouter(abort, &data, s.Debug)
|
|
||||||
done := s.startBackgroundServer(api, abort)
|
|
||||||
|
|
||||||
return "http://" + s.Address, done
|
api := NewRouter(cancel, data, s.Debug)
|
||||||
|
done := s.startBackgroundServer(api, ctx)
|
||||||
|
|
||||||
|
return "http://" + s.Address, done, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Server) startBackgroundServer(routes *gin.Engine, abort utils.ControlChan) utils.ControlChan {
|
func (s *Server) detectClusterMode(data *objects.DataLayer) error {
|
||||||
|
data.StatusInfo.ClusterMode = os.Getenv("HD_CLUSTER_MODE") != ""
|
||||||
|
if data.StatusInfo.ClusterMode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxs, err := data.ListContexts()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ctxs) == 0 {
|
||||||
|
log.Infof("Got no kubectl config contexts, will attempt to detect if we're inside cluster...")
|
||||||
|
app, err := data.AppForCtx("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ns, err := app.K8s.GetNameSpaces()
|
||||||
|
if err != nil { // no point in continuing without kubectl context and k8s connection
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf("Got %d namespaces listed", len(ns.Items))
|
||||||
|
data.StatusInfo.ClusterMode = true
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) startBackgroundServer(routes *gin.Engine, ctx context.Context) utils.ControlChan {
|
||||||
done := make(utils.ControlChan)
|
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 {
|
||||||
@@ -73,18 +112,10 @@ func (s Server) startBackgroundServer(routes *gin.Engine, abort utils.ControlCha
|
|||||||
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,
|
||||||
@@ -99,7 +130,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 *subproc.DataLayer) {
|
func discoverScanners(data *objects.DataLayer) {
|
||||||
potential := []subproc.Scanner{
|
potential := []subproc.Scanner{
|
||||||
&scanners.Checkov{Data: data},
|
&scanners.Checkov{Data: data},
|
||||||
&scanners.Trivy{Data: data},
|
&scanners.Trivy{Data: data},
|
||||||
@@ -113,7 +144,7 @@ func discoverScanners(data *subproc.DataLayer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkUpgrade(d *subproc.StatusInfo) { // TODO: check it once an hour
|
func checkUpgrade(d *objects.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"`
|
||||||
@@ -143,7 +174,7 @@ func checkUpgrade(d *subproc.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 LatestVer: %s", err)
|
log.Warnf("Failed to parse RepoLatestVer: %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)
|
||||||
@@ -153,3 +184,34 @@ func checkUpgrade(d *subproc.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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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/repo/update?name=" + repoName).fail(function (xhr) {
|
$.post("/api/helm/repositories/" + 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,31 +16,29 @@ $("#btnUpgradeCheck").click(function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function checkUpgradeable(name) {
|
function checkUpgradeable(name) {
|
||||||
$.getJSON("/api/helm/repo/search?name=" + name).fail(function (xhr) {
|
$.getJSON("/api/helm/repositories/latestver?name=" + name).fail(function (xhr) {
|
||||||
reportError("Failed to find chart in repo", xhr)
|
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) {
|
||||||
$("#btnUpgrade span").text("No upgrades")
|
btnUpgradeCheck.prop("disabled", true)
|
||||||
$("#btnUpgrade .icon").removeClass("bi-hourglass-split").addClass("bi-x-octagon")
|
btnUpgradeCheck.text("")
|
||||||
$("#btnUpgrade").prop("disabled", true)
|
|
||||||
$("#btnUpgradeCheck").prop("disabled", true)
|
|
||||||
$("#btnAddRepository").text("Add repository for it")
|
$("#btnAddRepository").text("Add repository for it")
|
||||||
$("#btnUpgradeCheck").text("")
|
} else {
|
||||||
return
|
$("#btnAddRepository").text("")
|
||||||
|
btnUpgradeCheck.text("Check for new version")
|
||||||
|
elm = data[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#btnUpgrade .icon").removeClass("bi-x-octagon").addClass("bi-hourglass-split")
|
$("#btnUpgrade .icon").removeClass("bi-arrow-up bi-pencil").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");
|
||||||
const elm = data[0]
|
btnUpgradeCheck.data("repo", elm.repository)
|
||||||
$("#btnUpgradeCheck").data("repo", elm.name.split('/').shift())
|
btnUpgradeCheck.data("chart", elm.name)
|
||||||
$("#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")
|
||||||
@@ -58,7 +56,14 @@ 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("chart", elm.name).data("initial", !verCur)
|
let chart = elm.repository + "/" + elm.name;
|
||||||
|
if (!elm.name) {
|
||||||
|
chart = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#upgradeModal').data("chart", chart).data("initial", !verCur)
|
||||||
|
$('#upgradeModal form .chart-name').val(chart)
|
||||||
|
$('#upgradeModal').data("newManifest", "")
|
||||||
|
|
||||||
$("#upgradeModalLabel .name").text(elm.name)
|
$("#upgradeModalLabel .name").text(elm.name)
|
||||||
|
|
||||||
@@ -69,53 +74,70 @@ 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", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
$.getJSON("/api/helm/repo/search?name=" + elm.name).fail(function (xhr) {
|
if (elm.name) {
|
||||||
reportError("Failed to find chart in repo", xhr)
|
$.getJSON("/api/helm/repositories/versions?name=" + elm.name).fail(function (xhr) {
|
||||||
}).done(function (vers) {
|
reportError("Failed to find chart in repo", xhr)
|
||||||
// fill versions
|
}).done(function (vers) {
|
||||||
$('#upgradeModal select').empty()
|
// fill versions
|
||||||
for (let i = 0; i < vers.length; i++) {
|
$('#upgradeModal select').empty()
|
||||||
const opt = $("<option value='" + vers[i].version + "'></option>");
|
for (let i = 0; i < vers.length; i++) {
|
||||||
if (vers[i].version === verCur) {
|
const opt = $("<option value='" + vers[i].version + "'></option>");
|
||||||
opt.html(vers[i].version + " ·")
|
if (vers[i].version === verCur) {
|
||||||
} else {
|
opt.html(vers[i].version + " ·")
|
||||||
opt.html(vers[i].version)
|
} else {
|
||||||
|
opt.html(vers[i].version)
|
||||||
|
}
|
||||||
|
$('#upgradeModal select').append(opt)
|
||||||
}
|
}
|
||||||
$('#upgradeModal select').append(opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#upgradeModal select').val(elm.version).trigger("change")
|
$('#upgradeModal select').val(elm.version).trigger("change").parent().show()
|
||||||
|
upgrPopUpCommon(verCur, ns, lastRev, name)
|
||||||
|
})
|
||||||
|
} else { // chart without repo reconfigure
|
||||||
|
$('#upgradeModal select').empty().trigger("change").parent().hide()
|
||||||
|
upgrPopUpCommon(verCur, ns, lastRev, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
|
function upgrPopUpCommon(verCur, ns, lastRev, name) {
|
||||||
myModal.show()
|
const myModal = new bootstrap.Modal(document.getElementById('upgradeModal'), {});
|
||||||
|
myModal.show()
|
||||||
|
|
||||||
if (verCur) {
|
if (verCur) {
|
||||||
// fill current values
|
// fill current values
|
||||||
$.get("/api/helm/charts/values?namespace=" + ns + "&revision=" + lastRev + "&name=" + name + "&flag=true").fail(function (xhr) {
|
$.get("/api/helm/releases/" + ns + "/" + name + "/values?userDefined=true&revision=" + lastRev).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)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
$("#upgradeModal textarea").val("").data("dirty", true)
|
$("#upgradeModal textarea").val("").data("dirty", true)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#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: "/api/helm/charts/install" + upgradeModalQstr() + "&flag=true",
|
url: upgradeModalURL(),
|
||||||
data: $("#upgradeModal textarea").data("dirty") ? $("#upgradeModal form").serialize() : null,
|
data: $("#upgradeModal form").serialize(),
|
||||||
}).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) {
|
||||||
@@ -156,22 +178,33 @@ $('#upgradeModal select').change(function () {
|
|||||||
|
|
||||||
// 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) {
|
const chart = $("#upgradeModal").data("chart");
|
||||||
reportError("Failed to get upgrade info", xhr)
|
// TODO: if chart is empty, query different URL that will restore values without repo
|
||||||
}).done(function (data) {
|
if (chart) {
|
||||||
data = hljs.highlight(data, {language: 'yaml'}).value
|
$.get("/api/helm/repositories/values?chart=" + chart + "&version=" + self.val()).fail(function (xhr) {
|
||||||
$("#upgradeModal .ref-vals").html(data)
|
reportError("Failed to get upgrade info", xhr)
|
||||||
})
|
}).done(function (data) {
|
||||||
|
data = hljs.highlight(data, {language: 'yaml'}).value
|
||||||
|
$("#upgradeModal .ref-vals").html(data)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
$("#upgradeModal .ref-vals").html("No original values information found")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$('#upgradeModal .btn-scan').click(function () {
|
$('#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" + upgradeModalQstr(),
|
url: "/api/scanners/manifests",
|
||||||
data: $("#upgradeModal form").serialize(),
|
processData: false,
|
||||||
|
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) {
|
||||||
@@ -185,7 +218,7 @@ $('#upgradeModal .btn-scan').click(function () {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const pre = $("<pre></pre>").text(res.OrigReport)
|
const pre = $("<pre></pre>").text(JSON.stringify(res.OrigReport, null, 2))
|
||||||
|
|
||||||
container.append("<h2>" + name + " Scan Results</h2>")
|
container.append("<h2>" + name + " Scan Results</h2>")
|
||||||
container.append(pre)
|
container.append(pre)
|
||||||
@@ -203,10 +236,10 @@ function requestChangeDiff() {
|
|||||||
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)
|
||||||
|
|
||||||
let values = null;
|
$('#upgradeModal form .preview-mode').val("true")
|
||||||
|
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())
|
||||||
@@ -219,36 +252,52 @@ function requestChangeDiff() {
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
url: "/api/helm/charts/install" + upgradeModalQstr(),
|
url: upgradeModalURL(),
|
||||||
data: values,
|
data: form,
|
||||||
}).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) {
|
||||||
diffBody.empty();
|
$('#upgradeModal').data("newManifest", data.manifest)
|
||||||
$("#upgradeModal .btn-confirm").prop("disabled", false)
|
|
||||||
|
|
||||||
const targetElement = document.getElementById('upgradeModalBody');
|
const form = new FormData();
|
||||||
const configuration = {
|
form.append('a', $('#upgradeModal').data("curManifest"));
|
||||||
inputFormat: 'diff', outputFormat: 'side-by-side',
|
form.append('b', data.manifest);
|
||||||
drawFileList: false, showFiles: false, highlight: true,
|
|
||||||
};
|
$.ajax({
|
||||||
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
type: "POST",
|
||||||
diff2htmlUi.draw()
|
url: "/diff",
|
||||||
if (!data) {
|
processData: false,
|
||||||
diffBody.html("No changes will happen to the cluster")
|
contentType: false,
|
||||||
}
|
data: form,
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
$("#upgradeModalBody").html("<p class='text-danger'>Failed to get upgrade info: " + xhr.responseText + "</p>")
|
||||||
|
}).done(function (data) {
|
||||||
|
diffBody.empty();
|
||||||
|
$("#upgradeModal .btn-confirm").prop("disabled", false)
|
||||||
|
|
||||||
|
const targetElement = document.getElementById('upgradeModalBody');
|
||||||
|
const configuration = {
|
||||||
|
inputFormat: 'diff', outputFormat: 'side-by-side',
|
||||||
|
drawFileList: false, showFiles: false, highlight: true,
|
||||||
|
};
|
||||||
|
const diff2htmlUi = new Diff2HtmlUI(targetElement, data, configuration);
|
||||||
|
diff2htmlUi.draw()
|
||||||
|
if (!data) {
|
||||||
|
diffBody.html("No changes will happen to the cluster")
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function upgradeModalQstr() {
|
function upgradeModalURL() {
|
||||||
let qstr = "?" +
|
let ns = $("#upgradeModal .rel-ns").val();
|
||||||
"namespace=" + $("#upgradeModal .rel-ns").val() +
|
if (!ns) {
|
||||||
"&name=" + $("#upgradeModal .rel-name").val() +
|
ns = "[empty]"
|
||||||
"&chart=" + $("#upgradeModal").data("chart") +
|
}
|
||||||
"&version=" + $('#upgradeModal select').val()
|
|
||||||
|
|
||||||
if ($("#upgradeModal").data("initial")) {
|
let qstr = "/api/helm/releases/" + ns;
|
||||||
qstr += "&initial=true"
|
if (!$("#upgradeModal").data("initial")) {
|
||||||
|
qstr += "/" + $("#upgradeModal .rel-name").val()
|
||||||
}
|
}
|
||||||
|
|
||||||
return qstr
|
return qstr
|
||||||
@@ -263,7 +312,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/charts?namespace=" + namespace + "&name=" + chart;
|
const url = "/api/helm/releases/" + namespace + "/" + chart;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: 'DELETE',
|
type: 'DELETE',
|
||||||
@@ -277,9 +326,7 @@ $("#btnUninstall").click(function () {
|
|||||||
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'));
|
const myModal = new bootstrap.Modal(document.getElementById('confirmModal'));
|
||||||
myModal.show()
|
myModal.show()
|
||||||
|
|
||||||
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
|
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources"
|
||||||
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) {
|
||||||
@@ -301,10 +348,13 @@ $("#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/charts/rollback?namespace=" + namespace + "&name=" + chart + "&revision=" + revisionNew;
|
const url = "/api/helm/releases/" + namespace + "/" + chart + "/rollback";
|
||||||
$.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 () {
|
||||||
@@ -315,8 +365,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 = "name=" + chart + "&namespace=" + namespace + "&revision=" + revisionNew + "&revisionDiff=" + revisionCur
|
let qstr = "revision=" + revisionNew + "&revisionDiff=" + revisionCur
|
||||||
let url = "/api/helm/charts/manifests"
|
let url = "/api/helm/releases/" + namespace + "/" + chart + "/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)
|
||||||
@@ -345,16 +395,23 @@ $("#btnAddRepository").click(function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
$("#btnTest").click(function() {
|
$("#btnTest").click(function() {
|
||||||
$("#testModal .test-result").empty().prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
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({
|
$.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: "/api/helm/charts/tests" + "?namespace=" + getHashParam("namespace") + "&name=" + getHashParam("chart")
|
url: "/api/helm/releases/" + getHashParam("namespace") + "/" + getHashParam("chart") + "/test"
|
||||||
}).fail(function (xhr) {
|
}).fail(function (xhr) {
|
||||||
reportError("Failed to execute test for chart", xhr)
|
reportError("Failed to execute test for chart", xhr)
|
||||||
|
myModal.hide()
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
$("#testModal .test-result").empty().html(data.replaceAll("\n", "<br>"))
|
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()
|
||||||
})
|
})
|
||||||
|
|
||||||
const myModal = new bootstrap.Modal(document.getElementById('testModal'), {});
|
|
||||||
myModal.show()
|
|
||||||
})
|
})
|
||||||
70
pkg/dashboard/static/api-docs.html
Normal file
70
pkg/dashboard/static/api-docs.html
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<link rel="icon" href="../static/logo.png"/>
|
||||||
|
<link rel="stylesheet" type="text/css"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui.css"/>
|
||||||
|
<title>
|
||||||
|
Helm Dashboard API
|
||||||
|
</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="swagger-ui">
|
||||||
|
<div class="center_progress">
|
||||||
|
<div class="lds-dual-ring"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-bundle.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.15.5/swagger-ui-standalone-preset.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let swaggerUrl = "openapi.json";
|
||||||
|
|
||||||
|
function reqOas() {
|
||||||
|
const request = new XMLHttpRequest();
|
||||||
|
request.open('GET', swaggerUrl, true);
|
||||||
|
request.setRequestHeader('Accept', 'application/json');
|
||||||
|
|
||||||
|
request.onload = function () {
|
||||||
|
if (request.status >= 200 && request.status < 400) {
|
||||||
|
// Success!
|
||||||
|
const data = JSON.parse(request.responseText);
|
||||||
|
display(data);
|
||||||
|
} else {
|
||||||
|
alert("Failed to get "+ swaggerUrl)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = function () {
|
||||||
|
alert("Failed to get "+ swaggerUrl)
|
||||||
|
};
|
||||||
|
|
||||||
|
request.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function display(data) {
|
||||||
|
const parent = document.querySelectorAll('#swagger-ui')[0];
|
||||||
|
parent.innerHTML = '';
|
||||||
|
let el = document.createElement('div');
|
||||||
|
el.id = "swDocs";
|
||||||
|
parent.appendChild(el);
|
||||||
|
|
||||||
|
SwaggerUIBundle({
|
||||||
|
spec: data,
|
||||||
|
dom_id: '#' + el.id,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIStandalonePreset
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
reqOas();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
@@ -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, flag) {
|
function loadContent(mode, namespace, name, revision, revDiff, userDefined) {
|
||||||
let qstr = "name=" + name + "&namespace=" + namespace + "&revision=" + revision
|
let qstr = "revision=" + revision
|
||||||
if (revDiff) {
|
if (revDiff) {
|
||||||
qstr += "&revisionDiff=" + revDiff
|
qstr += "&revisionDiff=" + revDiff
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flag) {
|
if (userDefined) {
|
||||||
qstr += "&flag=" + flag
|
qstr += "&userDefined=" + userDefined
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = "/api/helm/charts/" + mode
|
let url = "/api/helm/releases/" + namespace + "/" + name + "/" + 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,9 +149,7 @@ function showResources(namespace, chart, revision) {
|
|||||||
const resBody = $("#nav-resources .body");
|
const resBody = $("#nav-resources .body");
|
||||||
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
|
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
|
||||||
resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
|
resBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>');
|
||||||
let qstr = "name=" + chart + "&namespace=" + namespace + "&revision=" + revision
|
let url = "/api/helm/releases/" + namespace + "/" + chart + "/resources"
|
||||||
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) {
|
||||||
@@ -182,7 +180,7 @@ function showResources(namespace, chart, revision) {
|
|||||||
|
|
||||||
resBody.append(resBlock)
|
resBody.append(resBlock)
|
||||||
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
|
let ns = res.metadata.namespace ? res.metadata.namespace : namespace
|
||||||
$.getJSON("/api/kube/resources/" + res.kind.toLowerCase() + "?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
|
$.getJSON("/api/k8s/" + res.kind.toLowerCase() + "/get?name=" + res.metadata.name + "&namespace=" + ns).fail(function () {
|
||||||
//reportError("Failed to get list of resources")
|
//reportError("Failed to get list of resources")
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase);
|
const badge = $("<span class='badge me-2 fw-normal'></span>").text(data.status.phase);
|
||||||
@@ -239,7 +237,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/kube/describe/" + kind.toLowerCase() + "?name=" + name + "&namespace=" + ns).fail(function (xhr) {
|
$.get("/api/k8s/" + kind.toLowerCase() + "/describe?name=" + name + "&namespace=" + ns).fail(function (xhr) {
|
||||||
reportError("Failed to describe resource", xhr)
|
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
|
||||||
|
|||||||
@@ -54,7 +54,8 @@
|
|||||||
<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>
|
<li><a class="dropdown-item" href="api-docs" target="_blank"><i
|
||||||
|
class="bi-braces"></i> REST API</a></li>
|
||||||
<li>
|
<li>
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
</li>
|
</li>
|
||||||
@@ -194,7 +195,7 @@
|
|||||||
<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"
|
<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>
|
title="Run tests for this chart"><i class="bi-check-circle"></i> <span>Run tests</span>
|
||||||
</button>
|
</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"
|
||||||
@@ -367,14 +368,16 @@
|
|||||||
<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'></select></label> <span class="ver-old">(current version is <span
|
class='fw-bold text-success ver-new' name="version"></select></label> <span class="ver-old">(current version is <span
|
||||||
class='text-success ms-1'>0.0.0</span>)</span>
|
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">
|
Release Name: <input class="form-control rel-name" name="name">
|
||||||
</label>
|
</label>
|
||||||
<label class="form-label me-4 text-dark">
|
<label class="form-label me-4 text-dark">
|
||||||
Namespace (optional):
|
Namespace (optional):
|
||||||
|
|||||||
@@ -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/charts").fail(function (xhr) {
|
$.getJSON("/api/helm/releases").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,43 +44,13 @@ 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>`)
|
||||||
|
|
||||||
let chartName = elm.chart
|
if (elm.icon) {
|
||||||
let match = null
|
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-]+)*))?'
|
|
||||||
if (!new RegExp(chartNameRegex).test(chartName)) {
|
|
||||||
alert('Chart name does not match chart name regex.')
|
|
||||||
} else {
|
|
||||||
match = chartName.match(chartNameRegex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match) {
|
if (elm.description) {
|
||||||
chartName = elm.chart.substring(0, match.index - 1)
|
card.find(".rel-name div").text(elm.description)
|
||||||
} else {
|
|
||||||
// fall back to simple substr
|
|
||||||
chartName = elm.chart.substring(0, elm.chart.lastIndexOf("-"))
|
|
||||||
}
|
}
|
||||||
$.getJSON("/api/helm/repo/search?name=" + chartName).fail(function (xhr) {
|
|
||||||
// we're ok if we can't show icon and description
|
|
||||||
console.log("Failed to get repo name for charts", xhr)
|
|
||||||
}).done(function (data) {
|
|
||||||
if (data.length > 0) {
|
|
||||||
$.getJSON("/api/helm/charts/show?name=" + data[0].name).fail(function (xhr) {
|
|
||||||
console.log("Failed to get chart", xhr)
|
|
||||||
}).done(function (data) {
|
|
||||||
if (data) {
|
|
||||||
const res = data[0];
|
|
||||||
if (res.icon) {
|
|
||||||
card.find(".rel-name").attr("style", "background-image: url(" + res.icon + ")")
|
|
||||||
}
|
|
||||||
if (res.description) {
|
|
||||||
card.find(".rel-name div").text(res.description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
card.find(".rel-name span").text(elm.name)
|
card.find(".rel-name span").text(elm.name)
|
||||||
card.find(".rel-rev span").text("#" + elm.revision)
|
card.find(".rel-rev span").text("#" + elm.revision)
|
||||||
|
|||||||
749
pkg/dashboard/static/openapi.json
Normal file
749
pkg/dashboard/static/openapi.json
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.3",
|
||||||
|
"info": {
|
||||||
|
"title": "Helm Dashboard API",
|
||||||
|
"version": ""
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Releases"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Repositories"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "K8s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Scanners"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Miscellaneous"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/api/helm/releases": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Releases"
|
||||||
|
],
|
||||||
|
"description": "Get list of installed releases",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Returns list of installed releases"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/releases/{ns}": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "ns",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of kubernetes namespace, use '[emtpy]' if you want to use k8s context default"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Releases"
|
||||||
|
],
|
||||||
|
"description": "Install new release",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"multipart/form-data": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"chart": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Text of values.yaml to use"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "In case preview=true, the preview diff is generated",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"202": {
|
||||||
|
"description": "In case preview=false, the actial install is performed and resulting release object is returned",
|
||||||
|
"content": {
|
||||||
|
"application/json": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/releases/{ns}/{name}": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "ns",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of kubernetes namespace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of Helm release"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Releases"
|
||||||
|
],
|
||||||
|
"description": "Upgrade/reconfigure existing release",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"multipart/form-data": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"chart": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"values": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Text of values.yaml to use"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "In case preview=true, the preview diff is generated",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"202": {
|
||||||
|
"description": "In case preview=false, the actial install is performed and resulting release object is returned",
|
||||||
|
"content": {
|
||||||
|
"application/json": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/releases/{ns}/{name}/history": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "ns",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of kubernetes namespace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of Helm release"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Releases"
|
||||||
|
],
|
||||||
|
"description": "Get revision history for release",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of release revisions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/releases/{ns}/{name}/manifest": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "ns",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of kubernetes namespace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of Helm release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "revision",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Revision to get data from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "revisionDiff",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Revision to diff against"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Releases"
|
||||||
|
],
|
||||||
|
"description": "Get manifest for release",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Manifest text, or diff if revisionDiff is specified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/releases/{ns}/{name}/values": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "ns",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of kubernetes namespace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of Helm release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "revision",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Revision to get data from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "revisionDiff",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Revision to diff against"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userDefined",
|
||||||
|
"in": "query",
|
||||||
|
"description": "If set, only user-defined values will be listed"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Releases"
|
||||||
|
],
|
||||||
|
"description": "Get values for release",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Values YAML text, or diff if revisionDiff is specified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/releases/{ns}/{name}/notes": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "ns",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of kubernetes namespace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of Helm release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "revision",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Revision to get data from"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "revisionDiff",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Revision to diff against"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Releases"
|
||||||
|
],
|
||||||
|
"description": "Get textual notes for release",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Notes text, or diff if revisionDiff is specified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/releases/{ns}/{name}/resources": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "ns",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of kubernetes namespace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of Helm release"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Releases"
|
||||||
|
],
|
||||||
|
"description": "List of installed k8s resources for this release",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Structured list of resources",
|
||||||
|
"content": {
|
||||||
|
"application/json": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/releases/{ns}/{name}/rollback": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "ns",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of kubernetes namespace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of Helm release"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Releases"
|
||||||
|
],
|
||||||
|
"description": "Rollback the release to a previous revision",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/x-www-form-urlencoded": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"revision": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"202": {
|
||||||
|
"description": "Rolled back successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/releases/{ns}/{name}/test": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "ns",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of kubernetes namespace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of Helm release"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Releases"
|
||||||
|
],
|
||||||
|
"description": "Run the tests on a release",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Logs of a test run"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/repositories": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Repositories"
|
||||||
|
],
|
||||||
|
"description": "Get list of Helm repositories",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Returns list of Helm repositories"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Repositories"
|
||||||
|
],
|
||||||
|
"description": "Adds new repository",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/x-www-form-urlencoded": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"url"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Empty response in case repository were added"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/repositories/{repo}": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Name of Helm repository"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Repositories"
|
||||||
|
],
|
||||||
|
"description": "Get list of charts in repository",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Returns list of charts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Repositories"
|
||||||
|
],
|
||||||
|
"description": "Update repository from remote",
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Empty response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Repositories"
|
||||||
|
],
|
||||||
|
"description": "Remove repository",
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Empty response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/repositories/latestver": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Name of Helm chart to search for",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Find the latest available version of specified chart through all the repositories",
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Repositories"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The object with latest available version is returned"
|
||||||
|
},
|
||||||
|
"204": {
|
||||||
|
"description": "In case no matching repository found, the response is empty with status 204"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/repositories/versions": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Name of Helm chart to search for",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"description": "Get the list of versions for specified chart across the repositories",
|
||||||
|
"tags": [
|
||||||
|
"Repositories"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The list if chart versions is returned"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/helm/repositories/values": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "chart",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Name of Helm chart to search for, in format of <repository>/<chart-name>",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "version",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Version of Helm chart to get values from",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"description": "Get the original values.yaml file for the chart",
|
||||||
|
"tags": [
|
||||||
|
"Repositories"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The content of values.yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/k8s/contexts": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"K8s"
|
||||||
|
],
|
||||||
|
"description": "Get list of kubectl contexts configured locally",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Returns list of contexts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/k8s/{kind}/get": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "kind",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Kind of kubernetes resource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Name of kubernetes resource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "namespace",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Namespace of kubernetes resource",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"K8s"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Returns resources information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/k8s/{kind}/list": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "kind",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Kind of kubernetes resource",
|
||||||
|
"schema": {
|
||||||
|
"enum": [
|
||||||
|
"namespaces"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"K8s"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Returns list of resources"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/k8s/{kind}/describe": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "kind",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Kind of kubernetes resource"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Name of kubernetes resource",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "namespace",
|
||||||
|
"in": "query",
|
||||||
|
"description": "Namespace of kubernetes resource",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"K8s"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"text/plain": {}
|
||||||
|
},
|
||||||
|
"description": "Returns describe text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/scanners": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Scanners"
|
||||||
|
],
|
||||||
|
"description": "Get list of discovered scanners",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of scanners"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/scanners/manifests": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Scanners"
|
||||||
|
],
|
||||||
|
"description": "Scan manifests using all applicable scanners",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"multipart/form-data": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"manifest": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Text of manifest to scan"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Map of scan results per scanner type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/scanners/resource/{kind}": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "kind"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "namespace",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "name",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Scanners"
|
||||||
|
],
|
||||||
|
"description": "Scan specified k8s resource in cluster",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Information with scan results per scanner type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Miscellaneous"
|
||||||
|
],
|
||||||
|
"description": "Shuts down the Helm Dashboard application",
|
||||||
|
"responses": {
|
||||||
|
"202": {
|
||||||
|
"description": "Shutdown command has been accepted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/status": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Miscellaneous"
|
||||||
|
],
|
||||||
|
"description": "Gets application status",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Returns JSON with some options",
|
||||||
|
"headers": {
|
||||||
|
"X-Application-Name": {
|
||||||
|
"description": "A string to self-identify the application"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ function loadRepoView() {
|
|||||||
$("#sectionRepo .repo-details").hide()
|
$("#sectionRepo .repo-details").hide()
|
||||||
$("#sectionRepo").show()
|
$("#sectionRepo").show()
|
||||||
|
|
||||||
$.getJSON("/api/helm/repo").fail(function (xhr) {
|
$.getJSON("/api/helm/repositories").fail(function (xhr) {
|
||||||
reportError("Failed to get list of repositories", xhr)
|
reportError("Failed to get list of repositories", xhr)
|
||||||
sendStats('Get repo', {'status': 'fail'});
|
sendStats('Get repo', {'status': 'fail'});
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
@@ -20,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)
|
||||||
@@ -31,7 +31,7 @@ function loadRepoView() {
|
|||||||
$("#sectionRepo .repo-details .url").text(elm.url)
|
$("#sectionRepo .repo-details .url").text(elm.url)
|
||||||
|
|
||||||
$("#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/repo/charts?name=" + elm.name).fail(function (xhr) {
|
$.getJSON("/api/helm/repositories/" + elm.name).fail(function (xhr) {
|
||||||
reportError("Failed to get list of charts in repo", xhr)
|
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()
|
||||||
@@ -42,6 +42,11 @@ 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) {
|
||||||
@@ -86,7 +91,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/repo",
|
url: "/api/helm/repositories",
|
||||||
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)
|
||||||
@@ -100,7 +105,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/repo?name=" + $("#sectionRepo .repo-details h2").text(),
|
url: "/api/helm/repositories/" + $("#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 () {
|
||||||
@@ -114,7 +119,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/repo/update?name=" + $("#sectionRepo .repo-details h2").text(),
|
url: "/api/helm/repositories/" + $("#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 () {
|
||||||
@@ -132,8 +137,11 @@ 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 => {return obj.Name === getHashParam("context")});
|
const ctxFiltered = contexts.filter(obj => {
|
||||||
const contextNamespace = ctxFiltered.length?ctxFiltered[0].Namespace:""
|
return obj.Name === getHashParam("context")
|
||||||
|
});
|
||||||
|
const contextNamespace = ctxFiltered.length ? ctxFiltered[0].Namespace : ""
|
||||||
|
elm.repository = $("#sectionRepo .repo-details h2").text()
|
||||||
popUpUpgrade(elm, contextNamespace)
|
popUpUpgrade(elm, contextNamespace)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,14 @@ 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/charts/history?name=" + name + "&namespace=" + namespace).fail(function (xhr) {
|
$.getJSON("/api/helm/releases/" + namespace + "/" + name + "/history").fail(function (xhr) {
|
||||||
reportError("Failed to get chart details", xhr)
|
reportError("Failed to get chart details", xhr)
|
||||||
}).done(function (data) {
|
}).done(function (data) {
|
||||||
fillChartHistory(data, namespace, name);
|
fillChartHistory(data, namespace, name);
|
||||||
|
|
||||||
checkUpgradeable(data[data.length - 1].chart_name)
|
checkUpgradeable(data[0].chart_name)
|
||||||
|
|
||||||
|
$("#btnTest").toggle(data[0].has_tests)
|
||||||
|
|
||||||
const rev = getHashParam("revision")
|
const rev = getHashParam("revision")
|
||||||
if (rev) {
|
if (rev) {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function fillClusters(limNS) {
|
|||||||
filterInstalledList($("#installedList .body .row"))
|
filterInstalledList($("#installedList .body .row"))
|
||||||
})
|
})
|
||||||
|
|
||||||
$.getJSON("/api/kube/contexts").fail(function (xhr) {
|
$.getJSON("/api/k8s/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/kube/namespaces").fail(function (xhr) {
|
$.getJSON("/api/k8s/namespaces/list").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)
|
||||||
|
|||||||
@@ -1,318 +0,0 @@
|
|||||||
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 (d *DataLayer) RunTests(namespace string, name string) (string, error) {
|
|
||||||
cmd := []string{"test", name, "--namespace", namespace, "--logs"}
|
|
||||||
|
|
||||||
out, err := d.runCommandHelm(cmd...)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RevisionDiff(functor SectionFn, ext string, namespace string, name string, revision1 int, revision2 int, flag bool) (string, error) {
|
|
||||||
if revision1 == 0 || revision2 == 0 {
|
|
||||||
log.Debugf("One of revisions is zero: %d %d", revision1, revision2)
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest1, err := functor(namespace, name, revision1, flag)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest2, err := functor(namespace, name, revision2, flag)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
diff := GetDiff(manifest1, manifest2, strconv.Itoa(revision1)+ext, strconv.Itoa(revision2)+ext)
|
|
||||||
return diff, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDiff(text1 string, text2 string, name1 string, name2 string) string {
|
|
||||||
edits := myers.ComputeEdits(span.URIFromPath(""), text1, text2)
|
|
||||||
unified := gotextdiff.ToUnified(name1, name2, text1, edits)
|
|
||||||
diff := fmt.Sprint(unified)
|
|
||||||
log.Debugf("The diff is: %s", diff)
|
|
||||||
return diff
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package subproc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"helm.sh/helm/v3/pkg/release"
|
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFlow(t *testing.T) {
|
|
||||||
log.SetLevel(log.DebugLevel)
|
|
||||||
|
|
||||||
var _ release.Status
|
|
||||||
data := DataLayer{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package subproc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"helm.sh/helm/v3/pkg/release"
|
|
||||||
helmtime "helm.sh/helm/v3/pkg/time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// unpleasant copy from Helm sources, where they have it non-public
|
|
||||||
|
|
||||||
type ReleaseElement struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Namespace string `json:"namespace"`
|
|
||||||
Revision string `json:"revision"`
|
|
||||||
Updated helmtime.Time `json:"updated"`
|
|
||||||
Status release.Status `json:"status"`
|
|
||||||
Chart string `json:"chart"`
|
|
||||||
AppVersion string `json:"app_version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HistoryElement struct {
|
|
||||||
Revision int `json:"revision"`
|
|
||||||
Updated helmtime.Time `json:"updated"`
|
|
||||||
Status release.Status `json:"status"`
|
|
||||||
Chart string `json:"chart"`
|
|
||||||
AppVersion string `json:"app_version"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
|
|
||||||
ChartName string `json:"chart_name"` // 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"`
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -105,10 +104,9 @@ 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, revRequired bool) (*QueryProps, error) {
|
func GetQueryProps(c *gin.Context) (*QueryProps, error) {
|
||||||
qp := QueryProps{}
|
qp := QueryProps{}
|
||||||
|
|
||||||
qp.Namespace = c.Query("namespace")
|
qp.Namespace = c.Query("namespace")
|
||||||
@@ -117,11 +115,5 @@ func GetQueryProps(c *gin.Context, revRequired bool) (*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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,34 +10,24 @@ 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
|
||||||
revRequired bool
|
wantErr 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,
|
||||||
revRequired: true,
|
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing&revision=1",
|
||||||
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing&revision=1",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Get query props - no revision with revRequired true",
|
name: "Get query props - no namespace with revRequired true",
|
||||||
wantErr: true,
|
wantErr: false,
|
||||||
revRequired: true,
|
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&revision=1",
|
||||||
endpoint: "/api/v1/namespaces/komodorio/charts?name=testing&namespace=testing",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Get query props - no namespace with revRequired true",
|
name: "Get query props - no name with revRequired true",
|
||||||
wantErr: false,
|
wantErr: true,
|
||||||
revRequired: true,
|
endpoint: "/api/v1/namespaces/komodorio/charts?namespace=testing&revision=1",
|
||||||
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",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +36,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, tt.revRequired)
|
_, err := GetQueryProps(c)
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user