From c26c80e170375a9f3eb0cdc5af26ca15147078e2 Mon Sep 17 00:00:00 2001 From: derailed Date: Sat, 28 Dec 2019 12:22:22 -0700 Subject: [PATCH] checkpoint --- go.mod | 1 + go.sum | 1 + internal/dao/port_forwarder.go | 4 +- internal/dao/registry.go | 3 +- internal/keys.go | 29 +-- internal/model/alias_test.go | 3 + internal/model/container_test.go | 7 +- internal/model/generic.go | 9 +- internal/model/job.go | 12 +- internal/model/node.go | 2 +- internal/model/reconcile.go | 6 +- internal/model/table.go | 185 ++++++++++++++++++ internal/model/types.go | 3 + internal/render/color.go | 38 ++++ internal/render/crd.go | 11 ++ internal/render/generic.go | 4 - internal/render/portforward.go | 14 +- internal/render/row.go | 90 +++------ internal/render/{event.go => row_event.go} | 78 ++++---- .../{event_test.go => row_event_test.go} | 0 internal/render/row_header.go | 73 +++++++ internal/render/row_test.go | 18 +- internal/render/table.go | 75 +++++++ internal/ui/app.go | 4 +- internal/ui/cmd.go | 2 +- internal/ui/cmd_buff.go | 5 + internal/ui/select_table.go | 84 ++++---- internal/ui/table.go | 32 +-- internal/ui/table_test.go | 25 ++- internal/view/alias_test.go | 64 +++++- internal/view/app.go | 4 - internal/view/browser.go | 130 ++++++------ internal/view/command.go | 8 +- internal/view/container.go | 6 +- internal/view/container_test.go | 2 +- internal/view/context_test.go | 2 +- internal/view/cronjob.go | 6 +- internal/view/details.go | 4 +- internal/view/dp_test.go | 2 +- internal/view/ds_test.go | 2 +- internal/view/group.go | 28 +-- internal/view/help.go | 14 +- internal/view/help_test.go | 2 +- internal/view/job.go | 5 +- internal/view/node.go | 3 +- internal/view/ns.go | 9 +- internal/view/ns_test.go | 2 +- internal/view/pod_test.go | 2 +- internal/view/port_forward.go | 26 +-- internal/view/rbac_test.go | 2 +- internal/view/registrar.go | 27 +++ internal/view/screen_dump_test.go | 2 +- internal/view/secret_test.go | 2 +- internal/view/sts_test.go | 2 +- internal/view/subject.go | 77 -------- internal/view/subject_test.go | 17 -- internal/view/svc_test.go | 2 +- internal/view/table.go | 24 ++- internal/view/table_int_test.go | 65 +++--- internal/view/user.go | 28 +-- internal/watch/factory.go | 84 ++++---- internal/watch/forwarders.go | 8 +- 62 files changed, 934 insertions(+), 545 deletions(-) create mode 100644 internal/model/table.go create mode 100644 internal/render/color.go rename internal/render/{event.go => row_event.go} (83%) rename internal/render/{event_test.go => row_event_test.go} (100%) create mode 100644 internal/render/row_header.go delete mode 100644 internal/view/subject.go delete mode 100644 internal/view/subject_test.go diff --git a/go.mod b/go.mod index 2858f8e8..8049378a 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( gopkg.in/yaml.v2 v2.2.4 gotest.tools v2.2.0+incompatible k8s.io/api v0.0.0 + k8s.io/apiextensions-apiserver v0.0.0 k8s.io/apimachinery v0.0.0 k8s.io/cli-runtime v0.0.0 k8s.io/client-go v0.0.0 diff --git a/go.sum b/go.sum index 08b844d3..e51bc4c1 100644 --- a/go.sum +++ b/go.sum @@ -538,6 +538,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= k8s.io/api v0.0.0-20190918155943-95b840bb6a1f h1:8FRUST8oUkEI45WYKyD8ed7Ad0Kg5v11zHyPkEVb2xo= k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= +k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783 h1:V6ndwCPoao1yZ52agqOKaUAl7DYWVGiXjV7ePA2i610= k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY= k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655 h1:CS1tBQz3HOXiseWZu6ZicKX361CZLT97UFnnPx0aqBw= k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go index 18681883..20dc43b7 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -66,7 +66,7 @@ func (p *PortForwarder) Ports() []string { // Path returns the pod resource path. func (p *PortForwarder) Path() string { - return p.path + return p.path + ":" + p.container } // Container returns the targetes container. @@ -76,7 +76,7 @@ func (p *PortForwarder) Container() string { // Stop terminates a port forard func (p *PortForwarder) Stop() { - log.Debug().Msgf("<<< Stopping port forward %q %v", p.path, p.ports) + log.Debug().Msgf("<<< Stopping PortForward %q %v", p.path, p.ports) p.active = false close(p.stopChan) } diff --git a/internal/dao/registry.go b/internal/dao/registry.go index ed74738e..2becdf19 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -187,7 +187,8 @@ func loadPreferred(f Factory, m ResourceMetas) error { func loadCRDs(f Factory, m ResourceMetas) error { oo, err := f.List("apiextensions.k8s.io/v1beta1/customresourcedefinitions", "", labels.Everything()) if err != nil { - return err + log.Error().Err(err).Msgf("Fail CRDs load") + return nil } f.WaitForCacheSync() diff --git a/internal/keys.go b/internal/keys.go index 2304a624..63825175 100644 --- a/internal/keys.go +++ b/internal/keys.go @@ -6,18 +6,19 @@ type ContextKey string // A collection of context keys. const ( KeyFactory ContextKey = "factory" - KeyLabels ContextKey = "labels" - KeyFields ContextKey = "fields" - KeyTable ContextKey = "table" - KeyDir ContextKey = "dir" - KeyPath ContextKey = "path" - KeySubject ContextKey = "subject" - KeyGVR ContextKey = "gvr" - KeyForwards ContextKey = "forwards" - KeyContainers ContextKey = "containers" - KeyBenchCfg ContextKey = "benchcfg" - KeyAliases ContextKey = "aliases" - KeyUID ContextKey = "uid" - KeySubjectKind ContextKey = "subjectKind" - KeySubjectName ContextKey = "subjectName" + KeyLabels = "labels" + KeyFields = "fields" + KeyTable = "table" + KeyDir = "dir" + KeyPath = "path" + KeySubject = "subject" + KeyGVR = "gvr" + KeyForwards = "forwards" + KeyContainers = "containers" + KeyBenchCfg = "benchcfg" + KeyAliases = "aliases" + KeyUID = "uid" + KeySubjectKind = "subjectKind" + KeySubjectName = "subjectName" + KeyNamespace = "namespace" ) diff --git a/internal/model/alias_test.go b/internal/model/alias_test.go index d05fe68d..aa886515 100644 --- a/internal/model/alias_test.go +++ b/internal/model/alias_test.go @@ -74,6 +74,9 @@ func (f testFactory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object func (f testFactory) ForResource(ns, gvr string) informers.GenericInformer { return nil } +func (f testFactory) CanForResource(ns, gvr string, verbs ...string) (informers.GenericInformer, error) { + return nil, nil +} func (f testFactory) WaitForCacheSync() {} func (f testFactory) Forwarders() watch.Forwarders { return nil diff --git a/internal/model/container_test.go b/internal/model/container_test.go index bbf2c222..018a5733 100644 --- a/internal/model/container_test.go +++ b/internal/model/container_test.go @@ -63,8 +63,11 @@ func (f podFactory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, return nil, nil } func (f podFactory) ForResource(ns, gvr string) informers.GenericInformer { return nil } -func (f podFactory) WaitForCacheSync() {} -func (f podFactory) Forwarders() watch.Forwarders { return nil } +func (f podFactory) CanForResource(ns, gvr string, verbs ...string) (informers.GenericInformer, error) { + return nil, nil +} +func (f podFactory) WaitForCacheSync() {} +func (f podFactory) Forwarders() watch.Forwarders { return nil } func makePodFactory() model.Factory { return podFactory{} diff --git a/internal/model/generic.go b/internal/model/generic.go index e4bd360c..0bbc4d63 100644 --- a/internal/model/generic.go +++ b/internal/model/generic.go @@ -32,7 +32,10 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { }(time.Now()) // Ensures the factory is tracking this resource - _ = g.factory.ForResource(g.namespace, g.gvr) + _, err := g.factory.CanForResource(g.namespace, g.gvr) + if err != nil { + return nil, err + } gvr := client.GVR(g.gvr) fcodec, codec := g.codec(gvr.AsGV()) @@ -49,7 +52,9 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { Resource(gvr.ToR()). VersionedParams(&metav1beta1.TableOptions{}, codec). Do().Get() - + if err != nil { + return nil, err + } table, ok := o.(*metav1beta1.Table) if !ok { return nil, fmt.Errorf("expecting table but got %T", o) diff --git a/internal/model/job.go b/internal/model/job.go index adac093b..b507f5e8 100644 --- a/internal/model/job.go +++ b/internal/model/job.go @@ -29,6 +29,7 @@ func (c *Job) List(ctx context.Context) ([]runtime.Object, error) { return nil, errors.New("no cronjob path found in context") } + log.Debug().Msgf("Listing jobs %q %q--%q", c.gvr, uid, path) oo, err := c.Resource.List(ctx) if err != nil { return nil, err @@ -45,17 +46,12 @@ func (c *Job) List(ctx context.Context) ([]runtime.Object, error) { if err != nil { return nil, err } + log.Debug().Msgf("Looking at job %q -- %q", job.Name, cronName) if !isNamedAfter(cronName, job.Name) { continue } - id, ok := job.Spec.Selector.MatchLabels["controller-uid"] - if !ok { - continue - } - if isControlledBy(uid, id) { - log.Debug().Msgf("Job %s -- %#v -- %q -- %q", job.Name, job.Labels, uid, path) - jj = append(jj, j) - } + log.Debug().Msgf("GOT Job %s -- %#v -- %q -- %q", job.Name, job.Labels, uid, path) + jj = append(jj, j) } return jj, nil diff --git a/internal/model/node.go b/internal/model/node.go index d4409516..f93d1d4f 100644 --- a/internal/model/node.go +++ b/internal/model/node.go @@ -31,7 +31,7 @@ func (n *Node) List(_ context.Context) ([]runtime.Object, error) { oo := make([]runtime.Object, len(nn.Items)) for i, n := range nn.Items { - o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(n) + o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&n) if err != nil { return nil, err } diff --git a/internal/model/reconcile.go b/internal/model/reconcile.go index 0f3b7e12..a1fe7e8c 100644 --- a/internal/model/reconcile.go +++ b/internal/model/reconcile.go @@ -19,12 +19,12 @@ func Reconcile(ctx context.Context, table render.TableData, gvr client.GVR) (ren path, ok := ctx.Value(internal.KeyPath).(string) if !ok { - return table, fmt.Errorf("no path specified for %s", gvr) + return table, fmt.Errorf("no path in context for %s", gvr) } log.Debug().Msgf("Reconcile %q in ns %q with path %q", gvr, table.Namespace, path) factory, ok := ctx.Value(internal.KeyFactory).(Factory) if !ok { - return table, fmt.Errorf("no factory found for %s", gvr) + return table, fmt.Errorf("no Factory in context for %s", gvr) } m, ok := Registry[string(gvr)] if !ok { @@ -39,7 +39,6 @@ func Reconcile(ctx context.Context, table render.TableData, gvr client.GVR) (ren } m.Model.Init(table.Namespace, string(gvr), factory) - table.Header = m.Renderer.Header(table.Namespace) oo, err := m.Model.List(ctx) if err != nil { return table, err @@ -51,6 +50,7 @@ func Reconcile(ctx context.Context, table render.TableData, gvr client.GVR) (ren return table, err } update(&table, rows) + table.Header = m.Renderer.Header(table.Namespace) log.Debug().Msgf("Table returned [%d] events", len(table.RowEvents)) return table, nil diff --git a/internal/model/table.go b/internal/model/table.go new file mode 100644 index 00000000..6e4283bf --- /dev/null +++ b/internal/model/table.go @@ -0,0 +1,185 @@ +package model + +import ( + "context" + "fmt" + "sync/atomic" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" +) + +type TableListener interface { + TableDataChanged(render.TableData) + TableLoadFailed(error) +} + +type Table struct { + gvr string + namespace string + data render.TableData + listeners []TableListener + inUpdate int32 + refreshRate time.Duration +} + +// NewTable returns a new table model. +func NewTable(gvr string) *Table { + return &Table{ + gvr: gvr, + data: render.TableData{}, + refreshRate: 2 * time.Second, + } +} + +// Start initiates model updates. +func (t *Table) Start(ctx context.Context) { + t.Refresh(ctx) + go t.updater(ctx) +} + +// Refresh update the model now. +func (t *Table) Refresh(ctx context.Context) { + t.refresh(ctx) +} + +// GetNamespace returns the model namespace. +func (t *Table) GetNamespace() string { + return t.namespace +} + +// SetNamespace sets up model namespace. +func (t *Table) SetNamespace(ns string) { + t.namespace = ns + t.data.Clear() +} + +// SetRefreshRate sets model refresh duration. +func (t *Table) SetRefreshRate(d time.Duration) { + t.refreshRate = d +} + +// ClusterWide checks if resource is scope for all namespaces. +func (t *Table) ClusterWide() bool { + return t.namespace == render.AllNamespaces +} + +// InNamespace checks if current namespace matches desired namespace. +func (t *Table) InNamespace(ns string) bool { + return t.namespace == ns +} + +// Empty return true if no model data. +func (t *Table) Empty() bool { + return len(t.data.RowEvents) == 0 +} + +// Peek returns model data. +func (t *Table) Peek() render.TableData { + return t.data +} + +func (t *Table) updater(ctx context.Context) { + defer log.Debug().Msgf("Model canceled -- %q", t.gvr) + for { + select { + case <-ctx.Done(): + return + case <-time.After(t.refreshRate): + t.refresh(ctx) + } + } +} + +func (t *Table) 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.fireTableLoadFailed(err) + } + t.fireTableChanged(t.data) +} + +// AddListener adds a new model listener. +func (t *Table) AddListener(l TableListener) { + t.listeners = append(t.listeners, l) + t.fireTableChanged(t.data) +} + +// RemoveListener delete a listener from the list. +func (t *Table) RemoveListener(l TableListener) { + 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:]...) + } +} + +func (t *Table) fireTableChanged(data render.TableData) { + for _, l := range t.listeners { + l.TableDataChanged(data) + } +} + +func (t *Table) fireTableLoadFailed(err error) { + for _, l := range t.listeners { + l.TableLoadFailed(err) + } +} + +func (t *Table) reconcile(ctx context.Context) error { + defer func(t time.Time) { + log.Debug().Msgf("RECONCILE elapsed: %v", time.Since(t)) + }(time.Now()) + + path, ok := ctx.Value(internal.KeyPath).(string) + if !ok { + return fmt.Errorf("no path in context for %s", t.gvr) + } + + log.Debug().Msgf("Reconcile %q in %q:%q", t.gvr, t.namespace, path) + factory, ok := ctx.Value(internal.KeyFactory).(Factory) + if !ok { + return fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory)) + } + m, ok := Registry[string(t.gvr)] + if !ok { + log.Warn().Msgf("Resource %s not found in registry. Going generic!", t.gvr) + m = ResourceMeta{ + Model: &Generic{}, + Renderer: &render.Generic{}, + } + } + + if m.Model == nil { + m.Model = &Resource{} + } + m.Model.Init(t.namespace, string(t.gvr), factory) + oo, err := m.Model.List(ctx) + if err != nil { + return err + } + + rows := make(render.Rows, len(oo)) + if err := m.Model.Hydrate(oo, rows, m.Renderer); err != nil { + return err + } + t.data.Update(rows) + t.data.Namespace, t.data.Header = t.namespace, m.Renderer.Header(t.namespace) + + log.Debug().Msgf("Table returned [%d] events", len(t.data.RowEvents)) + return nil +} diff --git a/internal/model/types.go b/internal/model/types.go index 0387685a..f0acad0d 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -82,6 +82,9 @@ type Factory interface { // ForResource fetch an informer for a given resource. ForResource(ns, gvr string) informers.GenericInformer + // CanForResource fetch an informer for a given resource. + CanForResource(ns, gvr string, verbs ...string) (informers.GenericInformer, error) + // WaitForCacheSync synchronize the cache. WaitForCacheSync() diff --git a/internal/render/color.go b/internal/render/color.go new file mode 100644 index 00000000..dba824b0 --- /dev/null +++ b/internal/render/color.go @@ -0,0 +1,38 @@ +package render + +import "github.com/gdamore/tcell" + +var ( + // ModColor row modified color. + ModColor tcell.Color + // AddColor row added color. + AddColor tcell.Color + // ErrColor row err color. + ErrColor tcell.Color + // StdColor row default color. + StdColor tcell.Color + // HighlightColor row highlight color. + HighlightColor tcell.Color + // KillColor row deleted color. + KillColor tcell.Color + // CompletedColor row completed color. + CompletedColor tcell.Color +) + +// ColorerFunc represents a resource row colorer. +type ColorerFunc func(ns string, evt RowEvent) tcell.Color + +// DefaultColorer set the default table row colors. +func DefaultColorer(ns string, evt RowEvent) tcell.Color { + var col = StdColor + switch evt.Kind { + case EventAdd: + col = AddColor + case EventUpdate: + col = ModColor + case EventDelete: + col = KillColor + } + + return col +} diff --git a/internal/render/crd.go b/internal/render/crd.go index 2218b25a..f10716ab 100644 --- a/internal/render/crd.go +++ b/internal/render/crd.go @@ -5,8 +5,10 @@ import ( "time" "github.com/rs/zerolog/log" + // ext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + // "k8s.io/apimachinery/pkg/runtime" ) // CustomResourceDefinition renders a K8s CustomResourceDefinition to screen. @@ -32,6 +34,15 @@ func (CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error { return fmt.Errorf("Expected CustomResourceDefinition, but got %T", o) } + // BOZO!! + // log.Debug().Msgf("CRDO %#v", crd) + // var cr ext.CustomResourceDefinition + // err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr) + // if err != nil { + // return err + // } + // log.Debug().Msgf("\n%#v", cr) + meta, ok := crd.Object["metadata"].(map[string]interface{}) if !ok { return fmt.Errorf("expecting an interface map but got %T", crd.Object["metadata"]) diff --git a/internal/render/generic.go b/internal/render/generic.go index 3ac4f362..2b755718 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -6,7 +6,6 @@ import ( "fmt" "strings" - "github.com/rs/zerolog/log" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" ) @@ -38,7 +37,6 @@ func (g *Generic) Header(ns string) HeaderRow { h = append(h, Header{Name: strings.ToUpper(c.Name)}) } - log.Debug().Msgf("Generic Header %#v", h) return h } @@ -69,12 +67,10 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { r.ID = FQN(rns, r.ID) index++ } - for _, c := range row.Cells { r.Fields[index] = fmt.Sprintf("%v", c) index++ } - log.Debug().Msgf("Generic row %#v", r) return nil } diff --git a/internal/render/portforward.go b/internal/render/portforward.go index 4714178e..aad1b8c7 100644 --- a/internal/render/portforward.go +++ b/internal/render/portforward.go @@ -41,7 +41,7 @@ func (PortForward) ColorerFunc() ColorerFunc { func (PortForward) Header(ns string) HeaderRow { return HeaderRow{ Header{Name: "NAMESPACE"}, - Header{Name: "NAME"}, + Header{Name: "POD"}, Header{Name: "CONTAINER"}, Header{Name: "PORTS"}, Header{Name: "URL"}, @@ -59,12 +59,12 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error { } ports := strings.Split(pf.Ports()[0], ":") - ns, na := Namespaced(pf.Path()) + ns, n := Namespaced(pf.Path()) r.ID = pf.Path() r.Fields = Fields{ ns, - na, + trimContainer(n), pf.Container(), strings.Join(pf.Ports(), ","), UrlFor(pf.Config.Host, pf.Config.Path, ports[0]), @@ -78,6 +78,14 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error { // Helpers... +func trimContainer(n string) string { + tokens := strings.Split(n, ":") + if len(tokens) == 0 { + return n + } + return tokens[0] +} + // UrlFor computes fq url for a given benchmark configuration. func UrlFor(host, path, port string) string { if host == "" { diff --git a/internal/render/row.go b/internal/render/row.go index d15e6b95..d9d72ada 100644 --- a/internal/render/row.go +++ b/internal/render/row.go @@ -7,65 +7,32 @@ import ( "vbom.ml/util/sortorder" ) -const ageCol = "AGE" - // Fields represents a collection of row fields. type Fields []string +// Clone returns a copy of the fields. +func (f Fields) Clone() Fields { + cp := make(Fields, len(f)) + for i, v := range f { + cp[i] = v + } + return cp +} + +// ---------------------------------------------------------------------------- + // Row represents a colllection of columns. type Row struct { ID string Fields Fields } -// Rows represents a collection of rows. -type Rows []Row - -// Header represent a table header -type Header struct { - Name string - Align int - Decorator DecoratorFunc -} - -// HeaderRow represents a table header. -type HeaderRow []Header - -// Columns return header row columns as strings. -func (h HeaderRow) Columns() []string { - cc := make([]string, len(h)) - for i, c := range h { - cc[i] = c.Name - } - - return cc -} - -// HasAge returns true if table has an age column. -func (h HeaderRow) HasAge() bool { - for _, r := range h { - if r.Name == ageCol { - return true - } - } - - return false -} - -func (h HeaderRow) AgeCol(col int) bool { - if !h.HasAge() { - return false - } - return col == len(h)-1 -} - -// RowSorter sorts rows. -type RowSorter struct { - Rows Rows - Index int - Asc bool +// NewRow returns a new row with initialized fields. +func NewRow(cols int) Row { + return Row{Fields: make([]string, cols)} } +// Clone copies a row. func (r Row) Clone() Row { return Row{ ID: r.ID, @@ -73,14 +40,10 @@ func (r Row) Clone() Row { } } -func (f Fields) Clone() Fields { - res := make(Fields, len(f)) - for i, f := range f { - res[i] = f - } +// ---------------------------------------------------------------------------- - return res -} +// Rows represents a collection of rows. +type Rows []Row // Delete removes an element by id. func (rr Rows) Delete(id string) Rows { @@ -99,11 +62,6 @@ func (rr Rows) Delete(id string) Rows { return append(rr[:idx], rr[idx+1:]...) } -// NewRow returns a new row with initialized fields. -func NewRow(cols int) Row { - return Row{Fields: make([]string, cols)} -} - func (rr Rows) Upsert(r Row) Rows { idx, ok := rr.Find(r.ID) if !ok { @@ -131,6 +89,15 @@ func (rr Rows) Sort(col int, asc bool) { sort.Sort(t) } +// ---------------------------------------------------------------------------- + +// RowSorter sorts rows. +type RowSorter struct { + Rows Rows + Index int + Asc bool +} + func (s RowSorter) Len() int { return len(s.Rows) } @@ -143,6 +110,9 @@ func (s RowSorter) Less(i, j int) bool { return Less(s.Asc, s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index]) } +// ---------------------------------------------------------------------------- +// Helpers... + func Less(asc bool, c1, c2 string) bool { if o, ok := isDurationSort(asc, c1, c2); ok { return o diff --git a/internal/render/event.go b/internal/render/row_event.go similarity index 83% rename from internal/render/event.go rename to internal/render/row_event.go index f751bb54..ecd75dc3 100644 --- a/internal/render/event.go +++ b/internal/render/row_event.go @@ -2,9 +2,9 @@ package render import ( "fmt" + "reflect" "sort" - "github.com/gdamore/tcell" "github.com/rs/zerolog/log" ) @@ -35,9 +35,6 @@ type RowEvent struct { Deltas DeltaRow } -// RowEvents a collection of row events. -type RowEvents []RowEvent - // NewRowEvent returns a new row event. func NewRowEvent(kind ResEvent, row Row) RowEvent { return RowEvent{ @@ -64,6 +61,39 @@ func (r RowEvent) Clone() RowEvent { } } +func (r RowEvent) Changed(re RowEvent) bool { + if r.Kind != re.Kind { + log.Debug().Msgf("KIND Changed") + return true + } + if !reflect.DeepEqual(r.Deltas, re.Deltas) { + log.Debug().Msgf("DELTAS CHANGED") + return true + } + + return !reflect.DeepEqual(r.Row.Fields[:len(r.Row.Fields)-1], re.Row.Fields[:len(re.Row.Fields)-1]) +} + +// ---------------------------------------------------------------------------- + +// RowEvents a collection of row events. +type RowEvents []RowEvent + +// Changed returns true if the header changed. +func (rr RowEvents) Changed(r RowEvents) bool { + if len(rr) != len(r) { + return true + } + + for i := range rr { + if rr[i].Changed(r[i]) { + return true + } + } + + return false +} + // Clone returns a rowevents deep copy. func (rr RowEvents) Clone() RowEvents { res := make(RowEvents, len(rr)) @@ -103,10 +133,7 @@ func (rr RowEvents) Delete(id string) RowEvents { // Clear delete all row events func (rr RowEvents) Clear() RowEvents { - for _, e := range rr { - rr = rr.Delete(e.Row.ID) - } - return rr + return RowEvents{} } // FindIndex locates a row index by id. Returns false is not found. @@ -202,41 +229,6 @@ func findIndex(ss []string, s string) int { // ---------------------------------------------------------------------------- -var ( - // ModColor row modified color. - ModColor tcell.Color - // AddColor row added color. - AddColor tcell.Color - // ErrColor row err color. - ErrColor tcell.Color - // StdColor row default color. - StdColor tcell.Color - // HighlightColor row highlight color. - HighlightColor tcell.Color - // KillColor row deleted color. - KillColor tcell.Color - // CompletedColor row completed color. - CompletedColor tcell.Color -) - -// ColorerFunc represents a resource row colorer. -type ColorerFunc func(ns string, evt RowEvent) tcell.Color - -// DefaultColorer set the default table row colors. -func DefaultColorer(ns string, evt RowEvent) tcell.Color { - var col = StdColor - switch evt.Kind { - case EventAdd: - col = AddColor - case EventUpdate: - col = ModColor - case EventDelete: - col = KillColor - } - - return col -} - type StringSet []string func (ss StringSet) Add(item string) StringSet { diff --git a/internal/render/event_test.go b/internal/render/row_event_test.go similarity index 100% rename from internal/render/event_test.go rename to internal/render/row_event_test.go diff --git a/internal/render/row_header.go b/internal/render/row_header.go new file mode 100644 index 00000000..1562d048 --- /dev/null +++ b/internal/render/row_header.go @@ -0,0 +1,73 @@ +package render + +import "reflect" + +const ageCol = "AGE" + +// Header represent a table header +type Header struct { + Name string + Align int + Decorator DecoratorFunc +} + +// Clone copies a header. +func (h Header) Clone() Header { + return h +} + +// ---------------------------------------------------------------------------- + +// HeaderRow represents a table header. +type HeaderRow []Header + +func (hh HeaderRow) Clone() HeaderRow { + h := make(HeaderRow, len(hh)) + for i, v := range hh { + h[i] = v.Clone() + } + + return h +} + +// Clear clears out the header row. +func (hh HeaderRow) Clear() HeaderRow { + return HeaderRow{} +} + +// Changed returns true if the header changed. +func (hh HeaderRow) Changed(h HeaderRow) bool { + if len(hh) != len(h) { + return true + } + return !reflect.DeepEqual(hh.Columns(), h.Columns()) +} + +// Columns return header as a collection of strings. +func (h HeaderRow) Columns() []string { + cc := make([]string, len(h)) + for i, c := range h { + cc[i] = c.Name + } + + return cc +} + +// HasAge returns true if table has an age column. +func (h HeaderRow) HasAge() bool { + for _, r := range h { + if r.Name == ageCol { + return true + } + } + + return false +} + +// AgeCol checks if given column index is the age column. +func (h HeaderRow) AgeCol(col int) bool { + if !h.HasAge() { + return false + } + return col == len(h)-1 +} diff --git a/internal/render/row_test.go b/internal/render/row_test.go index f7149791..24b3b0bf 100644 --- a/internal/render/row_test.go +++ b/internal/render/row_test.go @@ -1,13 +1,23 @@ package render_test import ( + "fmt" + "reflect" "testing" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) -func TestRowDelete(t *testing.T) { +func TestFieldClone(t *testing.T) { + f := render.Fields{"a", "b", "c"} + f1 := f.Clone() + + assert.True(t, reflect.DeepEqual(f, f1)) + assert.NotEqual(t, fmt.Sprintf("%p", f), fmt.Sprintf("%p", f1)) +} + +func TestRowsDelete(t *testing.T) { uu := map[string]struct { rows render.Rows id string @@ -67,7 +77,7 @@ func TestRowDelete(t *testing.T) { } } -func TestSortText(t *testing.T) { +func TestRowsSortText(t *testing.T) { uu := map[string]struct { rows render.Rows col int @@ -145,7 +155,7 @@ func TestSortText(t *testing.T) { } } -func TestSortDuration(t *testing.T) { +func TestRowsSortDuration(t *testing.T) { uu := map[string]struct { rows render.Rows col int @@ -186,7 +196,7 @@ func TestSortDuration(t *testing.T) { } } -func TestSortMetrics(t *testing.T) { +func TestRowsSortMetrics(t *testing.T) { uu := map[string]struct { rows render.Rows col int diff --git a/internal/render/table.go b/internal/render/table.go index cfb64ff1..d8ceb542 100644 --- a/internal/render/table.go +++ b/internal/render/table.go @@ -6,3 +6,78 @@ type TableData struct { RowEvents RowEvents Namespace string } + +// Clear clears out the entire table. +func (t *TableData) Clear() { + t.Header, t.RowEvents = t.Header.Clear(), t.RowEvents.Clear() +} + +// Clone returns a copy of the table +func (t *TableData) Clone() TableData { + return cloneTable(*t) +} + +func cloneTable(t TableData) TableData { + return t +} + +// Update computes row deltas and update the table data. +func (t *TableData) Update(rows Rows) { + empty := len(t.RowEvents) == 0 + kk := make([]string, 0, len(rows)) + var blankDelta DeltaRow + for _, row := range rows { + kk = append(kk, row.ID) + if empty { + t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row)) + continue + } + if index, ok := t.RowEvents.FindIndex(row.ID); ok { + delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header.HasAge()) + if delta.IsBlank() { + t.RowEvents[index].Kind, t.RowEvents[index].Deltas = EventUnchanged, blankDelta + t.RowEvents[index].Row = row + } else { + t.RowEvents[index] = NewDeltaRowEvent(row, delta) + } + continue + } + t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row)) + } + + if !empty { + t.Delete(kk) + } +} + +// EnsureDeletes delete items in cache that are no longer valid. +func (t *TableData) Delete(newKeys []string) { + for _, re := range t.RowEvents { + var found bool + for i, key := range newKeys { + if key == re.Row.ID { + found = true + newKeys = append(newKeys[:i], newKeys[i+1:]...) + break + } + } + if !found { + t.RowEvents = t.RowEvents.Delete(re.Row.ID) + } + } +} + +// Diff checks if two tables are equal. +func (t *TableData) Diff(table TableData) bool { + if t.Namespace != table.Namespace { + return true + } + if t.Header.Changed(table.Header) { + return true + } + if t.RowEvents.Changed(table.RowEvents) { + return true + } + + return false +} diff --git a/internal/ui/app.go b/internal/ui/app.go index bf9bcda0..92ace63b 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -11,10 +11,8 @@ type App struct { *tview.Application Configurator - Main *Pages - + Main *Pages actions KeyActions - views map[string]tview.Primitive cmdBuff *CmdBuff } diff --git a/internal/ui/cmd.go b/internal/ui/cmd.go index 58903ea0..7c816f59 100644 --- a/internal/ui/cmd.go +++ b/internal/ui/cmd.go @@ -76,7 +76,7 @@ func (v *CmdView) BufferActive(f bool, k BufferKind) { v.SetTextColor(v.styles.FgColor()) v.SetBorderColor(colorFor(k)) v.icon = iconFor(k) - v.reset() + // v.reset() v.activate() } else { v.SetBorder(false) diff --git a/internal/ui/cmd_buff.go b/internal/ui/cmd_buff.go index bbb57804..a22dc96b 100644 --- a/internal/ui/cmd_buff.go +++ b/internal/ui/cmd_buff.go @@ -1,5 +1,7 @@ package ui +import "github.com/rs/zerolog/log" + const maxBuff = 10 const ( @@ -65,6 +67,7 @@ func (c *CmdBuff) IsActive() bool { // SetActive toggles cmd buffer active state. func (c *CmdBuff) SetActive(b bool) { + log.Debug().Msgf("CMDBUFF -- Active %t", b) c.active = b c.fireActive(c.active) } @@ -143,7 +146,9 @@ func (c *CmdBuff) fireChanged() { } func (c *CmdBuff) fireActive(b bool) { + log.Debug().Msgf("CMDBUFF LIST SIZE %d", len(c.listeners)) for _, l := range c.listeners { + log.Debug().Msgf("CMDBUFF LIST -- %T", l) l.BufferActive(b, c.kind) } } diff --git a/internal/ui/select_table.go b/internal/ui/select_table.go index f574996a..441ea00e 100644 --- a/internal/ui/select_table.go +++ b/internal/ui/select_table.go @@ -1,21 +1,46 @@ package ui import ( + "context" + "time" + + "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/tview" "github.com/gdamore/tcell" ) +type Tabular interface { + Empty() bool + Peek() render.TableData + ClusterWide() bool + GetNamespace() string + SetNamespace(string) + AddListener(model.TableListener) + Start(context.Context) + InNamespace(string) bool + SetRefreshRate(time.Duration) +} + // Selectable represents a table with selections. type SelectTable struct { *tview.Table - Data render.TableData - selectedItem string - selectedRow int - selectedFn func(string) string - selListeners []SelectedRowFunc - marks map[string]bool + model Tabular + selectedRow int + selectedFn func(string) string + selectionListeners []SelectedRowFunc + marks map[string]bool +} + +// SetModel sets the table model. +func (s *SelectTable) SetModel(m Tabular) { + s.model = m +} + +// GetModel returns the current model. +func (s *SelectTable) GetModel() Tabular { + return s.model } // ClearSelection reset selected row. @@ -49,10 +74,17 @@ func (s *SelectTable) GetSelectedItems() []string { // GetSelectedItem returns the currently selected item name. func (s *SelectTable) GetSelectedItem() string { - if s.selectedFn != nil { - return s.selectedFn(s.selectedItem) + if s.GetSelectedRowIndex() == 0 || s.model.Empty() { + return "" } - return s.selectedItem + sel, ok := s.GetCell(s.GetSelectedRowIndex(), 0).GetReference().(string) + if !ok { + return "" + } + if s.selectedFn != nil { + return s.selectedFn(sel) + } + return sel } // GetSelectedCell returns the content of a cell for the currently selected row. @@ -70,36 +102,13 @@ func (s *SelectTable) GetSelectedRowIndex() int { return s.selectedRow } -// RowSelected checks if there is an active row selection. -func (s *SelectTable) RowSelected() bool { - return s.selectedItem != "" -} - -// GetRow retrieves the entire selected row. -func (s *SelectTable) GetRow() render.Row { - return s.Data.RowEvents[s.GetSelectedRowIndex()].Row -} - -func (s *SelectTable) updateSelectedItem(r int) { - if r <= 0 || len(s.Data.RowEvents) == 0 { - s.selectedItem = "" - return - } - - if r-1 >= len(s.Data.RowEvents) { - return - } - s.selectedItem = s.Data.RowEvents[r-1].Row.ID -} - // SelectRow select a given row by index. func (s *SelectTable) SelectRow(r int, broadcast bool) { if !broadcast { s.SetSelectionChangedFunc(nil) } - defer s.SetSelectionChangedFunc(s.selChanged) + defer s.SetSelectionChangedFunc(s.selectionChanged) s.Select(r, 0) - s.updateSelectedItem(r) } // UpdateSelection refresh selected row. @@ -107,9 +116,8 @@ func (s *SelectTable) updateSelection(broadcast bool) { s.SelectRow(s.selectedRow, broadcast) } -func (s *SelectTable) selChanged(r, c int) { +func (s *SelectTable) selectionChanged(r, c int) { s.selectedRow = r - s.updateSelectedItem(r) if r == 0 { return } @@ -121,8 +129,8 @@ func (s *SelectTable) selChanged(r, c int) { s.SetSelectedStyle(tcell.ColorBlack, cell.Color, tcell.AttrBold) } - for _, f := range s.selListeners { - f(r, c) + for _, f := range s.selectionListeners { + f(r) } } @@ -159,5 +167,5 @@ func (s *Table) IsMarked(item string) bool { // AddSelectedRowListener add a new selected row listener. func (s *SelectTable) AddSelectedRowListener(f SelectedRowFunc) { - s.selListeners = append(s.selListeners, f) + s.selectionListeners = append(s.selectionListeners, f) } diff --git a/internal/ui/table.go b/internal/ui/table.go index a4a7e3d6..1050810a 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -20,7 +20,7 @@ type ( DecorateFunc func(render.TableData) render.TableData // SelectedRowFunc a table selection callback. - SelectedRowFunc func(r, c int) + SelectedRowFunc func(r int) ) // Table represents tabular data. @@ -38,15 +38,16 @@ type Table struct { } // NewTable returns a new table view. -func NewTable(title string) *Table { +func NewTable(gvr string) *Table { return &Table{ SelectTable: &SelectTable{ Table: tview.NewTable(), + model: model.NewTable(gvr), marks: make(map[string]bool), }, actions: make(KeyActions), cmdBuff: NewCmdBuff('/', FilterBuff), - BaseTitle: title, + BaseTitle: gvr, sortCol: SortColumn{index: -1, colCount: 0, asc: true}, } } @@ -67,7 +68,7 @@ func (t *Table) Init(ctx context.Context) { config.AsColor(t.styles.GetTable().CursorColor), tcell.AttrBold, ) - t.SetSelectionChangedFunc(t.selChanged) + t.SetSelectionChangedFunc(t.selectionChanged) t.SetInputCapture(t.keyboard) } @@ -91,7 +92,8 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { if t.SearchBuff().IsActive() { t.SearchBuff().Add(evt.Rune()) t.ClearSelection() - t.doUpdate(t.filtered(t.Data), len(t.Data.RowEvents) > 0) + data := t.GetModel().Peek() + t.doUpdate(t.filtered(data), len(data.RowEvents) > 0) t.UpdateTitle() t.SelectFirstRow() return nil @@ -112,7 +114,7 @@ func (t *Table) Hints() model.MenuHints { // GetFilteredData fetch filtered tabular data. func (t *Table) GetFilteredData() render.TableData { - return t.filtered(t.Data) + return t.filtered(t.GetModel().Peek()) } // SetDecorateFn specifies the default row decorator. @@ -133,10 +135,9 @@ func (t *Table) SetSortCol(index, count int, asc bool) { // Update table content. func (t *Table) Update(data render.TableData) { var firstRow bool - if len(t.Data.RowEvents) == 0 { + if t.GetRowCount() == 0 { firstRow = true } - t.Data = data if t.decorateFn != nil { data = t.decorateFn(data) @@ -176,7 +177,7 @@ func (t *Table) doUpdate(data render.TableData, firstRow bool) { if firstRow { t.SelectFirstRow() } - t.updateSelection(false) + t.updateSelection(true) } // SortColCmd designates a sorted column. @@ -250,6 +251,9 @@ func (t *Table) buildRow(ns string, r int, re render.RowEvent, header render.Hea log.Debug().Msgf("Marked!") c.SetTextColor(config.AsColor(t.styles.GetTable().MarkColor)) } + if col == 0 { + c.SetReference(re.Row.ID) + } t.SetCell(r, col, c) } } @@ -261,13 +265,17 @@ func (t *Table) ClearMarks() { // Refresh update the table data. func (t *Table) Refresh() { - t.Update(t.Data) + t.Update(t.model.Peek()) +} + +func (t *Table) GetSelectedRow() render.Row { + return t.model.Peek().RowEvents[t.GetSelectedRowIndex()].Row } // NameColIndex returns the index of the resource name column. func (t *Table) NameColIndex() int { col := 0 - if t.Data.Namespace == render.AllNamespaces { + if t.GetModel().ClusterWide() { col++ } return col @@ -315,7 +323,7 @@ func (t *Table) ShowDeleted() { // UpdateTitle refreshes the table title. func (t *Table) UpdateTitle() { - ns := t.Data.Namespace + ns := t.GetModel().GetNamespace() if ns == render.AllNamespaces { ns = render.NamespaceAll } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 01ca512d..46790d00 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -3,8 +3,10 @@ package ui_test import ( "context" "testing" + "time" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" @@ -36,11 +38,13 @@ func TestTableSelection(t *testing.T) { s, _ := config.NewStyles("") ctx := context.WithValue(context.Background(), ui.KeyStyles, s) v.Init(ctx) - v.Update(makeTableData()) + m := &testModel{} + v.SetModel(m) + v.Update(m.Peek()) v.SelectRow(1, true) - assert.True(t, v.RowSelected()) - assert.Equal(t, render.Row{ID: "r2", Fields: render.Fields{"blee", "duh", "zorg"}}, v.GetRow()) + assert.Equal(t, "r1", v.GetSelectedItem()) + assert.Equal(t, render.Row{ID: "r2", Fields: render.Fields{"blee", "duh", "zorg"}}, v.GetSelectedRow()) assert.Equal(t, "blee", v.GetSelectedCell(0)) assert.Equal(t, 1, v.GetSelectedRowIndex()) assert.Equal(t, []string{"r1"}, v.GetSelectedItems()) @@ -50,8 +54,23 @@ func TestTableSelection(t *testing.T) { assert.Equal(t, 1, v.GetSelectedRowIndex()) } +// ---------------------------------------------------------------------------- // Helpers... +type testModel struct{} + +var _ ui.Tabular = &testModel{} + +func (t *testModel) Empty() bool { return false } +func (t *testModel) Peek() render.TableData { return makeTableData() } +func (t *testModel) ClusterWide() bool { return false } +func (t *testModel) GetNamespace() string { return "blee" } +func (t *testModel) SetNamespace(string) {} +func (t *testModel) AddListener(model.TableListener) {} +func (t *testModel) Start(context.Context) {} +func (t *testModel) InNamespace(string) bool { return true } +func (t *testModel) SetRefreshRate(time.Duration) {} + func makeTableData() render.TableData { return render.TableData{ Namespace: "", diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index e92cd3cc..ed109f56 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -3,9 +3,12 @@ package view_test import ( "context" "testing" + "time" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/view" "github.com/gdamore/tcell" @@ -18,21 +21,22 @@ func TestAliasNew(t *testing.T) { assert.Nil(t, v.Init(makeContext())) assert.Equal(t, "Aliases", v.Name()) - assert.Equal(t, 9, len(v.Hints())) + assert.Equal(t, 10, len(v.Hints())) } // BOZO!! -// func TestAliasSearch(t *testing.T) { -// v := view.NewAlias(client.GVR("aliases")) -// assert.Nil(t, v.Init(makeContext())) -// v.GetTable().SearchBuff().SetActive(true) -// v.GetTable().SearchBuff().Set("dump") +func TestAliasSearch(t *testing.T) { + v := view.NewAlias(client.GVR("aliases")) + assert.Nil(t, v.Init(makeContext())) + v.GetTable().SetModel(&testModel{}) + v.GetTable().SearchBuff().SetActive(true) + v.GetTable().SearchBuff().Set("dump") -// v.GetTable().SendKey(tcell.NewEventKey(tcell.KeyRune, 'd', tcell.ModNone)) + v.GetTable().SendKey(tcell.NewEventKey(tcell.KeyRune, 'd', tcell.ModNone)) -// assert.Equal(t, 3, v.GetTable().GetColumnCount()) -// assert.Equal(t, 1, v.GetTable().GetRowCount()) -// } + assert.Equal(t, 3, v.GetTable().GetColumnCount()) + assert.Equal(t, 1, v.GetTable().GetRowCount()) +} func TestAliasGoto(t *testing.T) { v := view.NewAlias(client.GVR("aliases")) @@ -47,6 +51,7 @@ func TestAliasGoto(t *testing.T) { assert.True(t, v.GetTable().SearchBuff().IsActive()) } +// ---------------------------------------------------------------------------- // Helpers... type buffL struct { @@ -88,3 +93,42 @@ func (k ks) ClusterNames() ([]string, error) { func (k ks) NamespaceNames(nn []v1.Namespace) []string { return []string{"test"} } + +type testModel struct{} + +var _ ui.Tabular = &testModel{} + +func (t *testModel) Empty() bool { return false } +func (t *testModel) Peek() render.TableData { return makeTableData() } +func (t *testModel) ClusterWide() bool { return false } +func (t *testModel) GetNamespace() string { return "blee" } +func (t *testModel) SetNamespace(string) {} +func (t *testModel) AddListener(model.TableListener) {} +func (t *testModel) Start(context.Context) {} +func (t *testModel) InNamespace(string) bool { return true } +func (t *testModel) SetRefreshRate(time.Duration) {} + +func makeTableData() render.TableData { + return render.TableData{ + Namespace: render.ClusterScope, + Header: render.HeaderRow{ + render.Header{Name: "RESOURCE"}, + render.Header{Name: "COMMAND"}, + render.Header{Name: "APIGROUP"}, + }, + RowEvents: render.RowEvents{ + render.RowEvent{ + Row: render.Row{ + ID: "r1", + Fields: render.Fields{"blee", "duh", "fred"}, + }, + }, + render.RowEvent{ + Row: render.Row{ + ID: "r2", + Fields: render.Fields{"fred", "duh", "zorg"}, + }, + }, + }, + } +} diff --git a/internal/view/app.go b/internal/view/app.go index 3018c69a..f33e0a1c 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -56,13 +56,9 @@ func (a *App) ActiveView() model.Component { } func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey { - a.Content.DumpStack() - a.Content.DumpPages() if !a.Content.IsLast() { a.Content.Pop() } - a.Content.DumpStack() - a.Content.DumpPages() return nil } diff --git a/internal/view/browser.go b/internal/view/browser.go index 6c6a9058..6145fb8f 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -7,14 +7,12 @@ import ( "fmt" rt "runtime" "strconv" - "time" "github.com/atotto/clipboard" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" - "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" @@ -55,7 +53,6 @@ func NewBrowser(gvr client.GVR) ResourceViewer { // Init watches all running pods in given namespace func (b *Browser) Init(ctx context.Context) error { - log.Debug().Msgf("BROWSER INIT %s", b.gvr) var err error b.meta, err = dao.MetaFor(b.gvr) if err != nil { @@ -66,14 +63,16 @@ func (b *Browser) Init(ctx context.Context) error { return err } if !dao.IsK9sMeta(b.meta) { - _ = b.app.factory.ForResource(b.app.Config.ActiveNamespace(), b.GVR()) - b.app.factory.WaitForCacheSync() + if _, err := b.app.factory.CanForResource(b.app.Config.ActiveNamespace(), b.GVR()); err != nil { + return err + } } if b.bindKeysFn != nil { b.bindKeysFn(b.Actions()) } - b.Table.BaseTitle = b.meta.Kind + b.BaseTitle = b.meta.Kind + b.SetTitle(" [orange:i:]LOADING... ") b.accessor, err = dao.AccessorFor(b.app.factory, b.gvr) if err != nil { return err @@ -82,49 +81,47 @@ func (b *Browser) Init(ctx context.Context) error { b.envFn = b.defaultK9sEnv b.setNamespace(b.App().Config.ActiveNamespace()) - b.refresh() row, _ := b.GetSelection() if row == 0 && b.GetRowCount() > 0 { b.Select(1, 0) } + b.GetModel().AddListener(b) return nil } -// Start initializes updates. +// Start initializes browser updates. func (b *Browser) Start() { b.Stop() - log.Debug().Msgf("GOROUTINE %d", rt.NumGoroutine()) - log.Debug().Msgf("BROWSER START %s", b.gvr) + b.Table.Start() + ctx := b.defaultContext() + ctx, b.cancelFn = context.WithCancel(ctx) + if b.contextFn != nil { + ctx = b.contextFn(ctx) + } + if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" { + b.Path = path + } - var ctx context.Context - ctx, b.cancelFn = context.WithCancel(context.Background()) - go b.update(ctx) + b.GetModel().Start(ctx) } +// Stop terminates browser updates. func (b *Browser) Stop() { - if b.cancelFn != nil { - b.cancelFn() - b.cancelFn = nil - log.Debug().Msgf("BROWSER %s", b.BaseTitle) + if b.cancelFn == nil { + return } + b.Table.Stop() + log.Debug().Msgf("BROWSER %q", b.gvr) + b.cancelFn() + b.cancelFn = nil } -func (b *Browser) update(ctx context.Context) { - defer log.Debug().Msgf("UPDATER BAIL For %s", b.gvr) - for { - select { - case <-ctx.Done(): - log.Debug().Msgf("BROWSER <> -- %s", b.gvr) - return - case <-time.After(time.Duration(b.app.Config.K9s.GetRefreshRate()) * time.Second): - log.Debug().Msgf("GOROUTINE %d", rt.NumGoroutine()) - b.refresh() - } - } +func (b *Browser) refresh() { + b.Start() } // Name returns the component name. @@ -149,11 +146,12 @@ func (b *Browser) GetTable() *Table { return b.Table } // Actions()... func (b *Browser) cpCmd(evt *tcell.EventKey) *tcell.EventKey { - if !b.RowSelected() { + path := b.GetSelectedItem() + if path == "" { return evt } - _, n := client.Namespaced(b.GetSelectedItem()) + _, n := client.Namespaced(path) log.Debug().Msgf("Copied selection to clipboard %q", n) b.app.Flash().Info("Current selection copied to clipboard...") if err := clipboard.WriteAll(n); err != nil { @@ -164,7 +162,8 @@ func (b *Browser) cpCmd(evt *tcell.EventKey) *tcell.EventKey { } func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey { - if b.filterCmd(evt) == nil || !b.RowSelected() { + path := b.GetSelectedItem() + if b.filterCmd(evt) == nil || path == "" { return nil } @@ -172,7 +171,7 @@ func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey { if b.enterFn != nil { f = b.enterFn } - f(b.app, b.Data.Namespace, string(b.gvr), b.GetSelectedItem()) + f(b.app, b.GetModel().GetNamespace(), string(b.gvr), path) return nil } @@ -244,11 +243,11 @@ func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { } func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey { - log.Debug().Msgf("DESCRIBE %t -- %#v", b.RowSelected(), b.GetSelectedItems()) - if !b.RowSelected() { + path := b.GetSelectedItem() + if path == "" { return evt } - b.describeResource(b.app, b.Data.Namespace, string(b.gvr), b.GetSelectedItem()) + b.describeResource(b.app, b.GetModel().GetNamespace(), string(b.gvr), path) return nil } @@ -272,12 +271,12 @@ func (b *Browser) describeResource(app *App, _, _, sel string) { } func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey { - if !b.RowSelected() { + path := b.GetSelectedItem() + if path == "" { return evt } - path := b.GetSelectedItem() - log.Debug().Msgf("------ NAMESPACES %q vs %q", path, b.Data.Namespace) + log.Debug().Msgf("------ NAMESPACES %q vs %q", path, b.GetModel().GetNamespace()) o, err := b.app.factory.Get(string(b.gvr), path, labels.Everything()) if err != nil { b.app.Flash().Errf("Unable to get resource %q -- %s", b.gvr, err) @@ -317,14 +316,15 @@ func toYAML(o runtime.Object) (string, error) { } func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey { - if !b.RowSelected() { + path := b.GetSelectedItem() + if path == "" { return evt } b.Stop() defer b.Start() { - ns, po := client.Namespaced(b.GetSelectedItem()) + ns, n := client.Namespaced(path) args := make([]string, 0, 10) args = append(args, "edit") args = append(args, b.meta.Kind) @@ -333,7 +333,7 @@ func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey { if cfg := b.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { args = append(args, "--kubeconfig", *cfg) } - if !runK(true, b.app, append(args, po)...) { + if !runK(true, b.app, append(args, n)...) { b.app.Flash().Err(errors.New("Edit exec failed")) } } @@ -343,10 +343,10 @@ func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey { func (b *Browser) setNamespace(ns string) { if !b.meta.Namespaced { - b.Data.Namespace = render.ClusterScope + b.GetModel().SetNamespace(render.ClusterScope) return } - if b.Data.Namespace == ns { + if b.GetModel().InNamespace(ns) { return } @@ -354,8 +354,7 @@ func (b *Browser) setNamespace(ns string) { ns = render.AllNamespaces } log.Debug().Msgf("!!!!!! SETTING NS %q", ns) - b.Data.Namespace = ns - b.Data.RowEvents = b.Data.RowEvents.Clear() + b.GetModel().SetNamespace(ns) } func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -372,7 +371,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { b.UpdateTitle() b.SelectRow(1, true) b.app.CmdBuff().Reset() - if err := b.app.Config.SetActiveNamespace(b.Data.Namespace); err != nil { + if err := b.app.Config.SetActiveNamespace(b.GetModel().GetNamespace()); err != nil { log.Error().Err(err).Msg("Config save NS failed!") } if err := b.app.Config.Save(); err != nil { @@ -382,33 +381,30 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (b *Browser) refresh() { - if b.app.Conn() == nil { - return - } - ctx := b.defaultContext() - if b.contextFn != nil { - ctx = b.contextFn(ctx) - } - if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" { - b.Path = path - } - data, err := model.Reconcile(ctx, b.Table.Data, b.gvr) +// TableLoadChanged notifies view something went south. +func (b *Browser) TableLoadFailed(err error) { + b.app.QueueUpdateDraw(func() { + b.app.Flash().Err(err) + }) +} + +// TableDataChanged notifies view new data is available. +func (b *Browser) TableDataChanged(data render.TableData) { + b.Update(data) b.app.QueueUpdateDraw(func() { - if err != nil { - b.app.Flash().Err(err) - } b.refreshActions() - b.Update(data) }) } func (b *Browser) defaultContext() context.Context { - ctx := context.WithValue(context.Background(), internal.KeyFactory, b.app.factory) + ctx := context.Background() + + ctx = context.WithValue(ctx, internal.KeyFactory, b.app.factory) ctx = context.WithValue(ctx, internal.KeyGVR, string(b.gvr)) ctx = context.WithValue(ctx, internal.KeyPath, b.Path) ctx = context.WithValue(ctx, internal.KeyLabels, "") ctx = context.WithValue(ctx, internal.KeyFields, "") + ctx = context.WithValue(ctx, internal.KeyNamespace, b.App().Config.ActiveNamespace()) return ctx } @@ -491,8 +487,8 @@ func (b *Browser) customActions(aa ui.KeyActions) { func (b *Browser) execCmd(bin string, bg bool, args ...string) ui.ActionHandler { return func(evt *tcell.EventKey) *tcell.EventKey { - if !b.RowSelected() { - + path := b.GetSelectedItem() + if path == "" { return evt } @@ -519,5 +515,5 @@ func (b *Browser) execCmd(bin string, bg bool, args ...string) ui.ActionHandler } func (b *Browser) defaultK9sEnv() K9sEnv { - return defaultK9sEnv(b.app, b.GetSelectedItem(), b.GetRow()) + return defaultK9sEnv(b.app, b.GetSelectedItem(), b.GetSelectedRow()) } diff --git a/internal/view/command.go b/internal/view/command.go index 4cd3f6de..1668b8f8 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -52,7 +52,7 @@ func (c *command) defaultCmd() error { var canRX = regexp.MustCompile(`\Acan\s([u|g|s]):([\w-:]+)\b`) -func (c *command) isK9sCmd(cmd string) bool { +func (c *command) specialCmd(cmd string) bool { cmds := strings.Split(cmd, " ") switch cmds[0] { case "q", "Q", "quit": @@ -85,6 +85,10 @@ func (c *command) viewMetaFor(cmd string) (string, *MetaViewer, error) { if !ok { return "", nil, fmt.Errorf("Huh? `%s` command not found", cmd) } + if _, err := c.app.factory.CanForResource(c.app.Config.ActiveNamespace(), gvr); err != nil { + return "", nil, err + } + v, ok := customViewers[client.GVR(gvr)] if !ok { return gvr, &MetaViewer{viewerFn: NewBrowser}, nil @@ -95,7 +99,7 @@ func (c *command) viewMetaFor(cmd string) (string, *MetaViewer, error) { // Exec the command by showing associated display. func (c *command) run(cmd string) error { - if c.isK9sCmd(cmd) { + if c.specialCmd(cmd) { return nil } diff --git a/internal/view/container.go b/internal/view/container.go index 63984c76..81c951e7 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -50,7 +50,7 @@ func (c *Container) bindKeys(aa ui.KeyActions) { } func (c *Container) k9sEnv() K9sEnv { - env := defaultK9sEnv(c.App(), c.GetTable().GetSelectedItem(), c.GetTable().GetRow()) + env := defaultK9sEnv(c.App(), c.GetTable().GetSelectedItem(), c.GetTable().GetSelectedRow()) ns, n := client.Namespaced(c.GetTable().Path) env["POD"] = n env["NAMESPACE"] = ns @@ -59,9 +59,7 @@ func (c *Container) k9sEnv() K9sEnv { } func (c *Container) selectedContainer() string { - log.Debug().Msgf("Container SELECTED %s", c.GetTable().GetSelectedItem()) tokens := strings.Split(c.GetTable().GetSelectedItem(), "/") - return tokens[0] } @@ -152,7 +150,7 @@ func (c *Container) portForward(lport, cport string) { func (c *Container) runForward(pf *dao.PortForwarder, f *portforward.PortForwarder) { c.App().QueueUpdateDraw(func() { - c.App().factory.RegisterForwarder(pf) + c.App().factory.AddForwarder(pf) c.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0]) dialog.DismissPortForward(c.App().Content.Pages) }) diff --git a/internal/view/container_test.go b/internal/view/container_test.go index 15e85d8c..11a37af1 100644 --- a/internal/view/container_test.go +++ b/internal/view/container_test.go @@ -13,5 +13,5 @@ func TestContainerNew(t *testing.T) { assert.Nil(t, c.Init(makeCtx())) assert.Equal(t, "Containers", c.Name()) - assert.Equal(t, 17, len(c.Hints())) + assert.Equal(t, 18, len(c.Hints())) } diff --git a/internal/view/context_test.go b/internal/view/context_test.go index daa55c0a..21865ba4 100644 --- a/internal/view/context_test.go +++ b/internal/view/context_test.go @@ -13,5 +13,5 @@ func TestContext(t *testing.T) { assert.Nil(t, ctx.Init(makeCtx())) assert.Equal(t, "Contexts", ctx.Name()) - assert.Equal(t, 8, len(ctx.Hints())) + assert.Equal(t, 9, len(ctx.Hints())) } diff --git a/internal/view/cronjob.go b/internal/view/cronjob.go index 79759b06..ca6d9ae9 100644 --- a/internal/view/cronjob.go +++ b/internal/view/cronjob.go @@ -32,9 +32,9 @@ func NewCronJob(gvr client.GVR) ResourceViewer { return &c } -func (c *CronJob) showJobs(app *App, ns, res, path string) { - log.Debug().Msgf("Showing Jobs %q:%q -- %q", ns, res, path) - o, err := app.factory.Get("batch/v1beta1/cronjobs", path, labels.Everything()) +func (c *CronJob) showJobs(app *App, ns, gvr, path string) { + log.Debug().Msgf("Showing Jobs %q:%q -- %q", ns, gvr, path) + o, err := app.factory.Get(gvr, path, labels.Everything()) if err != nil { app.Flash().Err(err) return diff --git a/internal/view/details.go b/internal/view/details.go index 36f9363f..715fd94b 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -85,8 +85,8 @@ func (d *Details) Hints() model.MenuHints { func (d *Details) bindKeys() { d.actions.Set(ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Back", d.app.PrevCmd, true), - tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, true), + tcell.KeyEscape: ui.NewKeyAction("Back", d.app.PrevCmd, false), + tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true), }) } diff --git a/internal/view/dp_test.go b/internal/view/dp_test.go index adf9173d..d9c93664 100644 --- a/internal/view/dp_test.go +++ b/internal/view/dp_test.go @@ -13,6 +13,6 @@ func TestDeploy(t *testing.T) { assert.Nil(t, v.Init(makeCtx())) assert.Equal(t, "Deployments", v.Name()) - assert.Equal(t, 16, len(v.Hints())) + assert.Equal(t, 17, len(v.Hints())) } diff --git a/internal/view/ds_test.go b/internal/view/ds_test.go index c23b2222..b9f6cd3d 100644 --- a/internal/view/ds_test.go +++ b/internal/view/ds_test.go @@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) { assert.Nil(t, v.Init(makeCtx())) assert.Equal(t, "DaemonSets", v.Name()) - assert.Equal(t, 15, len(v.Hints())) + assert.Equal(t, 16, len(v.Hints())) } diff --git a/internal/view/group.go b/internal/view/group.go index 43ec7957..f04be749 100644 --- a/internal/view/group.go +++ b/internal/view/group.go @@ -17,31 +17,33 @@ type Group struct { // NewGroup returns a new subject viewer. func NewGroup(gvr client.GVR) ResourceViewer { - s := Group{ResourceViewer: NewBrowser(gvr)} - s.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) - s.SetBindKeysFn(s.bindKeys) - s.SetContextFn(s.subjectCtx) - return &s + g := Group{ResourceViewer: NewBrowser(gvr)} + g.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) + g.SetBindKeysFn(g.bindKeys) + g.SetContextFn(g.subjectCtx) + + return &g } -func (s *Group) bindKeys(aa ui.KeyActions) { +func (g *Group) bindKeys(aa ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace) aa.Add(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Rules", s.policyCmd, true), - ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.GetTable().SortColCmd(1, true), false), + tcell.KeyEnter: ui.NewKeyAction("Rules", g.policyCmd, true), + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", g.GetTable().SortColCmd(1, true), false), }) } -func (s *Group) subjectCtx(ctx context.Context) context.Context { +func (g *Group) subjectCtx(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeySubjectKind, "Group") } -func (s *Group) policyCmd(evt *tcell.EventKey) *tcell.EventKey { - if !s.GetTable().RowSelected() { +func (g *Group) policyCmd(evt *tcell.EventKey) *tcell.EventKey { + path := g.GetTable().GetSelectedItem() + if path == "" { return evt } - if err := s.App().inject(NewPolicy(s.App(), "Group", s.GetTable().GetSelectedItem())); err != nil { - s.App().Flash().Err(err) + if err := g.App().inject(NewPolicy(g.App(), "Group", path)); err != nil { + g.App().Flash().Err(err) } return nil diff --git a/internal/view/help.go b/internal/view/help.go index 6c5c6919..12639f2a 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -116,7 +116,7 @@ func (v *Help) showGeneral() model.MenuHints { }, { Mnemonic: "esc", - Description: "Clear filter", + Description: "Back/Clear", }, { Mnemonic: "tab", @@ -138,6 +138,18 @@ func (v *Help) showGeneral() model.MenuHints { Mnemonic: ":q", Description: "Quit", }, + { + Mnemonic: "space", + Description: "Mark", + }, + { + Mnemonic: "Ctrl-space", + Description: "Clear Marks", + }, + { + Mnemonic: "Ctrl-s", + Description: "Save", + }, } } diff --git a/internal/view/help_test.go b/internal/view/help_test.go index 5df3dd3d..1e65c3e3 100644 --- a/internal/view/help_test.go +++ b/internal/view/help_test.go @@ -20,7 +20,7 @@ func TestHelp(t *testing.T) { v := view.NewHelp() assert.Nil(t, v.Init(ctx)) - assert.Equal(t, 25, v.GetRowCount()) + assert.Equal(t, 26, v.GetRowCount()) assert.Equal(t, 10, v.GetColumnCount()) assert.Equal(t, "", v.GetCell(1, 0).Text) assert.Equal(t, "Erase", v.GetCell(1, 1).Text) diff --git a/internal/view/job.go b/internal/view/job.go index 7b20ba21..ce4b2bff 100644 --- a/internal/view/job.go +++ b/internal/view/job.go @@ -23,9 +23,8 @@ func NewJob(gvr client.GVR) ResourceViewer { return &j } -// TODO!! Change enter signature? -func (*Job) showPods(app *App, _, res, path string) { - o, err := app.factory.Get("batch/v1/jobs", path, labels.Everything()) +func (*Job) showPods(app *App, _, gvr, path string) { + o, err := app.factory.Get(gvr, path, labels.Everything()) if err != nil { app.Flash().Err(err) return diff --git a/internal/view/node.go b/internal/view/node.go index 5805c5e1..0d6548b9 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -40,7 +40,8 @@ func (n *Node) showPods(app *App, ns, res, sel string) { } func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey { - if !n.GetTable().RowSelected() { + path := n.GetTable().GetSelectedItem() + if path == "" { return evt } diff --git a/internal/view/ns.go b/internal/view/ns.go index 0df38d18..1e0271fb 100644 --- a/internal/view/ns.go +++ b/internal/view/ns.go @@ -52,8 +52,6 @@ func (n *Namespace) useNsCmd(evt *tcell.EventKey) *tcell.EventKey { } n.useNamespace(path) - log.Debug().Msgf("NS TABLE %#v", n.GetTable().Data) - return nil } @@ -71,13 +69,10 @@ func (n *Namespace) useNamespace(ns string) { } func (n *Namespace) decorate(data render.TableData) render.TableData { - if n.App().Conn() == nil { - return render.TableData{} + if n.App().Conn() == nil || len(data.RowEvents) == 0 { + return data } - // log.Debug().Msgf("CLONING %q", data.Namespace) - // don't want to change the cache here thus need to clone!! - // res := data.Clone() // checks if all ns is in the list if not add it. if _, ok := data.RowEvents.FindIndex(render.NamespaceAll); !ok { data.RowEvents = append(data.RowEvents, diff --git a/internal/view/ns_test.go b/internal/view/ns_test.go index 9bfca133..cb36c8ac 100644 --- a/internal/view/ns_test.go +++ b/internal/view/ns_test.go @@ -13,5 +13,5 @@ func TestNSCleanser(t *testing.T) { assert.Nil(t, ns.Init(makeCtx())) assert.Equal(t, "Namespaces", ns.Name()) - assert.Equal(t, 12, len(ns.Hints())) + assert.Equal(t, 13, len(ns.Hints())) } diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go index 4f3c9df9..78e53aae 100644 --- a/internal/view/pod_test.go +++ b/internal/view/pod_test.go @@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) { assert.Nil(t, po.Init(makeCtx())) assert.Equal(t, "Pods", po.Name()) - assert.Equal(t, 24, len(po.Hints())) + assert.Equal(t, 25, len(po.Hints())) } // Helpers... diff --git a/internal/view/port_forward.go b/internal/view/port_forward.go index e0d1cdc3..4e16cdc8 100644 --- a/internal/view/port_forward.go +++ b/internal/view/port_forward.go @@ -51,10 +51,8 @@ func (p *PortForward) bindKeys(aa ui.KeyActions) { tcell.KeyCtrlB: ui.NewKeyAction("Bench", p.benchCmd, true), tcell.KeyCtrlK: ui.NewKeyAction("Bench Stop", p.benchStopCmd, true), tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, true), - // ui.KeySlash: ui.NewKeyAction("Filter", p.activateCmd, false), - tcell.KeyEsc: ui.NewKeyAction("Back", p.App().PrevCmd, false), - ui.KeyShiftP: ui.NewKeyAction("Sort Ports", p.GetTable().SortColCmd(2, true), false), - ui.KeyShiftU: ui.NewKeyAction("Sort URL", p.GetTable().SortColCmd(4, true), false), + ui.KeyShiftP: ui.NewKeyAction("Sort Ports", p.GetTable().SortColCmd(2, true), false), + ui.KeyShiftU: ui.NewKeyAction("Sort URL", p.GetTable().SortColCmd(4, true), false), }) } @@ -78,7 +76,7 @@ func (p *PortForward) benchStopCmd(evt *tcell.EventKey) *tcell.EventKey { } func (p *PortForward) benchCmd(evt *tcell.EventKey) *tcell.EventKey { - sel := p.getSelectedItem() + sel := p.GetTable().GetSelectedItem() if sel == "" { return nil } @@ -89,8 +87,8 @@ func (p *PortForward) benchCmd(evt *tcell.EventKey) *tcell.EventKey { } r, _ := p.GetTable().GetSelection() - cfg, co := defaultConfig(), ui.TrimCell(p.GetTable().SelectTable, r, 2) - if b, ok := p.App().Bench.Benchmarks.Containers[containerID(sel, co)]; ok { + cfg := defaultConfig() + if b, ok := p.App().Bench.Benchmarks.Containers[sel]; ok { cfg = b } cfg.Name = sel @@ -129,27 +127,17 @@ func (p *PortForward) runBenchmark() { }) } -func (p *PortForward) getSelectedItem() string { - r, _ := p.GetTable().GetSelection() - if r == 0 { - return "" - } - return fwFQN( - fqn(ui.TrimCell(p.GetTable().SelectTable, r, 0), ui.TrimCell(p.GetTable().SelectTable, r, 1)), - ui.TrimCell(p.GetTable().SelectTable, r, 2), - ) -} - func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { if !p.GetTable().SearchBuff().Empty() { p.GetTable().SearchBuff().Reset() return nil } - sel := p.getSelectedItem() + sel := p.GetTable().GetSelectedItem() if sel == "" { return nil } + log.Debug().Msgf("PF DELETE %q", sel) showModal(p.App().Content.Pages, fmt.Sprintf("Delete PortForward `%s?", sel), func() { p.App().factory.DeleteForwarder(sel) diff --git a/internal/view/rbac_test.go b/internal/view/rbac_test.go index 31910e14..4193c82f 100644 --- a/internal/view/rbac_test.go +++ b/internal/view/rbac_test.go @@ -13,5 +13,5 @@ func TestRbacNew(t *testing.T) { assert.Nil(t, v.Init(makeCtx())) assert.Equal(t, "Rbac", v.Name()) - assert.Equal(t, 9, len(v.Hints())) + assert.Equal(t, 10, len(v.Hints())) } diff --git a/internal/view/registrar.go b/internal/view/registrar.go index 56e847d7..7684e0fc 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -1,5 +1,12 @@ package view +import ( + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/rs/zerolog/log" +) + func loadCustomViewers() MetaViewers { m := make(MetaViewers, 30) coreRes(m) @@ -7,6 +14,7 @@ func loadCustomViewers() MetaViewers { appsRes(m) rbacRes(m) batchRes(m) + extRes(m) return m } @@ -100,3 +108,22 @@ func batchRes(vv MetaViewers) { viewerFn: NewJob, } } + +func extRes(vv MetaViewers) { + vv["apiextensions.k8s.io/v1/customresourcedefinitions"] = MetaViewer{ + enterFn: showCRD, + } + vv["apiextensions.k8s.io/v1beta1/customresourcedefinitions"] = MetaViewer{ + enterFn: showCRD, + } +} + +func showCRD(app *App, ns, gvr, path string) { + log.Debug().Msgf(">>> CRD View %q -- %q -- %q", ns, gvr, path) + _, crdGVR := client.Namespaced(path) + log.Debug().Msgf("CRD %q", crdGVR) + tokens := strings.Split(crdGVR, ".") + if err := app.gotoResource(tokens[0]); err != nil { + app.Flash().Err(err) + } +} diff --git a/internal/view/screen_dump_test.go b/internal/view/screen_dump_test.go index dbf9ef4e..bb3962df 100644 --- a/internal/view/screen_dump_test.go +++ b/internal/view/screen_dump_test.go @@ -13,5 +13,5 @@ func TestScreenDumpNew(t *testing.T) { assert.Nil(t, po.Init(makeCtx())) assert.Equal(t, "ScreenDumps", po.Name()) - assert.Equal(t, 11, len(po.Hints())) + assert.Equal(t, 12, len(po.Hints())) } diff --git a/internal/view/secret_test.go b/internal/view/secret_test.go index f8d8f788..085e9c4f 100644 --- a/internal/view/secret_test.go +++ b/internal/view/secret_test.go @@ -13,5 +13,5 @@ func TestSecretNew(t *testing.T) { assert.Nil(t, s.Init(makeCtx())) assert.Equal(t, "Secrets", s.Name()) - assert.Equal(t, 12, len(s.Hints())) + assert.Equal(t, 13, len(s.Hints())) } diff --git a/internal/view/sts_test.go b/internal/view/sts_test.go index 8f3260ec..fbc5bc27 100644 --- a/internal/view/sts_test.go +++ b/internal/view/sts_test.go @@ -13,5 +13,5 @@ func TestStatefulSetNew(t *testing.T) { assert.Nil(t, s.Init(makeCtx())) assert.Equal(t, "StatefulSets", s.Name()) - assert.Equal(t, 16, len(s.Hints())) + assert.Equal(t, 17, len(s.Hints())) } diff --git a/internal/view/subject.go b/internal/view/subject.go deleted file mode 100644 index 4db71477..00000000 --- a/internal/view/subject.go +++ /dev/null @@ -1,77 +0,0 @@ -package view - -import ( - "context" - - "github.com/derailed/k9s/internal" - "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" -) - -type ( - TableInfo interface { - Header() render.HeaderRow - GetCache() render.RowEvents - SetCache(render.RowEvents) - } - - // Subject presents a user/group viewer. - Subject struct { - ResourceViewer - - subjectKind string - } -) - -// NewSubject returns a new subject viewer. -func NewSubject(gvr client.GVR) ResourceViewer { - s := Subject{ResourceViewer: NewBrowser(gvr)} - s.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) - // BOZO!! - // s.GetTable().SetSortCol(1, len(s.Header()), true) - s.SetBindKeysFn(s.bindKeys) - s.SetContextFn(s.subjectCtx) - return &s -} - -// Name returns the component name -func (s *Subject) Name() string { - return "subjects" -} - -func (s *Subject) bindKeys(aa ui.KeyActions) { - aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Policies", s.policyCmd, true), - ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.GetTable().SortColCmd(1, true), false), - }) -} - -func (s *Subject) subjectCtx(ctx context.Context) context.Context { - return context.WithValue(ctx, internal.KeySubjectKind, mapSubject(s.subjectKind)) -} - -// SetSubject sets the subject name. -func (s *Subject) SetSubject(n string) { - s.subjectKind = mapSubject(n) -} - -func (s *Subject) policyCmd(evt *tcell.EventKey) *tcell.EventKey { - if !s.GetTable().RowSelected() { - return evt - } - - // BOZO!! - // _, n := client.Namespaced(s.GetSelectedItem()) - // subject, err := mapFuSubject(s.subjectKind) - // if err != nil { - // s.App().Flash().Err(err) - // return nil - // } - // BOZO!! - // s.App().inject(NewPolicy(s.app, subject, n)) - - return nil -} diff --git a/internal/view/subject_test.go b/internal/view/subject_test.go deleted file mode 100644 index 9581a976..00000000 --- a/internal/view/subject_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package view_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/view" - "github.com/stretchr/testify/assert" -) - -func TestSubjectNew(t *testing.T) { - s := view.NewSubject(client.GVR("subjects")) - - assert.Nil(t, s.Init(makeCtx())) - assert.Equal(t, "subjects", s.Name()) - assert.Equal(t, 9, len(s.Hints())) -} diff --git a/internal/view/svc_test.go b/internal/view/svc_test.go index 65996f88..9579eb00 100644 --- a/internal/view/svc_test.go +++ b/internal/view/svc_test.go @@ -132,5 +132,5 @@ func TestServiceNew(t *testing.T) { assert.Nil(t, s.Init(makeCtx())) assert.Equal(t, "Services", s.Name()) - assert.Equal(t, 16, len(s.Hints())) + assert.Equal(t, 17, len(s.Hints())) } diff --git a/internal/view/table.go b/internal/view/table.go index 811ac996..4b242368 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -2,6 +2,7 @@ package view import ( "context" + "time" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" @@ -16,15 +17,14 @@ type Table struct { enterFn EnterFunc } -func NewTable(title string) *Table { +func NewTable(gvr string) *Table { return &Table{ - Table: ui.NewTable(title), + Table: ui.NewTable(gvr), } } // Init initializes the component func (t *Table) Init(ctx context.Context) (err error) { - log.Debug().Msgf(">>>> Table INIT %s", t.BaseTitle) if t.app, err = extractApp(ctx); err != nil { return err } @@ -32,6 +32,8 @@ func (t *Table) Init(ctx context.Context) (err error) { t.Table.Init(ctx) t.bindKeys() + t.GetModel().SetRefreshRate(time.Duration(t.app.Config.K9s.GetRefreshRate()) * time.Second) + return nil } @@ -45,14 +47,13 @@ func (t *Table) App() *App { // Start runs the component. func (t *Table) Start() { - log.Debug().Msgf("Table START %s", t.BaseTitle) + t.Stop() t.SearchBuff().AddListener(t.app.Cmd()) t.SearchBuff().AddListener(t) } // Stop terminates the component. func (t *Table) Stop() { - log.Debug().Msgf("TABLE %s", t.BaseTitle) t.SearchBuff().RemoveListener(t.app.Cmd()) t.SearchBuff().RemoveListener(t) } @@ -85,9 +86,9 @@ func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { func (t *Table) bindKeys() { t.Actions().Add(ui.KeyActions{ - ui.KeySpace: ui.NewKeyAction("Mark", t.markCmd, true), - tcell.KeyCtrlSpace: ui.NewKeyAction("Marks Clear", t.clearMarksCmd, true), - tcell.KeyCtrlS: ui.NewKeyAction("Save", t.saveCmd, true), + ui.KeySpace: ui.NewKeyAction("Mark", t.markCmd, false), + tcell.KeyCtrlSpace: ui.NewKeyAction("Marks Clear", t.clearMarksCmd, false), + tcell.KeyCtrlS: ui.NewKeyAction("Save", t.saveCmd, false), ui.KeySlash: ui.NewKeyAction("Filter Mode", t.activateCmd, false), tcell.KeyEscape: ui.NewKeyAction("Filter Reset", t.resetCmd, false), tcell.KeyEnter: ui.NewKeyAction("Filter", t.filterCmd, false), @@ -100,7 +101,8 @@ func (t *Table) bindKeys() { } func (t *Table) markCmd(evt *tcell.EventKey) *tcell.EventKey { - if !t.RowSelected() { + path := t.GetSelectedItem() + if path == "" { return evt } t.ToggleMark() @@ -110,7 +112,8 @@ func (t *Table) markCmd(evt *tcell.EventKey) *tcell.EventKey { } func (t *Table) clearMarksCmd(evt *tcell.EventKey) *tcell.EventKey { - if !t.RowSelected() { + path := t.GetSelectedItem() + if path == "" { return evt } t.ClearMarks() @@ -147,6 +150,7 @@ func (t *Table) resetCmd(evt *tcell.EventKey) *tcell.EventKey { t.SearchBuff().Reset() return t.app.PrevCmd(evt) } + if ui.IsLabelSelector(t.SearchBuff().String()) { t.filterFn("") } diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index d37d6fd5..a8ee838f 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -5,8 +5,10 @@ import ( "io/ioutil" "path/filepath" "testing" + "time" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" @@ -59,29 +61,7 @@ func TestTableNew(t *testing.T) { func TestTableViewFilter(t *testing.T) { v := NewTable("test") v.Init(makeContext()) - - data := render.TableData{ - Header: render.HeaderRow{ - render.Header{Name: "NAMESPACE"}, - render.Header{Name: "NAME", Align: tview.AlignRight}, - render.Header{Name: "FRED"}, - render.Header{Name: "AGE", Decorator: render.AgeDecorator}, - }, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "blee", "10", "3m"}, - }, - }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "fred", "15", "1m"}, - }, - }, - }, - Namespace: "", - } - v.Update(data) + v.SetModel(&testTableModel{}) v.SearchBuff().SetActive(true) v.SearchBuff().Set("blee") v.filterCmd(nil) @@ -93,8 +73,35 @@ func TestTableViewFilter(t *testing.T) { func TestTableViewSort(t *testing.T) { v := NewTable("test") v.Init(makeContext()) + v.SetModel(&testTableModel{}) + v.SortColCmd(1, true)(nil) + assert.Equal(t, 3, v.GetRowCount()) + assert.Equal(t, "blee", v.GetCell(1, 1).Text) - data := render.TableData{ + v.SortInvertCmd(nil) + assert.Equal(t, 3, v.GetRowCount()) + assert.Equal(t, "fred", v.GetCell(1, 1).Text) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type testTableModel struct{} + +var _ ui.Tabular = &testTableModel{} + +func (t *testTableModel) Empty() bool { return false } +func (t *testTableModel) Peek() render.TableData { return makeTableData() } +func (t *testTableModel) ClusterWide() bool { return false } +func (t *testTableModel) GetNamespace() string { return "blee" } +func (t *testTableModel) SetNamespace(string) {} +func (t *testTableModel) AddListener(model.TableListener) {} +func (t *testTableModel) Start(context.Context) {} +func (t *testTableModel) InNamespace(string) bool { return true } +func (t *testTableModel) SetRefreshRate(time.Duration) {} + +func makeTableData() render.TableData { + return render.TableData{ Header: render.HeaderRow{ render.Header{Name: "NAMESPACE"}, render.Header{Name: "NAME", Align: tview.AlignRight}, @@ -116,18 +123,8 @@ func TestTableViewSort(t *testing.T) { }, Namespace: "", } - v.Update(data) - v.SortColCmd(1, true)(nil) - assert.Equal(t, 3, v.GetRowCount()) - assert.Equal(t, "blee", v.GetCell(1, 1).Text) - - v.SortInvertCmd(nil) - assert.Equal(t, 3, v.GetRowCount()) - assert.Equal(t, "fred", v.GetCell(1, 1).Text) } -// Helpers... - func makeContext() context.Context { a := NewApp(config.NewConfig(ks{})) ctx := context.WithValue(context.Background(), ui.KeyApp, a) diff --git a/internal/view/user.go b/internal/view/user.go index 6969eed7..6a363c2e 100644 --- a/internal/view/user.go +++ b/internal/view/user.go @@ -17,31 +17,33 @@ type User struct { // NewUser returns a new subject viewer. func NewUser(gvr client.GVR) ResourceViewer { - s := User{ResourceViewer: NewBrowser(gvr)} - s.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) - s.SetBindKeysFn(s.bindKeys) - s.SetContextFn(s.subjectCtx) - return &s + u := User{ResourceViewer: NewBrowser(gvr)} + u.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) + u.SetBindKeysFn(u.bindKeys) + u.SetContextFn(u.subjectCtx) + + return &u } -func (s *User) bindKeys(aa ui.KeyActions) { +func (u *User) bindKeys(aa ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace) aa.Add(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Rules", s.policyCmd, true), - ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.GetTable().SortColCmd(1, true), false), + tcell.KeyEnter: ui.NewKeyAction("Rules", u.policyCmd, true), + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", u.GetTable().SortColCmd(1, true), false), }) } -func (s *User) subjectCtx(ctx context.Context) context.Context { +func (u *User) subjectCtx(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeySubjectKind, "User") } -func (s *User) policyCmd(evt *tcell.EventKey) *tcell.EventKey { - if !s.GetTable().RowSelected() { +func (u *User) policyCmd(evt *tcell.EventKey) *tcell.EventKey { + path := u.GetTable().GetSelectedItem() + if path == "" { return evt } - if err := s.App().inject(NewPolicy(s.App(), "User", s.GetTable().GetSelectedItem())); err != nil { - s.App().Flash().Err(err) + if err := u.App().inject(NewPolicy(u.App(), "User", path)); err != nil { + u.App().Flash().Err(err) } return nil diff --git a/internal/watch/factory.go b/internal/watch/factory.go index d1512637..b93001c2 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -15,7 +15,6 @@ import ( "k8s.io/client-go/informers" ) -// Factory - *factories(ns) -> *informers const ( defaultResync = 10 * time.Minute allNamespaces = "" @@ -41,19 +40,16 @@ func NewFactory(client client.Connection) *Factory { } } +func (f *Factory) String() string { + return fmt.Sprintf("Factory ActiveNS %s", f.activeNS) +} + +// List returns a resource collection. func (f *Factory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error) { - auth, err := f.Client().CanI(ns, gvr, []string{"list"}) + inf, err := f.CanForResource(ns, gvr, "list") if err != nil { return nil, err } - if !auth { - return nil, fmt.Errorf("User has insufficient access to list %s", gvr) - } - - inf := f.ForResource(ns, gvr) - if inf == nil { - return nil, fmt.Errorf("No resource for GVR %s", gvr) - } if ns == clusterScope { return inf.Lister().List(sel) } @@ -61,38 +57,33 @@ func (f *Factory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, e return inf.Lister().ByNamespace(ns).List(sel) } +// Get retrieves a given resource. func (f *Factory) Get(gvr, path string, sel labels.Selector) (runtime.Object, error) { ns, n := namespaced(path) - auth, err := f.Client().CanI(ns, gvr, []string{"get"}) + inf, err := f.CanForResource(ns, gvr, "get") if err != nil { return nil, err } - if !auth { - return nil, fmt.Errorf("User has insufficient access to get %s", gvr) - } - - inf := f.ForResource(ns, gvr) - if inf == nil { - return nil, fmt.Errorf("No resource for GVR %s", gvr) - } if ns == clusterScope { return inf.Lister().Get(n) } - log.Debug().Msgf("GET %q--%q:%q", gvr, ns, path) return inf.Lister().ByNamespace(ns).Get(n) } +// WaitForCachesync waits for all factories to update their cache. func (f *Factory) WaitForCacheSync() { for _, fac := range f.factories { fac.WaitForCacheSync(f.stopChan) } } +// Init starts a factory. func (f *Factory) Init() { f.Start(f.stopChan) } +// Terminate terminates all watchers and forwards. func (f *Factory) Terminate() { if f.stopChan != nil { close(f.stopChan) @@ -104,17 +95,23 @@ func (f *Factory) Terminate() { f.forwarders.DeleteAll() } -// DeleteForwarder deletes portforward for a given container. -func (f *Factory) DeleteForwarder(path string) { - if fwd, ok := f.forwarders[path]; ok { - fwd.Stop() - delete(f.forwarders, path) - } +// RegisterForwarder registers a new portforward for a given container. +func (f *Factory) AddForwarder(pf Forwarder) { + f.forwarders[pf.Path()] = pf + f.forwarders.Dump() } -// RegisterForwarder registers a new portforward for a given container. -func (f *Factory) RegisterForwarder(pf Forwarder) { - f.forwarders[pf.Path()] = pf +// DeleteForwarder deletes portforward for a given container. +func (f *Factory) DeleteForwarder(path string) { + f.forwarders.Dump() + fwd, ok := f.forwarders[path] + if !ok { + log.Warn().Msgf("Unable to delete portForward %q", path) + return + } + fwd.Stop() + delete(f.forwarders, path) + f.forwarders.Dump() } // Forwards returns all portforwards. @@ -150,20 +147,32 @@ func (f *Factory) isClusterWide() bool { } func (f *Factory) preload(ns string) { - f.ForResource(ns, "v1/pods") - f.ForResource(allNamespaces, "apiextensions.k8s.io/v1beta1/customresourcedefinitions") - f.ForResource(clusterScope, "rbac.authorization.k8s.io/v1/clusterroles") - f.ForResource(allNamespaces, "rbac.authorization.k8s.io/v1/roles") + verbs := []string{"get", "list", "watch"} + _, _ = f.CanForResource(ns, "v1/pods", verbs...) + _, _ = f.CanForResource(allNamespaces, "apiextensions.k8s.io/v1beta1/customresourcedefinitions", verbs...) + _, _ = f.CanForResource(clusterScope, "rbac.authorization.k8s.io/v1/clusterroles", verbs...) + _, _ = f.CanForResource(allNamespaces, "rbac.authorization.k8s.io/v1/roles", verbs...) } +// CanForResource return an informer is user has access. +func (f *Factory) CanForResource(ns, gvr string, verbs ...string) (informers.GenericInformer, error) { + auth, err := f.Client().CanI(ns, gvr, verbs) + if err != nil { + return nil, err + } + if !auth { + return nil, fmt.Errorf("%v access denied on resource %q:%q", verbs, ns, gvr) + } + + return f.ForResource(ns, gvr), nil +} + +// FactoryFor returns a factory for a given namespace. func (f *Factory) FactoryFor(ns string) di.DynamicSharedInformerFactory { return f.factories[ns] } -func (f *Factory) Preload(ns, gvr string) { - _ = f.ForResource(ns, gvr) -} - +// ForResource returns an informer for a given resource. func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer { fact := f.ensureFactory(ns) inf := fact.ForResource(toGVR(gvr)) @@ -192,7 +201,6 @@ func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory { } func toGVR(gvr string) schema.GroupVersionResource { - log.Debug().Msgf("GVR -- %q", gvr) tokens := strings.Split(gvr, "/") if len(tokens) < 3 { tokens = append([]string{""}, tokens...) diff --git a/internal/watch/forwarders.go b/internal/watch/forwarders.go index 2b6fe9d3..801dbeb6 100644 --- a/internal/watch/forwarders.go +++ b/internal/watch/forwarders.go @@ -50,18 +50,18 @@ func (ff Forwarders) DeleteAll() { } // Kill stops and delete a port-forwards associated with pod. -func (ff Forwarders) Kill(pod string) int { +func (ff Forwarders) Kill(path string) int { ff.Dump() - log.Debug().Msgf("Delete port-forward %q", pod) - hasContainer := strings.Contains(pod, ":") + log.Debug().Msgf("Delete port-forward %q", path) + hasContainer := strings.Contains(path, ":") var stats int for k, f := range ff { victim := k if !hasContainer { victim = strings.Split(k, ":")[0] } - if victim == pod { + if victim == path { stats++ log.Debug().Msgf("Stopping and delete port-forward %s", k) f.Stop()