mirror of
https://github.com/komodorio/helm-dashboard.git
synced 2026-03-26 06:18: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:
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)
|
||||
}
|
||||
Reference in New Issue
Block a user