From 7bd7ec766ab2436fb2ffe3a220ae8493f525556e Mon Sep 17 00:00:00 2001 From: Jayson Wang Date: Wed, 20 Dec 2023 06:21:12 +0800 Subject: [PATCH] adding value, yaml and describe views to helm-history (#2341) * correct describe of helm history * value, yaml and describe view for helm-history * add value_extender and clean up code * move showValues to the new extender --- internal/dao/helm_chart.go | 1 + internal/dao/helm_history.go | 72 ++++++++++++++++++++++-------- internal/dao/registry.go | 3 +- internal/dao/types.go | 6 +++ internal/model/values.go | 42 ++++++++++++++---- internal/view/helm_chart.go | 41 +---------------- internal/view/helm_history.go | 6 +-- internal/view/value_extender.go | 79 +++++++++++++++++++++++++++++++++ 8 files changed, 179 insertions(+), 71 deletions(-) create mode 100644 internal/view/value_extender.go diff --git a/internal/dao/helm_chart.go b/internal/dao/helm_chart.go index 8aa26dca..0f1e8fdb 100644 --- a/internal/dao/helm_chart.go +++ b/internal/dao/helm_chart.go @@ -21,6 +21,7 @@ var ( _ Accessor = (*HelmChart)(nil) _ Nuker = (*HelmChart)(nil) _ Describer = (*HelmChart)(nil) + _ Valuer = (*HelmChart)(nil) ) // HelmChart represents a helm chart. diff --git a/internal/dao/helm_history.go b/internal/dao/helm_history.go index 38fda22f..d589298c 100644 --- a/internal/dao/helm_history.go +++ b/internal/dao/helm_history.go @@ -7,19 +7,23 @@ import ( "context" "fmt" "strconv" + "strings" + + "gopkg.in/yaml.v2" + "helm.sh/helm/v3/pkg/action" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render/helm" - "helm.sh/helm/v3/pkg/action" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" ) var ( _ Accessor = (*HelmHistory)(nil) _ Nuker = (*HelmHistory)(nil) _ Describer = (*HelmHistory)(nil) + _ Valuer = (*HelmHistory)(nil) ) // HelmHistory represents a helm chart. @@ -55,12 +59,24 @@ func (h *HelmHistory) List(ctx context.Context, _ string) ([]runtime.Object, err // Get returns a resource. func (h *HelmHistory) Get(_ context.Context, path string) (runtime.Object, error) { - ns, n := client.Namespaced(path) + fqn, rev, found := strings.Cut(path, ":") + if !found || len(rev) == 0 { + return nil, fmt.Errorf("invalid path %q", path) + } + + ns, n := client.Namespaced(fqn) cfg, err := ensureHelmConfig(h.Client(), ns) if err != nil { return nil, err } - resp, err := action.NewGet(cfg).Run(n) + + getter := action.NewGet(cfg) + getter.Version, err = strconv.Atoi(rev) + if err != nil { + return nil, err + } + + resp, err := getter.Run(n) if err != nil { return nil, err } @@ -70,32 +86,50 @@ func (h *HelmHistory) Get(_ context.Context, path string) (runtime.Object, error // Describe returns the chart notes. func (h *HelmHistory) Describe(path string) (string, error) { - ns, n := client.Namespaced(path) - cfg, err := ensureHelmConfig(h.Client(), ns) - if err != nil { - return "", err - } - resp, err := action.NewGet(cfg).Run(n) + rel, err := h.Get(context.Background(), path) if err != nil { return "", err } - return resp.Info.Notes, nil + resp, ok := rel.(helm.ReleaseRes) + if !ok { + return "", fmt.Errorf("expected helm.ReleaseRes, but got %T", rel) + } + + return resp.Release.Info.Notes, nil } // ToYAML returns the chart manifest. func (h *HelmHistory) ToYAML(path string, showManaged bool) (string, error) { - ns, n := client.Namespaced(path) - cfg, err := ensureHelmConfig(h.Client(), ns) - if err != nil { - return "", err - } - resp, err := action.NewGet(cfg).Run(n) + rel, err := h.Get(context.Background(), path) if err != nil { return "", err } - return resp.Manifest, nil + resp, ok := rel.(helm.ReleaseRes) + if !ok { + return "", fmt.Errorf("expected helm.ReleaseRes, but got %T", rel) + } + + return resp.Release.Manifest, nil +} + +// GetValues return the config for this chart. +func (h *HelmHistory) GetValues(path string, allValues bool) ([]byte, error) { + rel, err := h.Get(context.Background(), path) + if err != nil { + return nil, err + } + + resp, ok := rel.(helm.ReleaseRes) + if !ok { + return nil, fmt.Errorf("expected helm.ReleaseRes, but got %T", rel) + } + + if allValues { + return yaml.Marshal(resp.Release.Chart.Values) + } + return yaml.Marshal(resp.Release.Config) } func (h *HelmHistory) Rollback(_ context.Context, path, rev string) error { diff --git a/internal/dao/registry.go b/internal/dao/registry.go index 54fdbce5..aee73f22 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -102,6 +102,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) { client.NewGVR("v1/namespaces"): &Namespace{}, client.NewGVR("popeye"): &Popeye{}, client.NewGVR("helm"): &HelmChart{}, + client.NewGVR("helm-history"): &HelmHistory{}, client.NewGVR("dir"): &Dir{}, } @@ -314,7 +315,7 @@ func loadHelm(m ResourceMetas) { Kind: "History", Namespaced: true, Verbs: []string{"delete"}, - Categories: []string{k9sCat}, + Categories: []string{helmCat}, } } diff --git a/internal/dao/types.go b/internal/dao/types.go index e24cb22b..d51d25e8 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -170,3 +170,9 @@ type Sanitizer interface { // Sanitize nukes all resources in unhappy state. Sanitize(context.Context, string) (int, error) } + +// Valuer represents a resource with values. +type Valuer interface { + // GetValues returns values for a resource. + GetValues(path string, allValues bool) ([]byte, error) +} diff --git a/internal/model/values.go b/internal/model/values.go index 8d7135d7..890facd0 100644 --- a/internal/model/values.go +++ b/internal/model/values.go @@ -5,6 +5,7 @@ package model import ( "context" + "fmt" "strings" "sync/atomic" "time" @@ -18,6 +19,7 @@ import ( // Values tracks Helm values representations. type Values struct { + factory dao.Factory gvr client.GVR inUpdate int32 path string @@ -34,20 +36,36 @@ func NewValues(gvr client.GVR, path string) *Values { gvr: gvr, path: path, allValues: false, - lines: getValues(path, false), } } -func getHelmDao() *dao.HelmChart { - return Registry["helm"].DAO.(*dao.HelmChart) +// Init initializes the model. +func (v *Values) Init(f dao.Factory) error { + v.factory = f + + var err error + v.lines, err = v.getValues() + + return err } -func getValues(path string, allValues bool) []string { - vals, err := getHelmDao().GetValues(path, allValues) +func (v *Values) getValues() ([]string, error) { + accessor, err := dao.AccessorFor(v.factory, v.gvr) if err != nil { - log.Error().Err(err).Msgf("Failed to get Helm values") + return nil, err } - return strings.Split(string(vals), "\n") + + valuer, ok := accessor.(dao.Valuer) + if !ok { + return nil, fmt.Errorf("Resource %s is not Valuer", v.gvr) + } + + values, err := valuer.GetValues(v.path, v.allValues) + if err != nil { + return nil, err + } + + return strings.Split(string(values), "\n"), nil } // GVR returns the resource gvr. @@ -56,10 +74,16 @@ func (v *Values) GVR() client.GVR { } // ToggleValues toggles between user supplied values and computed values. -func (v *Values) ToggleValues() { +func (v *Values) ToggleValues() error { v.allValues = !v.allValues - lines := getValues(v.path, v.allValues) + + lines, err := v.getValues() + if err != nil { + return err + } + v.lines = lines + return nil } // GetPath returns the active resource path. diff --git a/internal/view/helm_chart.go b/internal/view/helm_chart.go index e6aba216..d4624e7a 100644 --- a/internal/view/helm_chart.go +++ b/internal/view/helm_chart.go @@ -8,23 +8,19 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" - "github.com/rs/zerolog/log" ) // HelmChart represents a helm chart view. type HelmChart struct { ResourceViewer - - Values *model.Values } -// NewHelm returns a new alias view. +// NewHelmChart returns a new helm-chart view. func NewHelmChart(gvr client.GVR) ResourceViewer { c := HelmChart{ - ResourceViewer: NewBrowser(gvr), + ResourceViewer: NewValueExtender(NewBrowser(gvr)), } c.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) c.GetTable().SetSelectedStyle(tcell.StyleDefault. @@ -46,7 +42,6 @@ func (c *HelmChart) bindKeys(aa ui.KeyActions) { aa.Add(ui.KeyActions{ ui.KeyR: ui.NewKeyAction("Releases", c.historyCmd, true), ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false), - ui.KeyV: ui.NewKeyAction("Values", c.getValsCmd(), true), }) } @@ -77,35 +72,3 @@ func (c *HelmChart) helmContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyPath, path) } - -func (c *HelmChart) getValsCmd() func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - path := c.GetTable().GetSelectedItem() - if path == "" { - return evt - } - c.Values = model.NewValues(c.GVR(), path) - v := NewLiveView(c.App(), "Values", c.Values) - v.actions.Add(ui.KeyActions{ - ui.KeyV: ui.NewKeyAction("Toggle All Values", c.toggleValuesCmd, true), - }) - if err := v.app.inject(v, false); err != nil { - v.app.Flash().Err(err) - } - return nil - } -} - -func (c *HelmChart) toggleValuesCmd(evt *tcell.EventKey) *tcell.EventKey { - c.Values.ToggleValues() - if err := c.Values.Refresh(c.defaultCtx()); err != nil { - log.Error().Err(err).Msgf("helm refresh failed") - return nil - } - c.App().Flash().Infof("Values toggled") - return nil -} - -func (c *HelmChart) defaultCtx() context.Context { - return context.WithValue(context.Background(), internal.KeyFactory, c.App().factory) -} diff --git a/internal/view/helm_history.go b/internal/view/helm_history.go index 9a1b7eb8..185acaeb 100644 --- a/internal/view/helm_history.go +++ b/internal/view/helm_history.go @@ -21,10 +21,10 @@ type History struct { ResourceViewer } -// NewHelm returns a new alias view. +// NewHistory returns a new helm-history view. func NewHistory(gvr client.GVR) ResourceViewer { h := History{ - ResourceViewer: NewBrowser(gvr), + ResourceViewer: NewValueExtender(NewBrowser(gvr)), } h.GetTable().SetColorerFn(helm.History{}.ColorerFunc()) h.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) @@ -35,7 +35,7 @@ func NewHistory(gvr client.GVR) ResourceViewer { return &h } -// Init initializes the vie +// Init initializes the view func (h *History) Init(ctx context.Context) error { if err := h.ResourceViewer.Init(ctx); err != nil { return err diff --git a/internal/view/value_extender.go b/internal/view/value_extender.go new file mode 100644 index 00000000..a7cf7f8c --- /dev/null +++ b/internal/view/value_extender.go @@ -0,0 +1,79 @@ +package view + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tcell/v2" + "github.com/rs/zerolog/log" +) + +// ValueExtender adds values actions to a given viewer. +type ValueExtender struct { + ResourceViewer +} + +// NewValueExtender returns a new extender. +func NewValueExtender(r ResourceViewer) ResourceViewer { + p := ValueExtender{ResourceViewer: r} + p.AddBindKeysFn(p.bindKeys) + p.GetTable().SetEnterFn(func(app *App, model ui.Tabular, gvr, path string) { + p.valuesCmd(nil) + }) + + return &p +} + +func (v *ValueExtender) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyV: ui.NewKeyAction("Values", v.valuesCmd, true), + }) +} + +func (v *ValueExtender) valuesCmd(evt *tcell.EventKey) *tcell.EventKey { + path := v.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + showValues(v.defaultCtx(), v.App(), path, v.GVR()) + return nil +} + +func (v *ValueExtender) defaultCtx() context.Context { + return context.WithValue(context.Background(), internal.KeyFactory, v.App().factory) +} + +func showValues(ctx context.Context, app *App, path string, gvr client.GVR) { + vm := model.NewValues(gvr, path) + if err := vm.Init(app.factory); err != nil { + app.Flash().Errf("Initializing the values model failed: %s", err) + } + + toggleValuesCmd := func(evt *tcell.EventKey) *tcell.EventKey { + if err := vm.ToggleValues(); err != nil { + app.Flash().Errf("Values toggle failed: %s", err) + return nil + } + + if err := vm.Refresh(ctx); err != nil { + log.Error().Err(err).Msgf("values refresh failed") + return nil + } + + app.Flash().Infof("Values toggled") + return nil + } + + v := NewLiveView(app, "Values", vm) + v.actions.Add(ui.KeyActions{ + ui.KeyV: ui.NewKeyAction("Toggle All Values", toggleValuesCmd, true), + }) + + if err := v.app.inject(v, false); err != nil { + v.app.Flash().Err(err) + } +}