From db9cdeb1c9362a1734b09b5428fb3e0278b61cee Mon Sep 17 00:00:00 2001 From: Andrei Pohilko Date: Wed, 23 Nov 2022 12:38:03 +0000 Subject: [PATCH] Split data layer into separate files --- pkg/dashboard/subproc/data.go | 300 +------------------------------ pkg/dashboard/subproc/kubectl.go | 144 +++++++++++++++ pkg/dashboard/subproc/repos.go | 159 ++++++++++++++++ 3 files changed, 310 insertions(+), 293 deletions(-) create mode 100644 pkg/dashboard/subproc/kubectl.go create mode 100644 pkg/dashboard/subproc/repos.go diff --git a/pkg/dashboard/subproc/data.go b/pkg/dashboard/subproc/data.go index 95a561c..2d9a74d 100644 --- a/pkg/dashboard/subproc/data.go +++ b/pkg/dashboard/subproc/data.go @@ -5,8 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "regexp" - "sort" "strconv" "strings" "time" @@ -17,7 +15,6 @@ import ( "github.com/komodorio/helm-dashboard/pkg/dashboard/utils" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" - "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1" ) @@ -64,20 +61,6 @@ func (d *DataLayer) runCommandHelm(cmd ...string) (string, error) { return d.runCommand(cmd...) } -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...) -} - func (d *DataLayer) forceNamespace(s *string) { if d.Namespace != "" { *s = d.Namespace @@ -102,46 +85,14 @@ func (d *DataLayer) CheckConnectivity() error { return nil } -type KubeContext struct { - IsCurrent bool - Name string - Cluster string - AuthInfo string - Namespace string -} - -func (d *DataLayer) ListContexts() (res []KubeContext, err error) { - out, err := d.runCommandKubectl("config", "get-contexts") - if err != nil { - return nil, err +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 } - - // 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 + return d.StatusInfo } func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) { @@ -197,70 +148,6 @@ func (d *DataLayer) ReleaseHistory(namespace string, releaseName string) (res [] return res, nil } -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 { - 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 - } - } - } -} - 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) { @@ -328,63 +215,6 @@ func (d *DataLayer) RevisionValues(namespace string, chartName string, revision }) } -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 -} - func (d *DataLayer) ReleaseUninstall(namespace string, name string) error { d.Cache.Invalidate(CacheKeyRelList, cacheTagRelease(namespace, name)) @@ -399,18 +229,6 @@ func (d *DataLayer) Rollback(namespace string, name string, rev int) error { return err } -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) 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) @@ -452,102 +270,6 @@ func (d *DataLayer) ChartInstall(namespace string, name string, repoChart string return out, nil } -// 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 -} - -func (d *DataLayer) ChartRepoList() (res []RepositoryElement, err error) { - out, err := d.Cache.String(CacheKeyAllRepos, nil, func() (string, error) { - 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) 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) 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 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) @@ -575,11 +297,3 @@ func GetDiff(text1 string, text2 string, name1 string, name2 string) string { log.Debugf("The diff is: %s", diff) return diff } - -type NamespaceElement struct { - Items []struct { - Metadata struct { - Name string `json:"name"` - } `json:"metadata"` - } `json:"items"` -} diff --git a/pkg/dashboard/subproc/kubectl.go b/pkg/dashboard/subproc/kubectl.go new file mode 100644 index 0000000..6773208 --- /dev/null +++ b/pkg/dashboard/subproc/kubectl.go @@ -0,0 +1,144 @@ +package subproc + +import ( + "encoding/json" + v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1" + "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) { + 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 +} diff --git a/pkg/dashboard/subproc/repos.go b/pkg/dashboard/subproc/repos.go new file mode 100644 index 0000000..ccb2bb7 --- /dev/null +++ b/pkg/dashboard/subproc/repos.go @@ -0,0 +1,159 @@ +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) { + 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 { + 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 +}