k9s/internal/model/yaml.go

240 lines
5.1 KiB
Go

package model
import (
"context"
"fmt"
"reflect"
"regexp"
"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/render"
"github.com/rs/zerolog/log"
"github.com/sahilm/fuzzy"
)
// ManageFieldOpts tracks managed fields.
const ManagedFieldsOpts = "ManagedFields"
// YAML tracks yaml resource representations.
type YAML struct {
gvr client.GVR
inUpdate int32
path string
query string
lines []string
listeners []ResourceViewerListener
options ViewerToggleOpts
}
// NewYAML return a new yaml resource model.
func NewYAML(gvr client.GVR, path string) *YAML {
return &YAML{
gvr: gvr,
path: path,
}
}
// GetPath returns the active resource path.
func (y *YAML) GetPath() string {
return y.path
}
// SetOptions toggle model options.
func (y *YAML) SetOptions(ctx context.Context, opts ViewerToggleOpts) {
y.options = opts
if err := y.refresh(ctx); err != nil {
y.fireResourceFailed(err)
}
}
// Filter filters the model.
func (y *YAML) Filter(q string) {
y.query = q
y.filterChanged(y.lines)
}
func (y *YAML) filterChanged(lines []string) {
y.fireResourceChanged(lines, y.filter(y.query, lines))
}
func (y *YAML) filter(q string, lines []string) fuzzy.Matches {
if q == "" {
return nil
}
if dao.IsFuzzySelector(q) {
return y.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
}
return y.rxFilter(q, lines)
}
func (*YAML) fuzzyFilter(q string, lines []string) fuzzy.Matches {
return fuzzy.Find(q, lines)
}
func (*YAML) rxFilter(q string, lines []string) fuzzy.Matches {
rx, err := regexp.Compile(`(?i)` + q)
if err != nil {
return nil
}
matches := make(fuzzy.Matches, 0, len(lines))
for i, l := range lines {
if loc := rx.FindStringIndex(l); len(loc) == 2 {
matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc})
}
}
return matches
}
func (y *YAML) fireResourceChanged(lines []string, matches fuzzy.Matches) {
for _, l := range y.listeners {
l.ResourceChanged(lines, matches)
}
}
func (y *YAML) fireResourceFailed(err error) {
for _, l := range y.listeners {
l.ResourceFailed(err)
}
}
// ClearFilter clear out the filter.
func (y *YAML) ClearFilter() {
y.query = ""
}
// Peel returns the current model data.
func (y *YAML) Peek() []string {
return y.lines
}
// Watch watches for YAML changes.
func (y *YAML) Watch(ctx context.Context) error {
if err := y.refresh(ctx); err != nil {
return err
}
go y.updater(ctx)
return nil
}
func (y *YAML) updater(ctx context.Context) {
defer log.Debug().Msgf("YAML canceled -- %q", y.gvr)
backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval)
delay := defaultReaderRefreshRate
for {
select {
case <-ctx.Done():
return
case <-time.After(delay):
if err := y.refresh(ctx); err != nil {
y.fireResourceFailed(err)
if delay = backOff.NextBackOff(); delay == backoff.Stop {
log.Error().Err(err).Msgf("YAML gave up!")
return
}
} else {
backOff.Reset()
delay = defaultReaderRefreshRate
}
}
}
}
func (y *YAML) refresh(ctx context.Context) error {
if !atomic.CompareAndSwapInt32(&y.inUpdate, 0, 1) {
log.Debug().Msgf("Dropping update...")
return nil
}
defer atomic.StoreInt32(&y.inUpdate, 0)
if err := y.reconcile(ctx); err != nil {
return err
}
return nil
}
func (y *YAML) reconcile(ctx context.Context) error {
s, err := y.ToYAML(ctx, y.gvr, y.path, y.options[ManagedFieldsOpts])
if err != nil {
return err
}
lines := strings.Split(s, "\n")
if reflect.DeepEqual(lines, y.lines) {
return nil
}
y.lines = lines
y.fireResourceChanged(y.lines, y.filter(y.query, y.lines))
return nil
}
// AddListener adds a new model listener.
func (y *YAML) AddListener(l ResourceViewerListener) {
y.listeners = append(y.listeners, l)
}
// RemoveListener delete a listener from the list.
func (y *YAML) RemoveListener(l ResourceViewerListener) {
victim := -1
for i, lis := range y.listeners {
if lis == l {
victim = i
break
}
}
if victim >= 0 {
y.listeners = append(y.listeners[:victim], y.listeners[victim+1:]...)
}
}
// ToYAML returns a resource yaml.
func (y *YAML) ToYAML(ctx context.Context, gvr client.GVR, path string, showManaged bool) (string, error) {
meta, err := getMeta(ctx, gvr)
if err != nil {
return "", err
}
desc, ok := meta.DAO.(dao.Describer)
if !ok {
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
}
return desc.ToYAML(path, showManaged)
}
func getMeta(ctx context.Context, gvr client.GVR) (ResourceMeta, error) {
meta := resourceMeta(gvr)
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return ResourceMeta{}, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
meta.DAO.Init(factory, gvr)
return meta, nil
}
func resourceMeta(gvr client.GVR) ResourceMeta {
meta, ok := Registry[gvr.String()]
if !ok {
meta = ResourceMeta{
DAO: &dao.Table{},
Renderer: &render.Generic{},
}
}
if meta.DAO == nil {
meta.DAO = &dao.Resource{}
}
return meta
}