// SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s package model import ( "context" "errors" "log/slog" "strings" "sync/atomic" "time" backoff "github.com/cenkalti/backoff/v4" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/slogs" "github.com/sahilm/fuzzy" ) // RevValues tracks Helm values representations. type RevValues struct { gvr *client.GVR inUpdate int32 path string rev string query string lines []string allValues bool listeners []ResourceViewerListener options ViewerToggleOpts } // NewRevValues return a new Helm values resource model. func NewRevValues(gvr *client.GVR, path, rev string) *RevValues { return &RevValues{ gvr: gvr, path: path, rev: rev, allValues: false, lines: getRevValues(path, rev), } } func getHelmHistDao() *dao.HelmHistory { return Registry[client.HmhGVR].DAO.(*dao.HelmHistory) } func getRevValues(path, _ string) []string { vals, err := getHelmHistDao().GetValues(path, true) if err != nil { slog.Error("Failed to get Helm values", slogs.Error, err) } return strings.Split(string(vals), "\n") } // GVR returns the resource gvr. func (v *RevValues) GVR() *client.GVR { return v.gvr } // GetPath returns the active resource path. func (v *RevValues) GetPath() string { return v.path } // SetOptions toggle model options. func (v *RevValues) SetOptions(ctx context.Context, opts ViewerToggleOpts) { v.options = opts if err := v.refresh(ctx); err != nil { v.fireResourceFailed(err) } } // Filter filters the model. func (v *RevValues) Filter(q string) { v.query = q v.filterChanged(v.lines) } func (v *RevValues) filterChanged(lines []string) { v.fireResourceChanged(lines, v.filter(v.query, lines)) } func (v *RevValues) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } if f, ok := internal.IsFuzzySelector(q); ok { return v.fuzzyFilter(strings.TrimSpace(f), lines) } return rxFilter(q, lines) } func (*RevValues) fuzzyFilter(q string, lines []string) fuzzy.Matches { return fuzzy.Find(q, lines) } func (v *RevValues) fireResourceChanged(lines []string, matches fuzzy.Matches) { for _, l := range v.listeners { l.ResourceChanged(lines, matches) } } func (v *RevValues) fireResourceFailed(err error) { for _, l := range v.listeners { l.ResourceFailed(err) } } // ClearFilter clear out the filter. func (v *RevValues) ClearFilter() { v.query = "" } // Peek returns the current model data. func (v *RevValues) Peek() []string { return v.lines } // Refresh updates model data. func (v *RevValues) Refresh(ctx context.Context) error { return v.refresh(ctx) } // Watch watches for Values changes. func (v *RevValues) Watch(ctx context.Context) error { if err := v.refresh(ctx); err != nil { return err } go v.updater(ctx) return nil } func (v *RevValues) updater(ctx context.Context) { defer slog.Debug("YAML canceled", slogs.GVR, v.gvr) backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval) delay := defaultReaderRefreshRate for { select { case <-ctx.Done(): return case <-time.After(delay): if err := v.refresh(ctx); err != nil { v.fireResourceFailed(err) if delay = backOff.NextBackOff(); delay == backoff.Stop { slog.Error("Giving up retrieving chart values", slogs.Error, err) return } } else { backOff.Reset() delay = defaultReaderRefreshRate } } } } func (v *RevValues) refresh(context.Context) error { if !atomic.CompareAndSwapInt32(&v.inUpdate, 0, 1) { slog.Debug("Dropping update...") return errors.New("refresh in progress, dropping") } defer atomic.StoreInt32(&v.inUpdate, 0) v.reconcile() return nil } func (v *RevValues) reconcile() { v.fireResourceChanged(v.lines, v.filter(v.query, v.lines)) } // AddListener adds a new model listener. func (v *RevValues) AddListener(l ResourceViewerListener) { v.listeners = append(v.listeners, l) } // RemoveListener delete a listener from the list. func (v *RevValues) RemoveListener(l ResourceViewerListener) { victim := -1 for i, lis := range v.listeners { if lis == l { victim = i break } } if victim >= 0 { v.listeners = append(v.listeners[:victim], v.listeners[victim+1:]...) } }