k9s/internal/resource/list.go

365 lines
8.1 KiB
Go

package resource
import (
"fmt"
"reflect"
wa "github.com/derailed/k9s/internal/watch"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
const (
// GetAccess set if resource can be fetched.
GetAccess = 1 << iota
// ListAccess set if resource can be listed.
ListAccess
// EditAccess set if resource can be edited.
EditAccess
// DeleteAccess set if resource can be deleted.
DeleteAccess
// ViewAccess set if resource can be viewed.
ViewAccess
// NamespaceAccess set if namespaced resource.
NamespaceAccess
// DescribeAccess set if resource can be described.
DescribeAccess
// SwitchAccess set if resource can be switched (Context).
SwitchAccess
// CRUDAccess Verbs.
CRUDAccess = GetAccess | ListAccess | DeleteAccess | ViewAccess | EditAccess
// AllVerbsAccess super powers.
AllVerbsAccess = CRUDAccess | NamespaceAccess
)
type (
// RowEvent represents a call for action after a resource reconciliation.
// Tracks whether a resource got added, deleted or updated.
RowEvent struct {
Action watch.EventType
Fields Row
Deltas Row
}
// RowEvents tracks resource update events.
RowEvents map[string]*RowEvent
// TypeMeta represents resource type meta data.
TypeMeta struct {
Name string
Namespaced bool
Group string
Version string
Kind string
Singular string
Plural string
ShortNames []string
}
// TableData tracks a K8s resource for tabular display.
TableData struct {
Header Row
Rows RowEvents
NumCols map[string]bool
Namespace string
}
// List protocol to display and update a collection of resources
List interface {
Data() TableData
Resource() Resource
Namespaced() bool
AllNamespaces() bool
GetNamespace() string
SetNamespace(string)
Reconcile(informer *wa.Informer, path *string) error
GetName() string
Access(flag int) bool
GetAccess() int
SetAccess(int)
SetFieldSelector(string)
SetLabelSelector(string)
HasSelectors() bool
}
// Columnar tracks resources that can be diplayed in a tabular fashion.
Columnar interface {
Header(ns string) Row
Fields(ns string) Row
ExtFields(*TypeMeta)
Name() string
SetPodMetrics(*mv1beta1.PodMetrics)
SetNodeMetrics(*mv1beta1.NodeMetrics)
}
// Columnars a collection of columnars.
Columnars []Columnar
// Row represents a collection of string fields.
Row []string
// Rows represents a collection of rows.
Rows []Row
// Resource represents a tabular Kubernetes resource.
Resource interface {
New(interface{}) Columnar
Get(path string) (Columnar, error)
List(ns string) (Columnars, error)
Delete(path string, cascade, force bool) error
Describe(gvr, pa string) (string, error)
Marshal(pa string) (string, error)
Header(ns string) Row
NumCols(ns string) map[string]bool
SetFieldSelector(string)
SetLabelSelector(string)
GetFieldSelector() string
GetLabelSelector() string
HasSelectors() bool
}
list struct {
namespace, name string
verbs int
resource Resource
cache RowEvents
}
)
func newRowEvent(a watch.EventType, f, d Row) *RowEvent {
return &RowEvent{Action: a, Fields: f, Deltas: d}
}
// NewList returns a new resource list.
func NewList(ns, name string, res Resource, verbs int) *list {
return &list{
namespace: ns,
name: name,
verbs: verbs,
resource: res,
cache: RowEvents{},
}
}
func (l *list) HasSelectors() bool {
return l.resource.HasSelectors()
}
// SetFieldSelector narrows down resource query given fields selection.
func (l *list) SetFieldSelector(s string) {
l.resource.SetFieldSelector(s)
}
// SetLabelSelector narrows down resource query via labels selections.
func (l *list) SetLabelSelector(s string) {
l.resource.SetLabelSelector(s)
}
// Access check access control on a given resource.
func (l *list) Access(f int) bool {
return l.verbs&f == f
}
// Access check access control on a given resource.
func (l *list) GetAccess() int {
return l.verbs
}
// Access check access control on a given resource.
func (l *list) SetAccess(f int) {
l.verbs = f
}
// Namespaced checks if k8s resource is namespaced.
func (l *list) Namespaced() bool {
return l.namespace != NotNamespaced
}
// AllNamespaces checks if this resource spans all namespaces.
func (l *list) AllNamespaces() bool {
return l.namespace == AllNamespaces
}
// GetNamespace associated with the resource.
func (l *list) GetNamespace() string {
if !l.Access(NamespaceAccess) {
l.namespace = NotNamespaced
}
return l.namespace
}
// SetNamespace updates the namespace on the list. Default ns is "" for all
// namespaces.
func (l *list) SetNamespace(n string) {
if !l.Namespaced() {
return
}
if n == AllNamespace {
n = AllNamespaces
}
if l.namespace == n {
return
}
l.cache = RowEvents{}
if l.Access(NamespaceAccess) {
l.namespace = n
if n == AllNamespace {
l.namespace = AllNamespaces
}
}
}
// GetName returns the kubernetes resource name.
func (l *list) GetName() string {
return l.name
}
// Resource returns a resource api connection.
func (l *list) Resource() Resource {
return l.resource
}
// Cache tracks previous resource state.
func (l *list) Data() TableData {
return TableData{
Header: l.resource.Header(l.namespace),
Rows: l.cache,
NumCols: l.resource.NumCols(l.namespace),
Namespace: l.namespace,
}
}
func (l *list) load(informer *wa.Informer, ns string) (Columnars, error) {
rr, err := informer.List(l.name, ns, metav1.ListOptions{
FieldSelector: l.resource.GetFieldSelector(),
LabelSelector: l.resource.GetLabelSelector(),
})
if err != nil {
return nil, err
}
items := make(Columnars, 0, len(rr))
for _, r := range rr {
res, err := l.fetchResource(informer, r, ns)
if err != nil {
return nil, err
}
items = append(items, res)
}
return items, nil
}
func (l *list) fetchResource(informer *wa.Informer, r interface{}, ns string) (Columnar, error) {
var err error
res := l.resource.New(r)
switch o := r.(type) {
case *v1.Node:
fqn := MetaFQN(o.ObjectMeta)
nmx, err := informer.Get(wa.NodeMXIndex, fqn, metav1.GetOptions{})
if err == nil {
res.SetNodeMetrics(nmx.(*mv1beta1.NodeMetrics))
}
case *v1.Pod:
fqn := MetaFQN(o.ObjectMeta)
pmx, err := informer.Get(wa.PodMXIndex, fqn, metav1.GetOptions{})
if err == nil {
res.SetPodMetrics(pmx.(*mv1beta1.PodMetrics))
}
case v1.Container:
pmx, err := informer.Get(wa.PodMXIndex, ns, metav1.GetOptions{})
if err == nil {
res.SetPodMetrics(pmx.(*mv1beta1.PodMetrics))
}
default:
err = fmt.Errorf("No informer matched %s:%s", l.name, ns)
}
return res, err
}
// Reconcile previous vs current state and emits delta events.
func (l *list) Reconcile(informer *wa.Informer, path *string) error {
ns := l.namespace
if path != nil {
ns = *path
}
items, err := l.load(informer, ns)
if err == nil {
l.update(items)
return nil
}
if items, err = l.resource.List(l.namespace); err != nil {
return err
}
l.update(items)
return nil
}
func (l *list) update(items Columnars) {
first := len(l.cache) == 0
kk := make([]string, 0, len(items))
for _, i := range items {
kk = append(kk, i.Name())
ff := i.Fields(l.namespace)
if first {
l.cache[i.Name()] = newRowEvent(New, ff, make(Row, len(ff)))
continue
}
dd := make(Row, len(ff))
a := watch.Added
if evt, ok := l.cache[i.Name()]; ok {
a = computeDeltas(evt, ff[:len(ff)-1], dd)
}
l.cache[i.Name()] = newRowEvent(a, ff, dd)
}
if first {
return
}
l.ensureDeletes(kk)
}
// EnsureDeletes delete items in cache that are no longer valid.
func (l *list) ensureDeletes(kk []string) {
for k := range l.cache {
var found bool
for i, key := range kk {
if k == key {
found = true
kk = append(kk[:i], kk[i+1:]...)
break
}
}
if !found {
delete(l.cache, k)
}
}
}
// Helpers...
func computeDeltas(evt *RowEvent, newRow, deltas Row) watch.EventType {
oldRow := evt.Fields[:len(evt.Fields)-1]
a := Unchanged
if !reflect.DeepEqual(oldRow, newRow) {
for i, field := range oldRow {
if field != newRow[i] {
deltas[i] = field
}
}
a = watch.Modified
}
return a
}