k9s/internal/model/tree.go

318 lines
6.8 KiB
Go

package model
import (
"context"
"fmt"
"regexp"
"strings"
"sync/atomic"
"time"
"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/derailed/k9s/internal/xray"
"github.com/rs/zerolog/log"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
)
const initTreeRefreshRate = 500 * time.Millisecond
// TreeListener represents a tree model listener.
type TreeListener interface {
// TreeChanged notifies the model data changed.
TreeChanged(*xray.TreeNode)
// TreeLoadFailed notifies the load failed.
TreeLoadFailed(error)
}
// Tree represents a tree model.
type Tree struct {
gvr client.GVR
namespace string
root *xray.TreeNode
listeners []TreeListener
inUpdate int32
refreshRate time.Duration
query string
}
// NewTree returns a new model.
func NewTree(gvr client.GVR) *Tree {
return &Tree{
gvr: gvr,
refreshRate: 2 * time.Second,
}
}
// ClearFilter clears out active filter.
func (t *Tree) ClearFilter() {
t.query = ""
}
// SetFilter sets the current filter.
func (t *Tree) SetFilter(q string) {
t.query = q
}
// AddListener adds a listener.
func (t *Tree) AddListener(l TreeListener) {
t.listeners = append(t.listeners, l)
}
// RemoveListener delete a listener.
func (t *Tree) RemoveListener(l TreeListener) {
victim := -1
for i, lis := range t.listeners {
if lis == l {
victim = i
break
}
}
if victim >= 0 {
t.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...)
}
}
// Watch initiates model updates.
func (t *Tree) Watch(ctx context.Context) {
t.Refresh(ctx)
go t.updater(ctx)
}
// Refresh update the model now.
func (t *Tree) Refresh(ctx context.Context) {
t.refresh(ctx)
}
// GetNamespace returns the model namespace.
func (t *Tree) GetNamespace() string {
return t.namespace
}
// SetNamespace sets up model namespace.
func (t *Tree) SetNamespace(ns string) {
t.namespace = ns
if t.root == nil {
return
}
t.root.Clear()
}
// SetRefreshRate sets model refresh duration.
func (t *Tree) SetRefreshRate(d time.Duration) {
t.refreshRate = d
}
// ClusterWide checks if resource is scope for all namespaces.
func (t *Tree) ClusterWide() bool {
return client.IsClusterWide(t.namespace)
}
// InNamespace checks if current namespace matches desired namespace.
func (t *Tree) InNamespace(ns string) bool {
return t.namespace == ns
}
// Empty return true if no model data.
func (t *Tree) Empty() bool {
return t.root.IsLeaf()
}
// Peek returns model data.
func (t *Tree) Peek() *xray.TreeNode {
return t.root
}
// Describe describes a given resource.
func (t *Tree) Describe(ctx context.Context, gvr, path string) (string, error) {
meta, err := t.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.Describe(path)
}
// ToYAML returns a resource yaml.
func (t *Tree) ToYAML(ctx context.Context, gvr, path string) (string, error) {
meta, err := t.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, false)
}
func (t *Tree) updater(ctx context.Context) {
defer log.Debug().Msgf("Tree-model canceled -- %q", t.gvr)
rate := initTreeRefreshRate
for {
select {
case <-ctx.Done():
t.root = nil
return
case <-time.After(rate):
rate = t.refreshRate
t.refresh(ctx)
}
}
}
func (t *Tree) refresh(ctx context.Context) {
if !atomic.CompareAndSwapInt32(&t.inUpdate, 0, 1) {
log.Debug().Msgf("Dropping update...")
return
}
defer atomic.StoreInt32(&t.inUpdate, 0)
if err := t.reconcile(ctx); err != nil {
log.Error().Err(err).Msg("Reconcile failed")
t.fireTreeLoadFailed(err)
return
}
}
func (t *Tree) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) {
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
a.Init(factory, t.gvr)
return a.List(ctx, client.CleanseNamespace(t.namespace))
}
func (t *Tree) reconcile(ctx context.Context) error {
meta := t.resourceMeta()
oo, err := t.list(ctx, meta.DAO)
if err != nil {
return err
}
ns := client.CleanseNamespace(t.namespace)
res := t.gvr.R()
root := xray.NewTreeNode(res, res)
ctx = context.WithValue(ctx, xray.KeyParent, root)
if _, ok := meta.TreeRenderer.(*xray.Generic); ok {
table, ok := oo[0].(*metav1beta1.Table)
if !ok {
return fmt.Errorf("expecting a Table but got %T", oo[0])
}
if err := genericTreeHydrate(ctx, ns, table, meta.TreeRenderer); err != nil {
return err
}
} else if err := treeHydrate(ctx, ns, oo, meta.TreeRenderer); err != nil {
return err
}
root.Sort()
if t.query != "" {
t.root = root.Filter(t.query, rxFilter)
}
if t.root == nil || t.root.Diff(root) {
t.root = root
t.fireTreeChanged(t.root)
}
return nil
}
func (t *Tree) resourceMeta() ResourceMeta {
meta, ok := Registry[t.gvr.String()]
if !ok {
meta = ResourceMeta{
DAO: &dao.Table{},
Renderer: &render.Generic{},
}
}
if meta.DAO == nil {
meta.DAO = &dao.Resource{}
}
return meta
}
func (t *Tree) fireTreeChanged(root *xray.TreeNode) {
for _, l := range t.listeners {
l.TreeChanged(root)
}
}
func (t *Tree) fireTreeLoadFailed(err error) {
for _, l := range t.listeners {
l.TreeLoadFailed(err)
}
}
func (t *Tree) getMeta(ctx context.Context, gvr string) (ResourceMeta, error) {
meta := t.resourceMeta()
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, client.NewGVR(gvr))
return meta, nil
}
// ----------------------------------------------------------------------------
// Helpers...
func rxFilter(q, path string) bool {
rx := regexp.MustCompile(`(?i)` + q)
tokens := strings.Split(path, "::")
for _, t := range tokens {
if rx.MatchString(t) {
return true
}
}
return false
}
func treeHydrate(ctx context.Context, ns string, oo []runtime.Object, re TreeRenderer) error {
if re == nil {
return fmt.Errorf("no tree renderer defined for this resource")
}
for _, o := range oo {
if err := re.Render(ctx, ns, o); err != nil {
return err
}
}
return nil
}
func genericTreeHydrate(ctx context.Context, ns string, table *metav1beta1.Table, re TreeRenderer) error {
tre, ok := re.(*xray.Generic)
if !ok {
return fmt.Errorf("expecting xray.Generic renderer but got %T", re)
}
tre.SetTable(ns, table)
// BOZO!! Need table row sorter!!
for _, row := range table.Rows {
if err := tre.Render(ctx, ns, row); err != nil {
return err
}
}
return nil
}