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