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, error) 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 }