diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 78b49ef8..85cbd234 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,14 +1,10 @@ - +## Changes Proposed -## Fixes Issue + - + - - -## Changes proposed - - +## Check List -## Check List (Check all the applicable boxes) - -- [ ] 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 - - - -## Note to reviewers +- [ ] The title of my pull request is a short description of the changes +- [ ] This PR relates to some issue: +- [ ] I have documented the changes made (if applicable) +- [ ] I have covered the changes with unit tests - diff --git a/README.md b/README.md index 91b3fa58..de10fc33 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ ![GitHub contributors](https://img.shields.io/github/contributors/komodorio/helm-dashboard) [![GitHub issues](https://img.shields.io/github/issues-raw/komodorio/helm-dashboard)](https://github.com/komodorio/helm-dashboard/issues) ![GitHub stars](https://img.shields.io/github/stars/komodorio/helm-dashboard?style=social) ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/komodorio/helm-dashboard) ![GitHub pull requests](https://img.shields.io/github/issues-pr/komodorio/helm-dashboard) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/komodorio/helm-dashboard) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/komodorio/helm-dashboard) [![GitHub license](https://img.shields.io/github/license/komodorio/helm-dashboard)](https://github.com/komodorio/helm-dashboard) -[Screenshot](screenshot.png) +[Screenshot](images/screenshot.png) ## 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. + +![](images/screenshot_local_charts.png) + +### Execute Helm tests For all the release(s) (installed helm charts), you can execute helm tests for that release. For the tests to execute successfully, you need to have existing tests for that helm chart. You can execute `helm test` for the specific release as below: -![](screenshot_run_test.png) +![](images/screenshot_run_test.png) The result of executed `helm test` for the release will be disapled as below: -![](screenshot_run_test_result.png) +![](images/screenshot_run_test_result.png) -## 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: -![](screenshot_scan_resource.png) +![](images/screenshot_scan_resource.png) 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: -![](screenshot_scan_manifest.png) +![](images/screenshot_scan_manifest.png) ## Support Channels diff --git a/screenshot.png b/images/screenshot.png similarity index 100% rename from screenshot.png rename to images/screenshot.png diff --git a/images/screenshot_local_charts.png b/images/screenshot_local_charts.png new file mode 100644 index 00000000..fe159108 Binary files /dev/null and b/images/screenshot_local_charts.png differ diff --git a/screenshot_run_test.png b/images/screenshot_run_test.png similarity index 100% rename from screenshot_run_test.png rename to images/screenshot_run_test.png diff --git a/screenshot_run_test_result.png b/images/screenshot_run_test_result.png similarity index 100% rename from screenshot_run_test_result.png rename to images/screenshot_run_test_result.png diff --git a/screenshot_scan_manifest.png b/images/screenshot_scan_manifest.png similarity index 100% rename from screenshot_scan_manifest.png rename to images/screenshot_scan_manifest.png diff --git a/screenshot_scan_resource.png b/images/screenshot_scan_resource.png similarity index 100% rename from screenshot_scan_resource.png rename to images/screenshot_scan_resource.png diff --git a/main.go b/main.go index 8c4cafc1..2b62aa49 100644 --- a/main.go +++ b/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()) diff --git a/pkg/dashboard/handlers/helmHandlers.go b/pkg/dashboard/handlers/helmHandlers.go index 2e489a37..91540c3c 100644 --- a/pkg/dashboard/handlers/helmHandlers.go +++ b/pkg/dashboard/handlers/helmHandlers.go @@ -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 { diff --git a/pkg/dashboard/objects/data.go b/pkg/dashboard/objects/data.go index f5883c06..d21a9feb 100644 --- a/pkg/dashboard/objects/data.go +++ b/pkg/dashboard/objects/data.go @@ -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) } } } diff --git a/pkg/dashboard/objects/repos.go b/pkg/dashboard/objects/repos.go index 5aedad3a..250a91ba 100644 --- a/pkg/dashboard/objects/repos.go +++ b/pkg/dashboard/objects/repos.go @@ -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 +} diff --git a/pkg/dashboard/objects/repos_test.go b/pkg/dashboard/objects/repos_test.go index f9c2b508..d02256d0 100644 --- a/pkg/dashboard/objects/repos_test.go +++ b/pkg/dashboard/objects/repos_test.go @@ -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.Equal(t, r.Orig.URL, testRepoUrl) -} - -func TestDelete(t *testing.T) { - testRepository := initRepository(t, validRepositoryConfigPath, false) - - testRepoName := "charts" // don't ever delete 'testing'! - err := testRepository.Delete(testRepoName) - if err != nil { - t.Fatal(err, "Failed to delete the repo") - } - - _, err = testRepository.Get(testRepoName) - if err == nil { - t.Fatal("Failed to delete repo") - } -} - -func TestGet(t *testing.T) { - // Initial repositiry name in test file - repoName := "charts" - - testRepository := initRepository(t, validRepositoryConfigPath, false) - - repo, err := testRepository.Get(repoName) - if err != nil { - t.Fatal(err, "Failed to get th repo") - } - - assert.Equal(t, repo.Orig.Name, repoName) + assert.NilError(t, err) + assert.Equal(t, r.URL(), testRepoUrl) + + // update repo + err = r.Update() + assert.NilError(t, err) + + // list charts + c, err := r.Charts() + assert.NilError(t, err) + + // contains chart + c, err = testRepository.Containing(c[0].Name) + assert.NilError(t, err) + + // chart by name from repo + c, err = r.ByName(c[0].Name) + assert.NilError(t, err) + + // get chart values + v, err := testRepository.GetChartValues(r.Name()+"/"+c[0].Name, c[0].Version) + assert.NilError(t, err) + assert.Assert(t, v != "") + + // delete added + err = testRepository.Delete(testRepoName) + assert.NilError(t, err) + + // final list + repos, err = testRepository.List() + assert.NilError(t, err) + assert.Equal(t, len(repos), 5) } func TestRepository_Charts_DevelDisabled(t *testing.T) { diff --git a/pkg/dashboard/server.go b/pkg/dashboard/server.go index a668f57d..eaadf0d7 100644 --- a/pkg/dashboard/server.go +++ b/pkg/dashboard/server.go @@ -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 diff --git a/pkg/dashboard/static/actions.js b/pkg/dashboard/static/actions.js index 43cddc46..677618ea 100644 --- a/pkg/dashboard/static/actions.js +++ b/pkg/dashboard/static/actions.js @@ -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 = $(""); + const opt = $("").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('') - 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(' 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(' 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 = "
Tests executed successfully

Empty response from API
" } else { output = data.replaceAll("\n", "
") diff --git a/pkg/dashboard/static/index.html b/pkg/dashboard/static/index.html index e3fcde61..2eb94553 100644 --- a/pkg/dashboard/static/index.html +++ b/pkg/dashboard/static/index.html @@ -479,7 +479,7 @@ - + + /BANNER END --> \ No newline at end of file diff --git a/pkg/dashboard/static/repo.js b/pkg/dashboard/static/repo.js index 33f67014..b7d0fcb1 100644 --- a/pkg/dashboard/static/repo.js +++ b/pkg/dashboard/static/repo.js @@ -10,7 +10,7 @@ function loadRepoView() { data.sort((a, b) => (a.name > b.name) - (a.name < b.name)) data.forEach(function (elm) { - let opt = $('
  • '); + let opt = $('
  • '); 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('') $.getJSON("/api/helm/repositories/" + elm.name).fail(function (xhr) { reportError("Failed to get list of charts in repo", xhr)