498 lines
10 KiB
Go
498 lines
10 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright Authors of K9s
|
|
|
|
package model1
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/derailed/k9s/internal"
|
|
"github.com/derailed/k9s/internal/client"
|
|
"github.com/derailed/k9s/internal/config"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/sahilm/fuzzy"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
)
|
|
|
|
type (
|
|
// SortFn represent a function that can sort columnar data.
|
|
SortFn func(rows Rows, sortCol SortColumn)
|
|
|
|
// SortColumn represents a sortable column.
|
|
SortColumn struct {
|
|
Name string
|
|
ASC bool
|
|
}
|
|
)
|
|
|
|
const spacer = " "
|
|
|
|
type FilterOpts struct {
|
|
Toast bool
|
|
Filter string
|
|
Invert bool
|
|
}
|
|
|
|
// TableData tracks a K8s resource for tabular display.
|
|
type TableData struct {
|
|
header Header
|
|
rowEvents *RowEvents
|
|
namespace string
|
|
gvr client.GVR
|
|
mx sync.RWMutex
|
|
}
|
|
|
|
// NewTableData returns a new table.
|
|
func NewTableData(gvr client.GVR) *TableData {
|
|
return &TableData{
|
|
gvr: gvr,
|
|
rowEvents: NewRowEvents(10),
|
|
}
|
|
}
|
|
|
|
func NewTableDataFull(gvr client.GVR, ns string, h Header, re *RowEvents) *TableData {
|
|
t := NewTableDataWithRows(gvr, h, re)
|
|
t.namespace = ns
|
|
|
|
return t
|
|
}
|
|
|
|
func NewTableDataWithRows(gvr client.GVR, h Header, re *RowEvents) *TableData {
|
|
t := NewTableData(gvr)
|
|
t.header, t.rowEvents = h, re
|
|
|
|
return t
|
|
}
|
|
|
|
func NewTableDataFromTable(td *TableData) *TableData {
|
|
t := NewTableData(td.gvr)
|
|
t.header = td.header
|
|
t.rowEvents = td.rowEvents
|
|
t.namespace = td.namespace
|
|
|
|
return t
|
|
}
|
|
|
|
func (t *TableData) AddRow(re RowEvent) {
|
|
t.rowEvents.Add(re)
|
|
}
|
|
|
|
func (t *TableData) SetRow(idx int, re RowEvent) {
|
|
t.rowEvents.Set(idx, re)
|
|
}
|
|
|
|
func (t *TableData) FindRow(id string) (RowEvent, bool) {
|
|
return t.rowEvents.Get(id)
|
|
}
|
|
|
|
func (t *TableData) RowAt(idx int) (RowEvent, bool) {
|
|
return t.rowEvents.At(idx)
|
|
}
|
|
|
|
func (t *TableData) RowsRange(f ReRangeFn) {
|
|
t.rowEvents.Range(f)
|
|
}
|
|
|
|
func (t *TableData) Sort(sc SortColumn) {
|
|
col, idx := t.HeadCol(sc.Name, false)
|
|
if idx < 0 {
|
|
return
|
|
}
|
|
t.rowEvents.Sort(
|
|
t.GetNamespace(),
|
|
idx,
|
|
col.Time,
|
|
col.MX,
|
|
col.Capacity,
|
|
sc.ASC,
|
|
)
|
|
}
|
|
|
|
func (t *TableData) Header() Header {
|
|
return t.header
|
|
}
|
|
|
|
// HeaderCount returns the number of header cols.
|
|
func (t *TableData) HeaderCount() int {
|
|
t.mx.RLock()
|
|
defer t.mx.RUnlock()
|
|
|
|
return len(t.header)
|
|
}
|
|
|
|
func (t *TableData) HeadCol(n string, w bool) (HeaderColumn, int) {
|
|
idx, ok := t.header.IndexOf(n, w)
|
|
if !ok {
|
|
return HeaderColumn{}, -1
|
|
}
|
|
|
|
return t.header[idx], idx
|
|
}
|
|
|
|
func (t *TableData) Filter(f FilterOpts) *TableData {
|
|
td := NewTableDataFromTable(t)
|
|
|
|
if f.Toast {
|
|
td.rowEvents = t.filterToast()
|
|
}
|
|
if f.Filter == "" || internal.IsLabelSelector(f.Filter) {
|
|
return td
|
|
}
|
|
if f, ok := internal.IsFuzzySelector(f.Filter); ok {
|
|
td.rowEvents = t.fuzzyFilter(f)
|
|
return td
|
|
}
|
|
rr, err := t.rxFilter(f.Filter, internal.IsInverseSelector(f.Filter))
|
|
if err == nil {
|
|
td.rowEvents = rr
|
|
} else {
|
|
log.Error().Err(err).Msg("rx filter failed")
|
|
}
|
|
|
|
return td
|
|
}
|
|
|
|
func (t *TableData) rxFilter(q string, inverse bool) (*RowEvents, error) {
|
|
if inverse {
|
|
q = q[1:]
|
|
}
|
|
rx, err := regexp.Compile(`(?i)(` + q + `)`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid rx filter %q: %w", q, err)
|
|
}
|
|
|
|
ageIndex, ok := t.header.IndexOf("AGE", true)
|
|
|
|
rr := NewRowEvents(t.RowCount() / 2)
|
|
t.rowEvents.Range(func(_ int, re RowEvent) bool {
|
|
ff := re.Row.Fields
|
|
if ok && ageIndex+1 <= len(ff) {
|
|
ff = append(ff[0:ageIndex], ff[ageIndex+1:]...)
|
|
}
|
|
fields := strings.Join(ff, spacer)
|
|
if (inverse && !rx.MatchString(fields)) ||
|
|
((!inverse) && rx.MatchString(fields)) {
|
|
rr.Add(re)
|
|
}
|
|
return true
|
|
})
|
|
|
|
return rr, nil
|
|
}
|
|
|
|
func (t *TableData) fuzzyFilter(q string) *RowEvents {
|
|
q = strings.TrimSpace(q)
|
|
ss := make([]string, 0, t.RowCount()/2)
|
|
t.rowEvents.Range(func(_ int, re RowEvent) bool {
|
|
ss = append(ss, re.Row.ID)
|
|
return true
|
|
})
|
|
|
|
mm := fuzzy.Find(q, ss)
|
|
rr := NewRowEvents(t.RowCount() / 2)
|
|
for _, m := range mm {
|
|
re, ok := t.rowEvents.At(m.Index)
|
|
if !ok {
|
|
log.Error().Msgf("unable to find event for index in fuzzfilter: %d", m.Index)
|
|
continue
|
|
}
|
|
rr.Add(re)
|
|
}
|
|
|
|
return rr
|
|
}
|
|
|
|
func (t *TableData) filterToast() *RowEvents {
|
|
idx, ok := t.header.IndexOf("VALID", true)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
rr := NewRowEvents(10)
|
|
t.rowEvents.Range(func(_ int, re RowEvent) bool {
|
|
if re.Row.Fields[idx] != "" {
|
|
rr.Add(re)
|
|
}
|
|
return true
|
|
})
|
|
|
|
return rr
|
|
}
|
|
|
|
func (t *TableData) GetNamespace() string {
|
|
t.mx.RLock()
|
|
defer t.mx.RUnlock()
|
|
|
|
return t.namespace
|
|
}
|
|
|
|
func (t *TableData) Reset(ns string) {
|
|
t.mx.Lock()
|
|
{
|
|
t.namespace = ns
|
|
}
|
|
t.mx.Unlock()
|
|
|
|
t.Clear()
|
|
}
|
|
|
|
func (t *TableData) Reconcile(ctx context.Context, r Renderer, oo []runtime.Object) error {
|
|
var rows Rows
|
|
|
|
if len(oo) > 0 {
|
|
if r.IsGeneric() {
|
|
table, ok := oo[0].(*metav1.Table)
|
|
if !ok {
|
|
return fmt.Errorf("expecting a meta table but got %T", oo[0])
|
|
}
|
|
rows = make(Rows, len(table.Rows))
|
|
if err := GenericHydrate(t.namespace, table, rows, r); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
rows = make(Rows, len(oo))
|
|
if err := Hydrate(t.namespace, oo, rows, r); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Update(rows)
|
|
t.SetHeader(t.namespace, r.Header(t.namespace))
|
|
if t.HeaderCount() == 0 {
|
|
return fmt.Errorf("fail to list resource %s", t.gvr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Empty checks if there are no entries.
|
|
func (t *TableData) Empty() bool {
|
|
t.mx.RLock()
|
|
defer t.mx.RUnlock()
|
|
|
|
return t.rowEvents.Empty()
|
|
}
|
|
|
|
func (t *TableData) SetRowEvents(re *RowEvents) {
|
|
t.rowEvents = re
|
|
}
|
|
|
|
func (t *TableData) GetRowEvents() *RowEvents {
|
|
return t.rowEvents
|
|
}
|
|
|
|
// RowCount returns the number of rows.
|
|
func (t *TableData) RowCount() int {
|
|
t.mx.RLock()
|
|
defer t.mx.RUnlock()
|
|
|
|
return t.rowEvents.Len()
|
|
}
|
|
|
|
// IndexOfHeader return the index of the header.
|
|
func (t *TableData) IndexOfHeader(h string) (int, bool) {
|
|
return t.header.IndexOf(h, false)
|
|
}
|
|
|
|
// Labelize prints out specific label columns.
|
|
func (t *TableData) Labelize(labels []string) *TableData {
|
|
idx, ok := t.header.IndexOf("LABELS", true)
|
|
if !ok {
|
|
return t
|
|
}
|
|
cols := []int{0, 1}
|
|
if client.IsNamespaced(t.namespace) {
|
|
cols = cols[1:]
|
|
}
|
|
data := TableData{
|
|
namespace: t.namespace,
|
|
header: t.header.Labelize(cols, idx, t.rowEvents),
|
|
}
|
|
data.rowEvents = t.rowEvents.Labelize(cols, idx, labels)
|
|
|
|
return &data
|
|
}
|
|
|
|
// Customize returns a new model with customized column layout.
|
|
func (t *TableData) Customize(vs *config.ViewSetting, sc SortColumn, manual, wide bool) (*TableData, SortColumn) {
|
|
if vs.IsBlank() {
|
|
if sc.Name != "" {
|
|
return t, sc
|
|
}
|
|
psc, err := t.sortCol(vs)
|
|
if err == nil {
|
|
return t, psc
|
|
}
|
|
return t, sc
|
|
}
|
|
|
|
cols := vs.Columns
|
|
cdata := TableData{
|
|
gvr: t.gvr,
|
|
namespace: t.namespace,
|
|
header: t.header.Customize(cols, wide),
|
|
}
|
|
ids := t.header.MapIndices(cols, wide)
|
|
cdata.rowEvents = t.rowEvents.Customize(ids)
|
|
if manual || vs == nil {
|
|
return &cdata, sc
|
|
}
|
|
psc, err := cdata.sortCol(vs)
|
|
if err != nil {
|
|
return &cdata, sc
|
|
}
|
|
|
|
return &cdata, psc
|
|
}
|
|
|
|
func (t *TableData) sortCol(vs *config.ViewSetting) (SortColumn, error) {
|
|
var psc SortColumn
|
|
|
|
if t.HeaderCount() == 0 {
|
|
return psc, errors.New("no header found")
|
|
}
|
|
name, order, _ := vs.SortCol()
|
|
if _, ok := t.header.IndexOf(name, false); ok {
|
|
psc.Name, psc.ASC = name, order
|
|
return psc, nil
|
|
}
|
|
if client.IsAllNamespaces(t.GetNamespace()) {
|
|
if _, ok := t.header.IndexOf("NAMESPACE", false); ok {
|
|
psc.Name = "NAMESPACE"
|
|
} else if _, ok := t.header.IndexOf("NAME", false); ok {
|
|
psc.Name = "NAME"
|
|
}
|
|
} else {
|
|
if _, ok := t.header.IndexOf("NAME", false); ok {
|
|
psc.Name = "NAME"
|
|
} else {
|
|
psc.Name = t.header[0].Name
|
|
}
|
|
}
|
|
psc.ASC = true
|
|
|
|
return psc, nil
|
|
}
|
|
|
|
// Clear clears out the entire table.
|
|
func (t *TableData) Clear() {
|
|
t.mx.Lock()
|
|
defer t.mx.Unlock()
|
|
|
|
t.header = t.header.Clear()
|
|
t.rowEvents.Clear()
|
|
}
|
|
|
|
// Clone returns a copy of the table.
|
|
func (t *TableData) Clone() *TableData {
|
|
t.mx.RLock()
|
|
defer t.mx.RUnlock()
|
|
|
|
return &TableData{
|
|
header: t.header.Clone(),
|
|
rowEvents: t.rowEvents.Clone(),
|
|
namespace: t.namespace,
|
|
}
|
|
}
|
|
|
|
func (t *TableData) ColumnNames(w bool) []string {
|
|
t.mx.RLock()
|
|
defer t.mx.RUnlock()
|
|
|
|
return t.header.ColumnNames(w)
|
|
}
|
|
|
|
// GetHeader returns table header.
|
|
func (t *TableData) GetHeader() Header {
|
|
t.mx.RLock()
|
|
defer t.mx.RUnlock()
|
|
|
|
return t.header
|
|
}
|
|
|
|
// SetHeader sets table header.
|
|
func (t *TableData) SetHeader(ns string, h Header) {
|
|
t.mx.Lock()
|
|
defer t.mx.Unlock()
|
|
|
|
t.namespace, t.header = ns, h
|
|
}
|
|
|
|
// Update computes row deltas and update the table data.
|
|
func (t *TableData) Update(rows Rows) {
|
|
empty := t.Empty()
|
|
kk := make(map[string]struct{}, len(rows))
|
|
var blankDelta DeltaRow
|
|
t.mx.Lock()
|
|
{
|
|
for _, row := range rows {
|
|
kk[row.ID] = struct{}{}
|
|
if empty {
|
|
t.rowEvents.Add(NewRowEvent(EventAdd, row))
|
|
continue
|
|
}
|
|
if index, ok := t.rowEvents.FindIndex(row.ID); ok {
|
|
ev, ok := t.rowEvents.At(index)
|
|
if !ok {
|
|
continue
|
|
}
|
|
delta := NewDeltaRow(ev.Row, row, t.header)
|
|
if delta.IsBlank() {
|
|
ev.Kind, ev.Deltas, ev.Row = EventUnchanged, blankDelta, row
|
|
t.rowEvents.Set(index, ev)
|
|
} else {
|
|
t.rowEvents.Set(index, NewRowEventWithDeltas(row, delta))
|
|
}
|
|
continue
|
|
}
|
|
t.rowEvents.Add(NewRowEvent(EventAdd, row))
|
|
}
|
|
}
|
|
t.mx.Unlock()
|
|
|
|
if !empty {
|
|
t.Delete(kk)
|
|
}
|
|
}
|
|
|
|
// Delete removes items in cache that are no longer valid.
|
|
func (t *TableData) Delete(newKeys map[string]struct{}) {
|
|
t.mx.Lock()
|
|
{
|
|
victims := make([]string, 0, 10)
|
|
t.rowEvents.Range(func(_ int, e RowEvent) bool {
|
|
if _, ok := newKeys[e.Row.ID]; !ok {
|
|
victims = append(victims, e.Row.ID)
|
|
} else {
|
|
delete(newKeys, e.Row.ID)
|
|
}
|
|
return true
|
|
})
|
|
for _, id := range victims {
|
|
if err := t.rowEvents.Delete(id); err != nil {
|
|
log.Error().Err(err).Msgf("table delete failed: %q", id)
|
|
}
|
|
}
|
|
}
|
|
t.mx.Unlock()
|
|
}
|
|
|
|
// Diff checks if two tables are equal.
|
|
func (t *TableData) Diff(t2 *TableData) bool {
|
|
if t2 == nil || t.namespace != t2.namespace || t.header.Diff(t2.header) {
|
|
return true
|
|
}
|
|
idx, ok := t.header.IndexOf("AGE", true)
|
|
if !ok {
|
|
idx = -1
|
|
}
|
|
return t.rowEvents.Diff(t2.rowEvents, idx)
|
|
}
|