[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:
Andrey Pokhilko
2023-02-01 13:24:34 +00:00
committed by GitHub
parent 6ffcdf2b8e
commit e13aa2fde6
44 changed files with 4338 additions and 1278 deletions

View File

@@ -1,100 +0,0 @@
package subproc
import (
"context"
"errors"
"github.com/eko/gocache/v3/marshaler"
"github.com/eko/gocache/v3/store"
gocache "github.com/patrickmn/go-cache"
log "github.com/sirupsen/logrus"
"time"
)
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 {
Marshaler *marshaler.Marshaler `json:"-"`
HitCount int
MissCount int
}
func NewCache() *Cache {
gocacheClient := gocache.New(60*time.Minute, 10*time.Minute)
gocacheStore := store.NewGoCache(gocacheClient)
// TODO: use tiered cache with some disk backend, allow configuring that static cache folder
// Initializes marshaler
marshal := marshaler.New(gocacheStore)
return &Cache{
Marshaler: marshal,
}
}
func (c *Cache) String(key CacheKey, tags []string, callback func() (string, error)) (string, error) {
if tags == nil {
tags = make([]string, 0)
}
tags = append(tags, key)
ctx := context.Background()
out := ""
_, err := c.Marshaler.Get(ctx, key, &out)
if err == nil {
log.Debugf("Using cached value for %s", key)
c.HitCount++
return out, nil
} else if !errors.Is(err, store.NotFound{}) {
return "", err
}
c.MissCount++
out, err = callback()
if err != nil {
return "", err
}
err = c.Marshaler.Set(ctx, key, out, store.WithTags(tags))
if err != nil {
return "", err
}
return out, nil
}
func (c *Cache) Invalidate(tags ...CacheKey) {
log.Debugf("Invalidating tags %v", tags)
err := c.Marshaler.Invalidate(context.Background(), store.WithInvalidateTags(tags))
if err != nil {
log.Warnf("Failed to invalidate tags %v: %s", tags, err)
}
}
func (c *Cache) Clear() error {
c.HitCount = 0
c.MissCount = 0
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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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
}