Support working with local charts (#215)
* Basic functioning * Support reconfiguring * Improve tests coverage * Always update local repo, don't offer to delete it * Handle multi-repo correctly * Document local charts usage * Screenshot for docs
30
.github/pull_request_template.md
vendored
@@ -1,14 +1,10 @@
|
||||
<!-- If your PR fixes an open issue, use `Closes #999` to link your PR with the issue. #999 stands for the issue number you are fixing -->
|
||||
## Changes Proposed
|
||||
|
||||
## Fixes Issue
|
||||
<!-- Describe the proposed changes and any additional information -->
|
||||
|
||||
<!-- Remove this section if not applicable -->
|
||||
<!-- Add all the screenshots which illustrate your changes -->
|
||||
|
||||
<!-- Example: Closes #31 -->
|
||||
|
||||
## Changes proposed
|
||||
|
||||
<!-- List all the proposed changes in your PR -->
|
||||
## Check List
|
||||
|
||||
<!-- Mark all the applicable boxes. To mark the box as done follow the following conventions -->
|
||||
<!--
|
||||
@@ -18,18 +14,8 @@
|
||||
[ ] - Not correct; marked as **not** done
|
||||
-->
|
||||
|
||||
## Check List (Check all the applicable boxes) <!-- Follow the above conventions to check the box -->
|
||||
- [ ] The title of my pull request is a short description of the changes
|
||||
- [ ] This PR relates to some issue: <!-- use "Closes #999" to auto-close related issue -->
|
||||
- [ ] I have documented the changes made (if applicable)
|
||||
- [ ] I have covered the changes with unit tests
|
||||
|
||||
- [ ] My code follows the code style of this project.
|
||||
- [ ] My change requires changes to the documentation.
|
||||
- [ ] I have updated the documentation accordingly.
|
||||
- [ ] All new and existing tests passed.
|
||||
- [ ] The title of my pull request is a short description of the requested changes.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!-- Add all the screenshots which support your changes -->
|
||||
|
||||
## Note to reviewers
|
||||
|
||||
<!-- Add notes to reviewers if applicable -->
|
||||
|
||||
29
README.md
@@ -13,7 +13,7 @@
|
||||
|
||||
 [](https://github.com/komodorio/helm-dashboard/issues)      [](https://github.com/komodorio/helm-dashboard)
|
||||
|
||||
<kbd>[<img src="screenshot.png" style="width: 100%; border: 1px solid silver;" border="1" alt="Screenshot">](screenshot.png)</kbd>
|
||||
<kbd>[<img src="images/screenshot.png" style="width: 100%; border: 1px solid silver;" border="1" alt="Screenshot">](images/screenshot.png)</kbd>
|
||||
|
||||
|
||||
## Description
|
||||
@@ -96,29 +96,44 @@ If you want to increase the logging verbosity and see all the debug info, use th
|
||||
|
||||
The official helm chart is [available here](https://github.com/komodorio/helm-charts/blob/master/charts/helm-dashboard)
|
||||
|
||||
## Selected Features
|
||||
|
||||
## Execute Helm tests
|
||||
### Support for Local Charts
|
||||
|
||||
Local Helm chart is a directory with specially named files and a `Chart.yaml` file, which you can install via Helm, without the need to publish the chart into Helm repository. Chart developers might want to experiment with the chart locally, before uploading into public repository. Also, a proprietary application might only use non-published chart as an approach to deploy the software.
|
||||
|
||||
For all the above use-cases, you may use Helm Dashboard UI, spcifying location of your local chart folders via special `--local-chart` command-line parameter. The parameter might be specified multiple times, for example:
|
||||
|
||||
```shell
|
||||
helm-dashboard --local-chart=/opt/charts/my-private-app --local-chart=/home/dev/sources/app/chart
|
||||
```
|
||||
|
||||
When _valid_ local chart sources specified, the repository list would contain a surrogate `[local]` entry, with those charts listed inside. All the chart operations are normal: installing, reconfiguring and upgrading.
|
||||
|
||||

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

|
||||

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

|
||||

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

|
||||

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

|
||||

|
||||
|
||||
## Support Channels
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 270 KiB |
BIN
images/screenshot_local_charts.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
30
main.go
@@ -22,14 +22,15 @@ var (
|
||||
)
|
||||
|
||||
type options struct {
|
||||
Version bool `long:"version" description:"Show tool version"`
|
||||
Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"`
|
||||
NoBrowser bool `short:"b" long:"no-browser" description:"Do not attempt to open Web browser upon start"`
|
||||
NoTracking bool `long:"no-analytics" description:"Disable user analytics (GA, DataDog etc.)"`
|
||||
BindHost string `long:"bind" description:"Host binding to start server (default: localhost)"` // default should be printed but not assigned as the precedence: flag > env > default
|
||||
Port uint `short:"p" long:"port" description:"Port to start server on" default:"8080"`
|
||||
Namespace string `short:"n" long:"namespace" description:"Namespace for HELM operations"`
|
||||
Devel bool `long:"devel" description:"Include development versions of charts"`
|
||||
Version bool `long:"version" description:"Show tool version"`
|
||||
Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"`
|
||||
NoBrowser bool `short:"b" long:"no-browser" description:"Do not attempt to open Web browser upon start"`
|
||||
NoTracking bool `long:"no-analytics" description:"Disable user analytics (Heap, DataDog etc.)"`
|
||||
BindHost string `long:"bind" description:"Host binding to start server (default: localhost)"` // default should be printed but not assigned as the precedence: flag > env > default
|
||||
Port uint `short:"p" long:"port" description:"Port to start server on" default:"8080"`
|
||||
Namespace string `short:"n" long:"namespace" description:"Namespace for HELM operations"`
|
||||
Devel bool `long:"devel" description:"Include development versions of charts"`
|
||||
LocalChart []string `long:"local-chart" description:"Specify location of local chart to include into UI"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -46,12 +47,13 @@ func main() {
|
||||
setupLogging(opts.Verbose)
|
||||
|
||||
server := dashboard.Server{
|
||||
Version: version,
|
||||
Namespaces: strings.Split(opts.Namespace, ","),
|
||||
Address: fmt.Sprintf("%s:%d", opts.BindHost, opts.Port),
|
||||
Debug: opts.Verbose,
|
||||
NoTracking: opts.NoTracking,
|
||||
Devel: opts.Devel,
|
||||
Version: version,
|
||||
Namespaces: strings.Split(opts.Namespace, ","),
|
||||
Address: fmt.Sprintf("%s:%d", opts.BindHost, opts.Port),
|
||||
Debug: opts.Verbose,
|
||||
NoTracking: opts.NoTracking,
|
||||
Devel: opts.Devel,
|
||||
LocalCharts: opts.LocalChart,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -3,11 +3,13 @@ package handlers
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hexops/gotextdiff"
|
||||
"github.com/hexops/gotextdiff/myers"
|
||||
"github.com/hexops/gotextdiff/span"
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
"github.com/rogpeppe/go-internal/semver"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -15,12 +17,11 @@ import (
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
helmtime "helm.sh/helm/v3/pkg/time"
|
||||
"k8s.io/utils/strings/slices"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HelmHandler struct {
|
||||
@@ -162,6 +163,7 @@ func (h *HelmHandler) RepoVersions(c *gin.Context) {
|
||||
AppVersion: r.AppVersion,
|
||||
Description: r.Description,
|
||||
Repository: r.Annotations[objects.AnnRepo],
|
||||
URLs: r.URLs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -194,6 +196,7 @@ func (h *HelmHandler) RepoLatestVer(c *gin.Context) {
|
||||
AppVersion: r.AppVersion,
|
||||
Description: r.Description,
|
||||
Repository: r.Annotations[objects.AnnRepo],
|
||||
URLs: r.URLs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -288,12 +291,19 @@ func (h *HelmHandler) Install(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
repoChart, err := h.checkLocalRepo(c.PostForm("chart"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
justTemplate := c.PostForm("preview") == "true"
|
||||
ns := c.Param("ns")
|
||||
if ns == "[empty]" {
|
||||
ns = ""
|
||||
}
|
||||
rel, err := app.Releases.Install(ns, c.PostForm("name"), c.PostForm("chart"), c.PostForm("version"), justTemplate, values)
|
||||
|
||||
rel, err := app.Releases.Install(ns, c.PostForm("name"), repoChart, c.PostForm("version"), justTemplate, values)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
@@ -306,6 +316,16 @@ func (h *HelmHandler) Install(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HelmHandler) checkLocalRepo(repoChart string) (string, error) {
|
||||
if strings.HasPrefix(repoChart, "file://") {
|
||||
repoChart = repoChart[len("file://"):]
|
||||
if !slices.Contains(h.Data.LocalCharts, repoChart) {
|
||||
return "", fmt.Errorf("chart path is not present in local charts: %s", repoChart)
|
||||
}
|
||||
}
|
||||
return repoChart, nil
|
||||
}
|
||||
|
||||
func (h *HelmHandler) Upgrade(c *gin.Context) {
|
||||
app := h.GetApp(c)
|
||||
if app == nil {
|
||||
@@ -325,8 +345,14 @@ func (h *HelmHandler) Upgrade(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
repoChart, err := h.checkLocalRepo(c.PostForm("chart"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
justTemplate := c.PostForm("preview") == "true"
|
||||
rel, err := existing.Upgrade(c.PostForm("chart"), c.PostForm("version"), justTemplate, values)
|
||||
rel, err := existing.Upgrade(repoChart, c.PostForm("version"), justTemplate, values)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
@@ -409,7 +435,13 @@ func (h *HelmHandler) RepoValues(c *gin.Context) {
|
||||
return // sets error inside
|
||||
}
|
||||
|
||||
out, err := app.Repositories.GetChartValues(c.Query("chart"), c.Query("version"))
|
||||
repoChart, err := h.checkLocalRepo(c.Query("chart"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := app.Repositories.GetChartValues(repoChart, c.Query("version"))
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
@@ -433,8 +465,8 @@ func (h *HelmHandler) RepoList(c *gin.Context) {
|
||||
out := []RepositoryElement{}
|
||||
for _, r := range repos {
|
||||
out = append(out, RepositoryElement{
|
||||
Name: r.Orig.Name,
|
||||
URL: r.Orig.URL,
|
||||
Name: r.Name(),
|
||||
URL: r.URL(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -521,15 +553,16 @@ func (h *HelmHandler) handleGetSection(rel *objects.Release, section string, rDi
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type RepoChartElement struct {
|
||||
type RepoChartElement struct { // TODO: do we need it at all? there is existing repo.ChartVersion in Helm
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
AppVersion string `json:"app_version"`
|
||||
Description string `json:"description"`
|
||||
|
||||
InstalledNamespace string `json:"installed_namespace"`
|
||||
InstalledName string `json:"installed_name"`
|
||||
Repository string `json:"repository"`
|
||||
InstalledNamespace string `json:"installed_namespace"`
|
||||
InstalledName string `json:"installed_name"`
|
||||
Repository string `json:"repository"`
|
||||
URLs []string `json:"urls"`
|
||||
}
|
||||
|
||||
func HReleaseToJSON(o *release.Release) *ReleaseElement {
|
||||
|
||||
@@ -32,6 +32,7 @@ type DataLayer struct {
|
||||
appPerContext map[string]*Application
|
||||
appPerContextMx *sync.Mutex
|
||||
devel bool
|
||||
LocalCharts []string
|
||||
}
|
||||
|
||||
type StatusInfo struct {
|
||||
@@ -170,6 +171,8 @@ func (d *DataLayer) AppForCtx(ctx string) (*Application, error) {
|
||||
return nil, errorx.Decorate(err, "Failed to create application for context '%s'", ctx)
|
||||
}
|
||||
|
||||
a.Repositories.LocalCharts = d.LocalCharts
|
||||
|
||||
app = a
|
||||
d.appPerContext[ctx] = app
|
||||
}
|
||||
@@ -218,7 +221,7 @@ func (d *DataLayer) loopUpdateRepos(ctx context.Context, interval time.Duration)
|
||||
for _, repo := range repos {
|
||||
err := repo.Update()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to update repo %s: %v", repo.Orig.Name, err)
|
||||
log.Warnf("Failed to update repo %s: %v", repo.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"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"
|
||||
@@ -26,9 +25,10 @@ type Repositories struct {
|
||||
HelmConfig *action.Configuration
|
||||
mx sync.Mutex
|
||||
versionConstraint *semver.Constraints
|
||||
LocalCharts []string
|
||||
}
|
||||
|
||||
func (r *Repositories) Load() (*repo.File, error) {
|
||||
func (r *Repositories) load() (*repo.File, error) {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
@@ -40,20 +40,28 @@ func (r *Repositories) Load() (*repo.File, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (r *Repositories) List() ([]*Repository, error) {
|
||||
f, err := r.Load()
|
||||
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{}
|
||||
res := []Repository{}
|
||||
for _, item := range f.Repositories {
|
||||
res = append(res, &Repository{
|
||||
Settings: r.Settings,
|
||||
Orig: item,
|
||||
res = append(res, &HelmRepo{
|
||||
Settings: r.Settings,
|
||||
Orig: item,
|
||||
versionConstraint: r.versionConstraint,
|
||||
})
|
||||
}
|
||||
|
||||
if len(r.LocalCharts) > 0 {
|
||||
lc := LocalChart{
|
||||
LocalCharts: r.LocalCharts,
|
||||
}
|
||||
res = append(res, &lc)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -71,7 +79,7 @@ func (r *Repositories) Add(name string, url string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := r.Load()
|
||||
f, err := r.load()
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "Failed to load repo config")
|
||||
}
|
||||
@@ -114,7 +122,7 @@ func (r *Repositories) Add(name string, url string) error {
|
||||
}
|
||||
|
||||
func (r *Repositories) Delete(name string) error {
|
||||
f, err := r.Load()
|
||||
f, err := r.load()
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "failed to load repo information")
|
||||
}
|
||||
@@ -136,25 +144,22 @@ func (r *Repositories) Delete(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repositories) Get(name string) (*Repository, error) {
|
||||
f, err := r.Load()
|
||||
func (r *Repositories) Get(name string) (Repository, error) {
|
||||
l, err := r.List()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to load repo information")
|
||||
return nil, errorx.Decorate(err, "failed to get list of repos")
|
||||
}
|
||||
|
||||
for _, entry := range f.Repositories {
|
||||
if entry.Name == name {
|
||||
return &Repository{
|
||||
Settings: r.Settings,
|
||||
Orig: entry,
|
||||
versionConstraint: r.versionConstraint,
|
||||
}, nil
|
||||
for _, entry := range l {
|
||||
if entry.Name() == name {
|
||||
return entry, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errorx.DataUnavailable.New("Could not find reposiroty '%s'", name)
|
||||
return nil, errorx.DataUnavailable.New("Could not find repository '%s'", name)
|
||||
}
|
||||
|
||||
// Containing returns list of chart versions for the given chart name, across all repositories
|
||||
func (r *Repositories) Containing(name string) (repo.ChartVersions, error) {
|
||||
list, err := r.List()
|
||||
if err != nil {
|
||||
@@ -165,7 +170,7 @@ func (r *Repositories) Containing(name string) (repo.ChartVersions, error) {
|
||||
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.Warnf("Failed to get data from repo '%s', updating it might help", rep.Name())
|
||||
log.Debugf("The error was: %v", err)
|
||||
continue
|
||||
}
|
||||
@@ -178,7 +183,7 @@ func (r *Repositories) Containing(name string) (repo.ChartVersions, error) {
|
||||
v.Annotations = map[string]string{}
|
||||
}
|
||||
|
||||
v.Annotations[AnnRepo] = rep.Orig.Name
|
||||
v.Annotations[AnnRepo] = rep.Name()
|
||||
|
||||
// Validate the versions against semantic version constraints and filter
|
||||
version, err := semver.NewVersion(v.Version)
|
||||
@@ -199,24 +204,6 @@ func (r *Repositories) Containing(name string) (repo.ChartVersions, error) {
|
||||
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)
|
||||
@@ -234,7 +221,15 @@ func (r *Repositories) GetChartValues(chart string, ver string) (string, error)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
type Repository interface {
|
||||
Name() string
|
||||
URL() string
|
||||
Update() error
|
||||
Charts() (repo.ChartVersions, error)
|
||||
ByName(name string) (repo.ChartVersions, error)
|
||||
}
|
||||
|
||||
type HelmRepo struct {
|
||||
Settings *cli.EnvSettings
|
||||
Orig *repo.Entry
|
||||
mx sync.Mutex
|
||||
@@ -242,11 +237,19 @@ type Repository struct {
|
||||
versionConstraint *semver.Constraints
|
||||
}
|
||||
|
||||
func (r *Repository) indexFileName() string {
|
||||
func (r *HelmRepo) Name() string {
|
||||
return r.Orig.Name
|
||||
}
|
||||
|
||||
func (r *HelmRepo) URL() string {
|
||||
return r.Orig.URL
|
||||
}
|
||||
|
||||
func (r *HelmRepo) indexFileName() string {
|
||||
return filepath.Join(r.Settings.RepositoryCache, helmpath.CacheIndexFile(r.Orig.Name))
|
||||
}
|
||||
|
||||
func (r *Repository) getIndex() (*repo.IndexFile, error) {
|
||||
func (r *HelmRepo) getIndex() (*repo.IndexFile, error) {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
|
||||
@@ -260,13 +263,13 @@ func (r *Repository) getIndex() (*repo.IndexFile, error) {
|
||||
return ind, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Charts() ([]*repo.ChartVersion, error) {
|
||||
func (r *HelmRepo) Charts() (repo.ChartVersions, error) {
|
||||
ind, err := r.getIndex()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get repo index")
|
||||
}
|
||||
|
||||
res := []*repo.ChartVersion{}
|
||||
res := repo.ChartVersions{}
|
||||
for _, cv := range ind.Entries {
|
||||
for _, v := range cv {
|
||||
version, err := semver.NewVersion(v.Version)
|
||||
@@ -292,7 +295,7 @@ func (r *Repository) Charts() ([]*repo.ChartVersion, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ByName(name string) (repo.ChartVersions, error) {
|
||||
func (r *HelmRepo) ByName(name string) (repo.ChartVersions, error) {
|
||||
ind, err := r.getIndex()
|
||||
if err != nil {
|
||||
return nil, errorx.Decorate(err, "failed to get repo index")
|
||||
@@ -305,7 +308,7 @@ func (r *Repository) ByName(name string) (repo.ChartVersions, error) {
|
||||
return repo.ChartVersions{}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Update() error {
|
||||
func (r *HelmRepo) Update() error {
|
||||
r.mx.Lock()
|
||||
defer r.mx.Unlock()
|
||||
log.Infof("Updating repository: %s", r.Orig.Name)
|
||||
@@ -366,3 +369,59 @@ func versionConstaint(isDevelEnabled bool) (*semver.Constraints, error) {
|
||||
|
||||
return constraint, nil
|
||||
}
|
||||
|
||||
type LocalChart struct {
|
||||
LocalCharts []string
|
||||
|
||||
charts map[string]repo.ChartVersions
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
// Update reloads the chart information from disk
|
||||
func (l *LocalChart) Update() error {
|
||||
l.mx.Lock()
|
||||
defer l.mx.Unlock()
|
||||
|
||||
l.charts = map[string]repo.ChartVersions{}
|
||||
for _, lc := range l.LocalCharts {
|
||||
c, err := loader.Load(lc)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to load chart from '%s': %s", lc, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// we don't filter out dev versions here, because local chart implies user wants to see the chart anyway
|
||||
l.charts[c.Name()] = repo.ChartVersions{&repo.ChartVersion{
|
||||
URLs: []string{l.URL() + lc},
|
||||
Metadata: c.Metadata,
|
||||
}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LocalChart) Name() string {
|
||||
return "[local]"
|
||||
}
|
||||
|
||||
func (l *LocalChart) URL() string {
|
||||
return "file://"
|
||||
}
|
||||
|
||||
func (l *LocalChart) Charts() (repo.ChartVersions, error) {
|
||||
_ = l.Update() // always re-read, for chart devs to have quick debug loop
|
||||
res := repo.ChartVersions{}
|
||||
for _, c := range l.charts {
|
||||
res = append(res, c...)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (l *LocalChart) ByName(name string) (repo.ChartVersions, error) {
|
||||
_ = l.Update() // always re-read, for chart devs to have quick debug loop
|
||||
for n, c := range l.charts {
|
||||
if n == name {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return repo.ChartVersions{}, nil
|
||||
}
|
||||
|
||||
@@ -57,67 +57,61 @@ func initRepository(t *testing.T, filePath string, devel bool) *Repositories {
|
||||
Settings: settings,
|
||||
HelmConfig: &action.Configuration{}, // maybe use copy of getFakeHelmConfig from api_test.go
|
||||
versionConstraint: vc,
|
||||
LocalCharts: []string{"../../../charts/helm-dashboard"},
|
||||
}
|
||||
|
||||
return testRepository
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
func TestFlow(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, false)
|
||||
|
||||
// initial list
|
||||
repos, err := testRepository.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, len(repos), 5)
|
||||
|
||||
assert.Equal(t, len(repos), 4)
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
testRepoName := "TEST"
|
||||
testRepoUrl := "https://helm.github.io/examples"
|
||||
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, false)
|
||||
err := testRepository.Add(testRepoName, testRepoUrl)
|
||||
if err != nil {
|
||||
t.Fatal(err, "Failed to add repo")
|
||||
}
|
||||
// add repo
|
||||
err = testRepository.Add(testRepoName, testRepoUrl)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// get repo
|
||||
r, err := testRepository.Get(testRepoName)
|
||||
if err != nil {
|
||||
t.Fatal(err, "Failed to add repo")
|
||||
}
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, r.URL(), testRepoUrl)
|
||||
|
||||
assert.Equal(t, r.Orig.URL, testRepoUrl)
|
||||
}
|
||||
// update repo
|
||||
err = r.Update()
|
||||
assert.NilError(t, err)
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, false)
|
||||
// list charts
|
||||
c, err := r.Charts()
|
||||
assert.NilError(t, err)
|
||||
|
||||
testRepoName := "charts" // don't ever delete 'testing'!
|
||||
err := testRepository.Delete(testRepoName)
|
||||
if err != nil {
|
||||
t.Fatal(err, "Failed to delete the repo")
|
||||
}
|
||||
// contains chart
|
||||
c, err = testRepository.Containing(c[0].Name)
|
||||
assert.NilError(t, err)
|
||||
|
||||
_, err = testRepository.Get(testRepoName)
|
||||
if err == nil {
|
||||
t.Fatal("Failed to delete repo")
|
||||
}
|
||||
}
|
||||
// chart by name from repo
|
||||
c, err = r.ByName(c[0].Name)
|
||||
assert.NilError(t, err)
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
// Initial repositiry name in test file
|
||||
repoName := "charts"
|
||||
// get chart values
|
||||
v, err := testRepository.GetChartValues(r.Name()+"/"+c[0].Name, c[0].Version)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, v != "")
|
||||
|
||||
testRepository := initRepository(t, validRepositoryConfigPath, false)
|
||||
// delete added
|
||||
err = testRepository.Delete(testRepoName)
|
||||
assert.NilError(t, err)
|
||||
|
||||
repo, err := testRepository.Get(repoName)
|
||||
if err != nil {
|
||||
t.Fatal(err, "Failed to get th repo")
|
||||
}
|
||||
|
||||
assert.Equal(t, repo.Orig.Name, repoName)
|
||||
// final list
|
||||
repos, err = testRepository.List()
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, len(repos), 5)
|
||||
}
|
||||
|
||||
func TestRepository_Charts_DevelDisabled(t *testing.T) {
|
||||
|
||||
@@ -24,12 +24,13 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Version string
|
||||
Namespaces []string
|
||||
Address string
|
||||
Debug bool
|
||||
NoTracking bool
|
||||
Devel bool
|
||||
Version string
|
||||
Namespaces []string
|
||||
Address string
|
||||
Debug bool
|
||||
NoTracking bool
|
||||
Devel bool
|
||||
LocalCharts []string
|
||||
}
|
||||
|
||||
func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (string, utils.ControlChan, error) {
|
||||
@@ -38,6 +39,8 @@ func (s *Server) StartServer(ctx context.Context, cancel context.CancelFunc) (st
|
||||
return "", nil, errorx.Decorate(err, "Failed to create data layer")
|
||||
}
|
||||
|
||||
data.LocalCharts = s.LocalCharts
|
||||
|
||||
isDevModeWithAnalytics := os.Getenv("HD_DEV_ANALYTICS") == "true"
|
||||
data.StatusInfo.Analytics = (!s.NoTracking && s.Version != "0.0.0") || isDevModeWithAnalytics
|
||||
|
||||
|
||||
@@ -56,13 +56,7 @@ function checkUpgradeable(name) {
|
||||
function popUpUpgrade(elm, ns, name, verCur, lastRev) {
|
||||
$("#upgradeModal .btn-confirm").prop("disabled", true)
|
||||
|
||||
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("initial", !verCur)
|
||||
$('#upgradeModal').data("newManifest", "")
|
||||
|
||||
$("#upgradeModalLabel .name").text(elm.name)
|
||||
@@ -93,14 +87,17 @@ function popUpUpgrade(elm, ns, name, verCur, lastRev) {
|
||||
$.getJSON("/api/helm/repositories/versions?name=" + elm.name).fail(function (xhr) {
|
||||
reportError("Failed to find chart in repo", xhr)
|
||||
}).done(function (vers) {
|
||||
vers.sort((b, a) => (a.version > b.version) - (a.version < b.version))
|
||||
|
||||
// fill versions
|
||||
$('#upgradeModal select').empty()
|
||||
for (let i = 0; i < vers.length; i++) {
|
||||
const opt = $("<option value='" + vers[i].version + "'></option>");
|
||||
const opt = $("<option value='" + vers[i].version + "'></option>").data("ver", vers[i]);
|
||||
const label = vers[i].repository + " @ " + vers[i].version;
|
||||
if (vers[i].version === verCur) {
|
||||
opt.html(vers[i].version + " ·")
|
||||
opt.html(label + " ✓")
|
||||
} else {
|
||||
opt.html(vers[i].version)
|
||||
opt.html(label)
|
||||
}
|
||||
$('#upgradeModal select').append(opt)
|
||||
}
|
||||
@@ -162,9 +159,7 @@ function changeTimer() {
|
||||
if (reconfigTimeout) {
|
||||
window.clearTimeout(reconfigTimeout)
|
||||
}
|
||||
reconfigTimeout = window.setTimeout(function () {
|
||||
requestChangeDiff()
|
||||
}, 500)
|
||||
reconfigTimeout = window.setTimeout(requestChangeDiff, 500)
|
||||
}
|
||||
|
||||
$("#upgradeModal textarea").keyup(changeTimer)
|
||||
@@ -173,12 +168,26 @@ $("#upgradeModal .rel-ns").keyup(changeTimer)
|
||||
|
||||
$('#upgradeModal select').change(function () {
|
||||
const self = $(this)
|
||||
const ver = self.find("option:selected").data("ver");
|
||||
|
||||
let chart = ver.repository + "/" + ver.name;
|
||||
if (!ver.name) {
|
||||
chart = ""
|
||||
}
|
||||
|
||||
// local chart case
|
||||
if (ver.urls && ver.urls.length && ver.urls[0].startsWith("file://")) {
|
||||
chart = ver.urls[0];
|
||||
}
|
||||
|
||||
$('#upgradeModal').data("chart", chart)
|
||||
$('#upgradeModal form .chart-name').val(chart)
|
||||
|
||||
requestChangeDiff()
|
||||
|
||||
// fill reference values
|
||||
$("#upgradeModal .ref-vals").html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>')
|
||||
const chart = $("#upgradeModal").data("chart");
|
||||
|
||||
// TODO: if chart is empty, query different URL that will restore values without repo
|
||||
if (chart) {
|
||||
$.get("/api/helm/repositories/values?chart=" + chart + "&version=" + self.val()).fail(function (xhr) {
|
||||
@@ -231,7 +240,6 @@ $('#upgradeModal .btn-scan').click(function () {
|
||||
})
|
||||
|
||||
function requestChangeDiff() {
|
||||
const self = $('#upgradeModal select');
|
||||
const diffBody = $("#upgradeModalBody");
|
||||
diffBody.empty().append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Calculating diff...')
|
||||
$("#upgradeModal .btn-confirm").prop("disabled", true)
|
||||
@@ -394,7 +402,7 @@ $("#btnAddRepository").click(function () {
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
$("#btnTest").click(function() {
|
||||
$("#btnTest").click(function () {
|
||||
const myModal = new bootstrap.Modal(document.getElementById('testModal'), {});
|
||||
$("#testModal .test-result").empty().prepend('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Waiting for completion...')
|
||||
myModal.show()
|
||||
@@ -406,7 +414,7 @@ $("#btnTest").click(function() {
|
||||
myModal.hide()
|
||||
}).done(function (data) {
|
||||
var output;
|
||||
if(data.length == 0 || data == null || data == "") {
|
||||
if (data.length == 0 || data == null || data == "") {
|
||||
output = "<div>Tests executed successfully<br><br><pre>Empty response from API<pre></div>"
|
||||
} else {
|
||||
output = data.replaceAll("\n", "<br>")
|
||||
|
||||
@@ -479,7 +479,7 @@
|
||||
<script src="static/actions.js"></script>
|
||||
<script src="static/scripts.js"></script>
|
||||
|
||||
<!-- BANNER START -->
|
||||
<!-- BANNER START
|
||||
<a id="banner"
|
||||
href="https://helm-dashboard-survey.komodor.com/"
|
||||
class="display-none position-absolute top-0 start-50 translate-middle-x bg-primary text-light rounded px-2 mt-1 text-decoration-none py-1">Help
|
||||
@@ -519,7 +519,7 @@
|
||||
$("#banner").hide()
|
||||
})
|
||||
</script>
|
||||
<!-- /BANNER END -->
|
||||
/BANNER END -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,7 +10,7 @@ function loadRepoView() {
|
||||
data.sort((a, b) => (a.name > b.name) - (a.name < b.name))
|
||||
|
||||
data.forEach(function (elm) {
|
||||
let opt = $('<li class="mb-2"><label><input type="radio" name="cluster" class="me-2"/><span></span></label></li>');
|
||||
let opt = $('<li class="mb-2"><label><input type="radio" name="repo" class="me-2"/><span></span></label></li>');
|
||||
opt.attr('title', elm.url)
|
||||
opt.find("input").val(elm.name).text(elm.name).data("item", elm)
|
||||
opt.find("span").text(elm.name)
|
||||
@@ -30,6 +30,8 @@ function loadRepoView() {
|
||||
$("#sectionRepo .repo-details h2").text(elm.name)
|
||||
$("#sectionRepo .repo-details .url").text(elm.url)
|
||||
|
||||
$("#sectionRepo .btn-remove").prop("disabled", elm.url.startsWith('file://'))
|
||||
|
||||
$("#sectionRepo .repo-details ul").html('<span class="spinner-border spinner-border-sm mx-1" role="status" aria-hidden="true"></span>')
|
||||
$.getJSON("/api/helm/repositories/" + elm.name).fail(function (xhr) {
|
||||
reportError("Failed to get list of charts in repo", xhr)
|
||||
|
||||