checkpoint

mine
derailed 2019-12-28 12:22:22 -07:00
parent 99fa0e9952
commit c26c80e170
62 changed files with 934 additions and 545 deletions

1
go.mod
View File

@ -57,6 +57,7 @@ require (
gopkg.in/yaml.v2 v2.2.4 gopkg.in/yaml.v2 v2.2.4
gotest.tools v2.2.0+incompatible gotest.tools v2.2.0+incompatible
k8s.io/api v0.0.0 k8s.io/api v0.0.0
k8s.io/apiextensions-apiserver v0.0.0
k8s.io/apimachinery v0.0.0 k8s.io/apimachinery v0.0.0
k8s.io/cli-runtime v0.0.0 k8s.io/cli-runtime v0.0.0
k8s.io/client-go v0.0.0 k8s.io/client-go v0.0.0

1
go.sum
View File

@ -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= 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 h1:8FRUST8oUkEI45WYKyD8ed7Ad0Kg5v11zHyPkEVb2xo=
k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= 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/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 h1:CS1tBQz3HOXiseWZu6ZicKX361CZLT97UFnnPx0aqBw=
k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4=

View File

@ -66,7 +66,7 @@ func (p *PortForwarder) Ports() []string {
// Path returns the pod resource path. // Path returns the pod resource path.
func (p *PortForwarder) Path() string { func (p *PortForwarder) Path() string {
return p.path return p.path + ":" + p.container
} }
// Container returns the targetes container. // Container returns the targetes container.
@ -76,7 +76,7 @@ func (p *PortForwarder) Container() string {
// Stop terminates a port forard // Stop terminates a port forard
func (p *PortForwarder) Stop() { 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 p.active = false
close(p.stopChan) close(p.stopChan)
} }

View File

@ -187,7 +187,8 @@ func loadPreferred(f Factory, m ResourceMetas) error {
func loadCRDs(f Factory, m ResourceMetas) error { func loadCRDs(f Factory, m ResourceMetas) error {
oo, err := f.List("apiextensions.k8s.io/v1beta1/customresourcedefinitions", "", labels.Everything()) oo, err := f.List("apiextensions.k8s.io/v1beta1/customresourcedefinitions", "", labels.Everything())
if err != nil { if err != nil {
return err log.Error().Err(err).Msgf("Fail CRDs load")
return nil
} }
f.WaitForCacheSync() f.WaitForCacheSync()

View File

@ -6,18 +6,19 @@ type ContextKey string
// A collection of context keys. // A collection of context keys.
const ( const (
KeyFactory ContextKey = "factory" KeyFactory ContextKey = "factory"
KeyLabels ContextKey = "labels" KeyLabels = "labels"
KeyFields ContextKey = "fields" KeyFields = "fields"
KeyTable ContextKey = "table" KeyTable = "table"
KeyDir ContextKey = "dir" KeyDir = "dir"
KeyPath ContextKey = "path" KeyPath = "path"
KeySubject ContextKey = "subject" KeySubject = "subject"
KeyGVR ContextKey = "gvr" KeyGVR = "gvr"
KeyForwards ContextKey = "forwards" KeyForwards = "forwards"
KeyContainers ContextKey = "containers" KeyContainers = "containers"
KeyBenchCfg ContextKey = "benchcfg" KeyBenchCfg = "benchcfg"
KeyAliases ContextKey = "aliases" KeyAliases = "aliases"
KeyUID ContextKey = "uid" KeyUID = "uid"
KeySubjectKind ContextKey = "subjectKind" KeySubjectKind = "subjectKind"
KeySubjectName ContextKey = "subjectName" KeySubjectName = "subjectName"
KeyNamespace = "namespace"
) )

View File

@ -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 { func (f testFactory) ForResource(ns, gvr string) informers.GenericInformer {
return nil return nil
} }
func (f testFactory) CanForResource(ns, gvr string, verbs ...string) (informers.GenericInformer, error) {
return nil, nil
}
func (f testFactory) WaitForCacheSync() {} func (f testFactory) WaitForCacheSync() {}
func (f testFactory) Forwarders() watch.Forwarders { func (f testFactory) Forwarders() watch.Forwarders {
return nil return nil

View File

@ -63,6 +63,9 @@ func (f podFactory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object,
return nil, nil return nil, nil
} }
func (f podFactory) ForResource(ns, gvr string) informers.GenericInformer { return nil } func (f podFactory) ForResource(ns, gvr string) informers.GenericInformer { return nil }
func (f podFactory) CanForResource(ns, gvr string, verbs ...string) (informers.GenericInformer, error) {
return nil, nil
}
func (f podFactory) WaitForCacheSync() {} func (f podFactory) WaitForCacheSync() {}
func (f podFactory) Forwarders() watch.Forwarders { return nil } func (f podFactory) Forwarders() watch.Forwarders { return nil }

View File

@ -32,7 +32,10 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) {
}(time.Now()) }(time.Now())
// Ensures the factory is tracking this resource // 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) gvr := client.GVR(g.gvr)
fcodec, codec := g.codec(gvr.AsGV()) fcodec, codec := g.codec(gvr.AsGV())
@ -49,7 +52,9 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) {
Resource(gvr.ToR()). Resource(gvr.ToR()).
VersionedParams(&metav1beta1.TableOptions{}, codec). VersionedParams(&metav1beta1.TableOptions{}, codec).
Do().Get() Do().Get()
if err != nil {
return nil, err
}
table, ok := o.(*metav1beta1.Table) table, ok := o.(*metav1beta1.Table)
if !ok { if !ok {
return nil, fmt.Errorf("expecting table but got %T", o) return nil, fmt.Errorf("expecting table but got %T", o)

View File

@ -29,6 +29,7 @@ func (c *Job) List(ctx context.Context) ([]runtime.Object, error) {
return nil, errors.New("no cronjob path found in context") 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) oo, err := c.Resource.List(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -45,18 +46,13 @@ func (c *Job) List(ctx context.Context) ([]runtime.Object, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debug().Msgf("Looking at job %q -- %q", job.Name, cronName)
if !isNamedAfter(cronName, job.Name) { if !isNamedAfter(cronName, job.Name) {
continue continue
} }
id, ok := job.Spec.Selector.MatchLabels["controller-uid"] log.Debug().Msgf("GOT Job %s -- %#v -- %q -- %q", job.Name, job.Labels, uid, path)
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) jj = append(jj, j)
} }
}
return jj, nil return jj, nil
} }

View File

@ -31,7 +31,7 @@ func (n *Node) List(_ context.Context) ([]runtime.Object, error) {
oo := make([]runtime.Object, len(nn.Items)) oo := make([]runtime.Object, len(nn.Items))
for i, n := range nn.Items { for i, n := range nn.Items {
o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(n) o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&n)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -19,12 +19,12 @@ func Reconcile(ctx context.Context, table render.TableData, gvr client.GVR) (ren
path, ok := ctx.Value(internal.KeyPath).(string) path, ok := ctx.Value(internal.KeyPath).(string)
if !ok { 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) log.Debug().Msgf("Reconcile %q in ns %q with path %q", gvr, table.Namespace, path)
factory, ok := ctx.Value(internal.KeyFactory).(Factory) factory, ok := ctx.Value(internal.KeyFactory).(Factory)
if !ok { 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)] m, ok := Registry[string(gvr)]
if !ok { 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) m.Model.Init(table.Namespace, string(gvr), factory)
table.Header = m.Renderer.Header(table.Namespace)
oo, err := m.Model.List(ctx) oo, err := m.Model.List(ctx)
if err != nil { if err != nil {
return table, err return table, err
@ -51,6 +50,7 @@ func Reconcile(ctx context.Context, table render.TableData, gvr client.GVR) (ren
return table, err return table, err
} }
update(&table, rows) update(&table, rows)
table.Header = m.Renderer.Header(table.Namespace)
log.Debug().Msgf("Table returned [%d] events", len(table.RowEvents)) log.Debug().Msgf("Table returned [%d] events", len(table.RowEvents))
return table, nil return table, nil

185
internal/model/table.go Normal file
View File

@ -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
}

View File

@ -82,6 +82,9 @@ type Factory interface {
// ForResource fetch an informer for a given resource. // ForResource fetch an informer for a given resource.
ForResource(ns, gvr string) informers.GenericInformer 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 synchronize the cache.
WaitForCacheSync() WaitForCacheSync()

38
internal/render/color.go Normal file
View File

@ -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
}

View File

@ -5,8 +5,10 @@ import (
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
// ext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
// "k8s.io/apimachinery/pkg/runtime"
) )
// CustomResourceDefinition renders a K8s CustomResourceDefinition to screen. // 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) 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{}) meta, ok := crd.Object["metadata"].(map[string]interface{})
if !ok { if !ok {
return fmt.Errorf("expecting an interface map but got %T", crd.Object["metadata"]) return fmt.Errorf("expecting an interface map but got %T", crd.Object["metadata"])

View File

@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/rs/zerolog/log"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" 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)}) h = append(h, Header{Name: strings.ToUpper(c.Name)})
} }
log.Debug().Msgf("Generic Header %#v", h)
return h return h
} }
@ -69,12 +67,10 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
r.ID = FQN(rns, r.ID) r.ID = FQN(rns, r.ID)
index++ index++
} }
for _, c := range row.Cells { for _, c := range row.Cells {
r.Fields[index] = fmt.Sprintf("%v", c) r.Fields[index] = fmt.Sprintf("%v", c)
index++ index++
} }
log.Debug().Msgf("Generic row %#v", r)
return nil return nil
} }

View File

@ -41,7 +41,7 @@ func (PortForward) ColorerFunc() ColorerFunc {
func (PortForward) Header(ns string) HeaderRow { func (PortForward) Header(ns string) HeaderRow {
return HeaderRow{ return HeaderRow{
Header{Name: "NAMESPACE"}, Header{Name: "NAMESPACE"},
Header{Name: "NAME"}, Header{Name: "POD"},
Header{Name: "CONTAINER"}, Header{Name: "CONTAINER"},
Header{Name: "PORTS"}, Header{Name: "PORTS"},
Header{Name: "URL"}, Header{Name: "URL"},
@ -59,12 +59,12 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
} }
ports := strings.Split(pf.Ports()[0], ":") ports := strings.Split(pf.Ports()[0], ":")
ns, na := Namespaced(pf.Path()) ns, n := Namespaced(pf.Path())
r.ID = pf.Path() r.ID = pf.Path()
r.Fields = Fields{ r.Fields = Fields{
ns, ns,
na, trimContainer(n),
pf.Container(), pf.Container(),
strings.Join(pf.Ports(), ","), strings.Join(pf.Ports(), ","),
UrlFor(pf.Config.Host, pf.Config.Path, ports[0]), 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... // 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. // UrlFor computes fq url for a given benchmark configuration.
func UrlFor(host, path, port string) string { func UrlFor(host, path, port string) string {
if host == "" { if host == "" {

View File

@ -7,65 +7,32 @@ import (
"vbom.ml/util/sortorder" "vbom.ml/util/sortorder"
) )
const ageCol = "AGE"
// Fields represents a collection of row fields. // Fields represents a collection of row fields.
type Fields []string 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. // Row represents a colllection of columns.
type Row struct { type Row struct {
ID string ID string
Fields Fields Fields Fields
} }
// Rows represents a collection of rows. // NewRow returns a new row with initialized fields.
type Rows []Row func NewRow(cols int) Row {
return Row{Fields: make([]string, cols)}
// 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
} }
// Clone copies a row.
func (r Row) Clone() Row { func (r Row) Clone() Row {
return Row{ return Row{
ID: r.ID, 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. // Delete removes an element by id.
func (rr Rows) Delete(id string) Rows { 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:]...) 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 { func (rr Rows) Upsert(r Row) Rows {
idx, ok := rr.Find(r.ID) idx, ok := rr.Find(r.ID)
if !ok { if !ok {
@ -131,6 +89,15 @@ func (rr Rows) Sort(col int, asc bool) {
sort.Sort(t) sort.Sort(t)
} }
// ----------------------------------------------------------------------------
// RowSorter sorts rows.
type RowSorter struct {
Rows Rows
Index int
Asc bool
}
func (s RowSorter) Len() int { func (s RowSorter) Len() int {
return len(s.Rows) 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]) 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 { func Less(asc bool, c1, c2 string) bool {
if o, ok := isDurationSort(asc, c1, c2); ok { if o, ok := isDurationSort(asc, c1, c2); ok {
return o return o

View File

@ -2,9 +2,9 @@ package render
import ( import (
"fmt" "fmt"
"reflect"
"sort" "sort"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -35,9 +35,6 @@ type RowEvent struct {
Deltas DeltaRow Deltas DeltaRow
} }
// RowEvents a collection of row events.
type RowEvents []RowEvent
// NewRowEvent returns a new row event. // NewRowEvent returns a new row event.
func NewRowEvent(kind ResEvent, row Row) RowEvent { func NewRowEvent(kind ResEvent, row Row) RowEvent {
return 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. // Clone returns a rowevents deep copy.
func (rr RowEvents) Clone() RowEvents { func (rr RowEvents) Clone() RowEvents {
res := make(RowEvents, len(rr)) res := make(RowEvents, len(rr))
@ -103,10 +133,7 @@ func (rr RowEvents) Delete(id string) RowEvents {
// Clear delete all row events // Clear delete all row events
func (rr RowEvents) Clear() RowEvents { func (rr RowEvents) Clear() RowEvents {
for _, e := range rr { return RowEvents{}
rr = rr.Delete(e.Row.ID)
}
return rr
} }
// FindIndex locates a row index by id. Returns false is not found. // 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 type StringSet []string
func (ss StringSet) Add(item string) StringSet { func (ss StringSet) Add(item string) StringSet {

View File

@ -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
}

View File

@ -1,13 +1,23 @@
package render_test package render_test
import ( import (
"fmt"
"reflect"
"testing" "testing"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert" "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 { uu := map[string]struct {
rows render.Rows rows render.Rows
id string 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 { uu := map[string]struct {
rows render.Rows rows render.Rows
col int 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 { uu := map[string]struct {
rows render.Rows rows render.Rows
col int 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 { uu := map[string]struct {
rows render.Rows rows render.Rows
col int col int

View File

@ -6,3 +6,78 @@ type TableData struct {
RowEvents RowEvents RowEvents RowEvents
Namespace string 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
}

View File

@ -12,9 +12,7 @@ type App struct {
Configurator Configurator
Main *Pages Main *Pages
actions KeyActions actions KeyActions
views map[string]tview.Primitive views map[string]tview.Primitive
cmdBuff *CmdBuff cmdBuff *CmdBuff
} }

View File

@ -76,7 +76,7 @@ func (v *CmdView) BufferActive(f bool, k BufferKind) {
v.SetTextColor(v.styles.FgColor()) v.SetTextColor(v.styles.FgColor())
v.SetBorderColor(colorFor(k)) v.SetBorderColor(colorFor(k))
v.icon = iconFor(k) v.icon = iconFor(k)
v.reset() // v.reset()
v.activate() v.activate()
} else { } else {
v.SetBorder(false) v.SetBorder(false)

View File

@ -1,5 +1,7 @@
package ui package ui
import "github.com/rs/zerolog/log"
const maxBuff = 10 const maxBuff = 10
const ( const (
@ -65,6 +67,7 @@ func (c *CmdBuff) IsActive() bool {
// SetActive toggles cmd buffer active state. // SetActive toggles cmd buffer active state.
func (c *CmdBuff) SetActive(b bool) { func (c *CmdBuff) SetActive(b bool) {
log.Debug().Msgf("CMDBUFF -- Active %t", b)
c.active = b c.active = b
c.fireActive(c.active) c.fireActive(c.active)
} }
@ -143,7 +146,9 @@ func (c *CmdBuff) fireChanged() {
} }
func (c *CmdBuff) fireActive(b bool) { func (c *CmdBuff) fireActive(b bool) {
log.Debug().Msgf("CMDBUFF LIST SIZE %d", len(c.listeners))
for _, l := range c.listeners { for _, l := range c.listeners {
log.Debug().Msgf("CMDBUFF LIST -- %T", l)
l.BufferActive(b, c.kind) l.BufferActive(b, c.kind)
} }
} }

View File

@ -1,23 +1,48 @@
package ui package ui
import ( import (
"context"
"time"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/tview" "github.com/derailed/tview"
"github.com/gdamore/tcell" "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. // Selectable represents a table with selections.
type SelectTable struct { type SelectTable struct {
*tview.Table *tview.Table
Data render.TableData model Tabular
selectedItem string
selectedRow int selectedRow int
selectedFn func(string) string selectedFn func(string) string
selListeners []SelectedRowFunc selectionListeners []SelectedRowFunc
marks map[string]bool 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. // ClearSelection reset selected row.
func (s *SelectTable) ClearSelection() { func (s *SelectTable) ClearSelection() {
s.Select(0, 0) s.Select(0, 0)
@ -49,10 +74,17 @@ func (s *SelectTable) GetSelectedItems() []string {
// GetSelectedItem returns the currently selected item name. // GetSelectedItem returns the currently selected item name.
func (s *SelectTable) GetSelectedItem() string { func (s *SelectTable) GetSelectedItem() string {
if s.selectedFn != nil { if s.GetSelectedRowIndex() == 0 || s.model.Empty() {
return s.selectedFn(s.selectedItem) 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. // GetSelectedCell returns the content of a cell for the currently selected row.
@ -70,36 +102,13 @@ func (s *SelectTable) GetSelectedRowIndex() int {
return s.selectedRow 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. // SelectRow select a given row by index.
func (s *SelectTable) SelectRow(r int, broadcast bool) { func (s *SelectTable) SelectRow(r int, broadcast bool) {
if !broadcast { if !broadcast {
s.SetSelectionChangedFunc(nil) s.SetSelectionChangedFunc(nil)
} }
defer s.SetSelectionChangedFunc(s.selChanged) defer s.SetSelectionChangedFunc(s.selectionChanged)
s.Select(r, 0) s.Select(r, 0)
s.updateSelectedItem(r)
} }
// UpdateSelection refresh selected row. // UpdateSelection refresh selected row.
@ -107,9 +116,8 @@ func (s *SelectTable) updateSelection(broadcast bool) {
s.SelectRow(s.selectedRow, broadcast) s.SelectRow(s.selectedRow, broadcast)
} }
func (s *SelectTable) selChanged(r, c int) { func (s *SelectTable) selectionChanged(r, c int) {
s.selectedRow = r s.selectedRow = r
s.updateSelectedItem(r)
if r == 0 { if r == 0 {
return return
} }
@ -121,8 +129,8 @@ func (s *SelectTable) selChanged(r, c int) {
s.SetSelectedStyle(tcell.ColorBlack, cell.Color, tcell.AttrBold) s.SetSelectedStyle(tcell.ColorBlack, cell.Color, tcell.AttrBold)
} }
for _, f := range s.selListeners { for _, f := range s.selectionListeners {
f(r, c) f(r)
} }
} }
@ -159,5 +167,5 @@ func (s *Table) IsMarked(item string) bool {
// AddSelectedRowListener add a new selected row listener. // AddSelectedRowListener add a new selected row listener.
func (s *SelectTable) AddSelectedRowListener(f SelectedRowFunc) { func (s *SelectTable) AddSelectedRowListener(f SelectedRowFunc) {
s.selListeners = append(s.selListeners, f) s.selectionListeners = append(s.selectionListeners, f)
} }

View File

@ -20,7 +20,7 @@ type (
DecorateFunc func(render.TableData) render.TableData DecorateFunc func(render.TableData) render.TableData
// SelectedRowFunc a table selection callback. // SelectedRowFunc a table selection callback.
SelectedRowFunc func(r, c int) SelectedRowFunc func(r int)
) )
// Table represents tabular data. // Table represents tabular data.
@ -38,15 +38,16 @@ type Table struct {
} }
// NewTable returns a new table view. // NewTable returns a new table view.
func NewTable(title string) *Table { func NewTable(gvr string) *Table {
return &Table{ return &Table{
SelectTable: &SelectTable{ SelectTable: &SelectTable{
Table: tview.NewTable(), Table: tview.NewTable(),
model: model.NewTable(gvr),
marks: make(map[string]bool), marks: make(map[string]bool),
}, },
actions: make(KeyActions), actions: make(KeyActions),
cmdBuff: NewCmdBuff('/', FilterBuff), cmdBuff: NewCmdBuff('/', FilterBuff),
BaseTitle: title, BaseTitle: gvr,
sortCol: SortColumn{index: -1, colCount: 0, asc: true}, 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), config.AsColor(t.styles.GetTable().CursorColor),
tcell.AttrBold, tcell.AttrBold,
) )
t.SetSelectionChangedFunc(t.selChanged) t.SetSelectionChangedFunc(t.selectionChanged)
t.SetInputCapture(t.keyboard) t.SetInputCapture(t.keyboard)
} }
@ -91,7 +92,8 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if t.SearchBuff().IsActive() { if t.SearchBuff().IsActive() {
t.SearchBuff().Add(evt.Rune()) t.SearchBuff().Add(evt.Rune())
t.ClearSelection() 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.UpdateTitle()
t.SelectFirstRow() t.SelectFirstRow()
return nil return nil
@ -112,7 +114,7 @@ func (t *Table) Hints() model.MenuHints {
// GetFilteredData fetch filtered tabular data. // GetFilteredData fetch filtered tabular data.
func (t *Table) GetFilteredData() render.TableData { func (t *Table) GetFilteredData() render.TableData {
return t.filtered(t.Data) return t.filtered(t.GetModel().Peek())
} }
// SetDecorateFn specifies the default row decorator. // SetDecorateFn specifies the default row decorator.
@ -133,10 +135,9 @@ func (t *Table) SetSortCol(index, count int, asc bool) {
// Update table content. // Update table content.
func (t *Table) Update(data render.TableData) { func (t *Table) Update(data render.TableData) {
var firstRow bool var firstRow bool
if len(t.Data.RowEvents) == 0 { if t.GetRowCount() == 0 {
firstRow = true firstRow = true
} }
t.Data = data
if t.decorateFn != nil { if t.decorateFn != nil {
data = t.decorateFn(data) data = t.decorateFn(data)
@ -176,7 +177,7 @@ func (t *Table) doUpdate(data render.TableData, firstRow bool) {
if firstRow { if firstRow {
t.SelectFirstRow() t.SelectFirstRow()
} }
t.updateSelection(false) t.updateSelection(true)
} }
// SortColCmd designates a sorted column. // 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!") log.Debug().Msgf("Marked!")
c.SetTextColor(config.AsColor(t.styles.GetTable().MarkColor)) c.SetTextColor(config.AsColor(t.styles.GetTable().MarkColor))
} }
if col == 0 {
c.SetReference(re.Row.ID)
}
t.SetCell(r, col, c) t.SetCell(r, col, c)
} }
} }
@ -261,13 +265,17 @@ func (t *Table) ClearMarks() {
// Refresh update the table data. // Refresh update the table data.
func (t *Table) Refresh() { 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. // NameColIndex returns the index of the resource name column.
func (t *Table) NameColIndex() int { func (t *Table) NameColIndex() int {
col := 0 col := 0
if t.Data.Namespace == render.AllNamespaces { if t.GetModel().ClusterWide() {
col++ col++
} }
return col return col
@ -315,7 +323,7 @@ func (t *Table) ShowDeleted() {
// UpdateTitle refreshes the table title. // UpdateTitle refreshes the table title.
func (t *Table) UpdateTitle() { func (t *Table) UpdateTitle() {
ns := t.Data.Namespace ns := t.GetModel().GetNamespace()
if ns == render.AllNamespaces { if ns == render.AllNamespaces {
ns = render.NamespaceAll ns = render.NamespaceAll
} }

View File

@ -3,8 +3,10 @@ package ui_test
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -36,11 +38,13 @@ func TestTableSelection(t *testing.T) {
s, _ := config.NewStyles("") s, _ := config.NewStyles("")
ctx := context.WithValue(context.Background(), ui.KeyStyles, s) ctx := context.WithValue(context.Background(), ui.KeyStyles, s)
v.Init(ctx) v.Init(ctx)
v.Update(makeTableData()) m := &testModel{}
v.SetModel(m)
v.Update(m.Peek())
v.SelectRow(1, true) v.SelectRow(1, true)
assert.True(t, v.RowSelected()) assert.Equal(t, "r1", v.GetSelectedItem())
assert.Equal(t, render.Row{ID: "r2", Fields: render.Fields{"blee", "duh", "zorg"}}, v.GetRow()) 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, "blee", v.GetSelectedCell(0))
assert.Equal(t, 1, v.GetSelectedRowIndex()) assert.Equal(t, 1, v.GetSelectedRowIndex())
assert.Equal(t, []string{"r1"}, v.GetSelectedItems()) assert.Equal(t, []string{"r1"}, v.GetSelectedItems())
@ -50,8 +54,23 @@ func TestTableSelection(t *testing.T) {
assert.Equal(t, 1, v.GetSelectedRowIndex()) assert.Equal(t, 1, v.GetSelectedRowIndex())
} }
// ----------------------------------------------------------------------------
// Helpers... // 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 { func makeTableData() render.TableData {
return render.TableData{ return render.TableData{
Namespace: "", Namespace: "",

View File

@ -3,9 +3,12 @@ package view_test
import ( import (
"context" "context"
"testing" "testing"
"time"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "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/ui"
"github.com/derailed/k9s/internal/view" "github.com/derailed/k9s/internal/view"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@ -18,21 +21,22 @@ func TestAliasNew(t *testing.T) {
assert.Nil(t, v.Init(makeContext())) assert.Nil(t, v.Init(makeContext()))
assert.Equal(t, "Aliases", v.Name()) assert.Equal(t, "Aliases", v.Name())
assert.Equal(t, 9, len(v.Hints())) assert.Equal(t, 10, len(v.Hints()))
} }
// BOZO!! // BOZO!!
// func TestAliasSearch(t *testing.T) { func TestAliasSearch(t *testing.T) {
// v := view.NewAlias(client.GVR("aliases")) v := view.NewAlias(client.GVR("aliases"))
// assert.Nil(t, v.Init(makeContext())) assert.Nil(t, v.Init(makeContext()))
// v.GetTable().SearchBuff().SetActive(true) v.GetTable().SetModel(&testModel{})
// v.GetTable().SearchBuff().Set("dump") 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, 3, v.GetTable().GetColumnCount())
// assert.Equal(t, 1, v.GetTable().GetRowCount()) assert.Equal(t, 1, v.GetTable().GetRowCount())
// } }
func TestAliasGoto(t *testing.T) { func TestAliasGoto(t *testing.T) {
v := view.NewAlias(client.GVR("aliases")) v := view.NewAlias(client.GVR("aliases"))
@ -47,6 +51,7 @@ func TestAliasGoto(t *testing.T) {
assert.True(t, v.GetTable().SearchBuff().IsActive()) assert.True(t, v.GetTable().SearchBuff().IsActive())
} }
// ----------------------------------------------------------------------------
// Helpers... // Helpers...
type buffL struct { type buffL struct {
@ -88,3 +93,42 @@ func (k ks) ClusterNames() ([]string, error) {
func (k ks) NamespaceNames(nn []v1.Namespace) []string { func (k ks) NamespaceNames(nn []v1.Namespace) []string {
return []string{"test"} 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"},
},
},
},
}
}

View File

@ -56,13 +56,9 @@ func (a *App) ActiveView() model.Component {
} }
func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey { func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey {
a.Content.DumpStack()
a.Content.DumpPages()
if !a.Content.IsLast() { if !a.Content.IsLast() {
a.Content.Pop() a.Content.Pop()
} }
a.Content.DumpStack()
a.Content.DumpPages()
return nil return nil
} }

View File

@ -7,14 +7,12 @@ import (
"fmt" "fmt"
rt "runtime" rt "runtime"
"strconv" "strconv"
"time"
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog" "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 // Init watches all running pods in given namespace
func (b *Browser) Init(ctx context.Context) error { func (b *Browser) Init(ctx context.Context) error {
log.Debug().Msgf("BROWSER INIT %s", b.gvr)
var err error var err error
b.meta, err = dao.MetaFor(b.gvr) b.meta, err = dao.MetaFor(b.gvr)
if err != nil { if err != nil {
@ -66,14 +63,16 @@ func (b *Browser) Init(ctx context.Context) error {
return err return err
} }
if !dao.IsK9sMeta(b.meta) { if !dao.IsK9sMeta(b.meta) {
_ = b.app.factory.ForResource(b.app.Config.ActiveNamespace(), b.GVR()) if _, err := b.app.factory.CanForResource(b.app.Config.ActiveNamespace(), b.GVR()); err != nil {
b.app.factory.WaitForCacheSync() return err
}
} }
if b.bindKeysFn != nil { if b.bindKeysFn != nil {
b.bindKeysFn(b.Actions()) 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) b.accessor, err = dao.AccessorFor(b.app.factory, b.gvr)
if err != nil { if err != nil {
return err return err
@ -82,49 +81,47 @@ func (b *Browser) Init(ctx context.Context) error {
b.envFn = b.defaultK9sEnv b.envFn = b.defaultK9sEnv
b.setNamespace(b.App().Config.ActiveNamespace()) b.setNamespace(b.App().Config.ActiveNamespace())
b.refresh()
row, _ := b.GetSelection() row, _ := b.GetSelection()
if row == 0 && b.GetRowCount() > 0 { if row == 0 && b.GetRowCount() > 0 {
b.Select(1, 0) b.Select(1, 0)
} }
b.GetModel().AddListener(b)
return nil return nil
} }
// Start initializes updates. // Start initializes browser updates.
func (b *Browser) Start() { func (b *Browser) Start() {
b.Stop() b.Stop()
log.Debug().Msgf("GOROUTINE %d", rt.NumGoroutine()) log.Debug().Msgf("GOROUTINE %d", rt.NumGoroutine())
log.Debug().Msgf("BROWSER START %s", b.gvr) log.Debug().Msgf("BROWSER START %s", b.gvr)
b.Table.Start()
var ctx context.Context b.Table.Start()
ctx, b.cancelFn = context.WithCancel(context.Background()) ctx := b.defaultContext()
go b.update(ctx) 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
} }
b.GetModel().Start(ctx)
}
// Stop terminates browser updates.
func (b *Browser) Stop() { func (b *Browser) Stop() {
if b.cancelFn != nil { if b.cancelFn == nil {
return
}
b.Table.Stop()
log.Debug().Msgf("BROWSER <STOP> %q", b.gvr)
b.cancelFn() b.cancelFn()
b.cancelFn = nil b.cancelFn = nil
log.Debug().Msgf("BROWSER <STOP> %s", b.BaseTitle)
}
} }
func (b *Browser) update(ctx context.Context) { func (b *Browser) refresh() {
defer log.Debug().Msgf("UPDATER BAIL For %s", b.gvr) b.Start()
for {
select {
case <-ctx.Done():
log.Debug().Msgf("BROWSER <<CANCELED>> -- %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()
}
}
} }
// Name returns the component name. // Name returns the component name.
@ -149,11 +146,12 @@ func (b *Browser) GetTable() *Table { return b.Table }
// Actions()... // Actions()...
func (b *Browser) cpCmd(evt *tcell.EventKey) *tcell.EventKey { func (b *Browser) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
if !b.RowSelected() { path := b.GetSelectedItem()
if path == "" {
return evt return evt
} }
_, n := client.Namespaced(b.GetSelectedItem()) _, n := client.Namespaced(path)
log.Debug().Msgf("Copied selection to clipboard %q", n) log.Debug().Msgf("Copied selection to clipboard %q", n)
b.app.Flash().Info("Current selection copied to clipboard...") b.app.Flash().Info("Current selection copied to clipboard...")
if err := clipboard.WriteAll(n); err != nil { 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 { 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 return nil
} }
@ -172,7 +171,7 @@ func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
if b.enterFn != nil { if b.enterFn != nil {
f = b.enterFn 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 return nil
} }
@ -244,11 +243,11 @@ func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey { func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("DESCRIBE %t -- %#v", b.RowSelected(), b.GetSelectedItems()) path := b.GetSelectedItem()
if !b.RowSelected() { if path == "" {
return evt 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 return nil
} }
@ -272,12 +271,12 @@ func (b *Browser) describeResource(app *App, _, _, sel string) {
} }
func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey { func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
if !b.RowSelected() { path := b.GetSelectedItem()
if path == "" {
return evt return evt
} }
path := b.GetSelectedItem() log.Debug().Msgf("------ NAMESPACES %q vs %q", path, b.GetModel().GetNamespace())
log.Debug().Msgf("------ NAMESPACES %q vs %q", path, b.Data.Namespace)
o, err := b.app.factory.Get(string(b.gvr), path, labels.Everything()) o, err := b.app.factory.Get(string(b.gvr), path, labels.Everything())
if err != nil { if err != nil {
b.app.Flash().Errf("Unable to get resource %q -- %s", b.gvr, err) 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 { func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey {
if !b.RowSelected() { path := b.GetSelectedItem()
if path == "" {
return evt return evt
} }
b.Stop() b.Stop()
defer b.Start() defer b.Start()
{ {
ns, po := client.Namespaced(b.GetSelectedItem()) ns, n := client.Namespaced(path)
args := make([]string, 0, 10) args := make([]string, 0, 10)
args = append(args, "edit") args = append(args, "edit")
args = append(args, b.meta.Kind) 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 != "" { if cfg := b.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
args = append(args, "--kubeconfig", *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")) 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) { func (b *Browser) setNamespace(ns string) {
if !b.meta.Namespaced { if !b.meta.Namespaced {
b.Data.Namespace = render.ClusterScope b.GetModel().SetNamespace(render.ClusterScope)
return return
} }
if b.Data.Namespace == ns { if b.GetModel().InNamespace(ns) {
return return
} }
@ -354,8 +354,7 @@ func (b *Browser) setNamespace(ns string) {
ns = render.AllNamespaces ns = render.AllNamespaces
} }
log.Debug().Msgf("!!!!!! SETTING NS %q", ns) log.Debug().Msgf("!!!!!! SETTING NS %q", ns)
b.Data.Namespace = ns b.GetModel().SetNamespace(ns)
b.Data.RowEvents = b.Data.RowEvents.Clear()
} }
func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { 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.UpdateTitle()
b.SelectRow(1, true) b.SelectRow(1, true)
b.app.CmdBuff().Reset() 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!") log.Error().Err(err).Msg("Config save NS failed!")
} }
if err := b.app.Config.Save(); err != nil { if err := b.app.Config.Save(); err != nil {
@ -382,33 +381,30 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
func (b *Browser) refresh() { // TableLoadChanged notifies view something went south.
if b.app.Conn() == nil { func (b *Browser) TableLoadFailed(err error) {
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)
b.app.QueueUpdateDraw(func() { b.app.QueueUpdateDraw(func() {
if err != nil {
b.app.Flash().Err(err) b.app.Flash().Err(err)
})
} }
b.refreshActions()
// TableDataChanged notifies view new data is available.
func (b *Browser) TableDataChanged(data render.TableData) {
b.Update(data) b.Update(data)
b.app.QueueUpdateDraw(func() {
b.refreshActions()
}) })
} }
func (b *Browser) defaultContext() context.Context { 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.KeyGVR, string(b.gvr))
ctx = context.WithValue(ctx, internal.KeyPath, b.Path) ctx = context.WithValue(ctx, internal.KeyPath, b.Path)
ctx = context.WithValue(ctx, internal.KeyLabels, "") ctx = context.WithValue(ctx, internal.KeyLabels, "")
ctx = context.WithValue(ctx, internal.KeyFields, "") ctx = context.WithValue(ctx, internal.KeyFields, "")
ctx = context.WithValue(ctx, internal.KeyNamespace, b.App().Config.ActiveNamespace())
return ctx 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 { func (b *Browser) execCmd(bin string, bg bool, args ...string) ui.ActionHandler {
return func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey {
if !b.RowSelected() { path := b.GetSelectedItem()
if path == "" {
return evt return evt
} }
@ -519,5 +515,5 @@ func (b *Browser) execCmd(bin string, bg bool, args ...string) ui.ActionHandler
} }
func (b *Browser) defaultK9sEnv() K9sEnv { func (b *Browser) defaultK9sEnv() K9sEnv {
return defaultK9sEnv(b.app, b.GetSelectedItem(), b.GetRow()) return defaultK9sEnv(b.app, b.GetSelectedItem(), b.GetSelectedRow())
} }

View File

@ -52,7 +52,7 @@ func (c *command) defaultCmd() error {
var canRX = regexp.MustCompile(`\Acan\s([u|g|s]):([\w-:]+)\b`) 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, " ") cmds := strings.Split(cmd, " ")
switch cmds[0] { switch cmds[0] {
case "q", "Q", "quit": case "q", "Q", "quit":
@ -85,6 +85,10 @@ func (c *command) viewMetaFor(cmd string) (string, *MetaViewer, error) {
if !ok { if !ok {
return "", nil, fmt.Errorf("Huh? `%s` command not found", cmd) 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)] v, ok := customViewers[client.GVR(gvr)]
if !ok { if !ok {
return gvr, &MetaViewer{viewerFn: NewBrowser}, nil 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. // Exec the command by showing associated display.
func (c *command) run(cmd string) error { func (c *command) run(cmd string) error {
if c.isK9sCmd(cmd) { if c.specialCmd(cmd) {
return nil return nil
} }

View File

@ -50,7 +50,7 @@ func (c *Container) bindKeys(aa ui.KeyActions) {
} }
func (c *Container) k9sEnv() K9sEnv { 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) ns, n := client.Namespaced(c.GetTable().Path)
env["POD"] = n env["POD"] = n
env["NAMESPACE"] = ns env["NAMESPACE"] = ns
@ -59,9 +59,7 @@ func (c *Container) k9sEnv() K9sEnv {
} }
func (c *Container) selectedContainer() string { func (c *Container) selectedContainer() string {
log.Debug().Msgf("Container SELECTED %s", c.GetTable().GetSelectedItem())
tokens := strings.Split(c.GetTable().GetSelectedItem(), "/") tokens := strings.Split(c.GetTable().GetSelectedItem(), "/")
return tokens[0] return tokens[0]
} }
@ -152,7 +150,7 @@ func (c *Container) portForward(lport, cport string) {
func (c *Container) runForward(pf *dao.PortForwarder, f *portforward.PortForwarder) { func (c *Container) runForward(pf *dao.PortForwarder, f *portforward.PortForwarder) {
c.App().QueueUpdateDraw(func() { 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]) c.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0])
dialog.DismissPortForward(c.App().Content.Pages) dialog.DismissPortForward(c.App().Content.Pages)
}) })

View File

@ -13,5 +13,5 @@ func TestContainerNew(t *testing.T) {
assert.Nil(t, c.Init(makeCtx())) assert.Nil(t, c.Init(makeCtx()))
assert.Equal(t, "Containers", c.Name()) assert.Equal(t, "Containers", c.Name())
assert.Equal(t, 17, len(c.Hints())) assert.Equal(t, 18, len(c.Hints()))
} }

View File

@ -13,5 +13,5 @@ func TestContext(t *testing.T) {
assert.Nil(t, ctx.Init(makeCtx())) assert.Nil(t, ctx.Init(makeCtx()))
assert.Equal(t, "Contexts", ctx.Name()) assert.Equal(t, "Contexts", ctx.Name())
assert.Equal(t, 8, len(ctx.Hints())) assert.Equal(t, 9, len(ctx.Hints()))
} }

View File

@ -32,9 +32,9 @@ func NewCronJob(gvr client.GVR) ResourceViewer {
return &c return &c
} }
func (c *CronJob) showJobs(app *App, ns, res, path string) { func (c *CronJob) showJobs(app *App, ns, gvr, path string) {
log.Debug().Msgf("Showing Jobs %q:%q -- %q", ns, res, path) log.Debug().Msgf("Showing Jobs %q:%q -- %q", ns, gvr, path)
o, err := app.factory.Get("batch/v1beta1/cronjobs", path, labels.Everything()) o, err := app.factory.Get(gvr, path, labels.Everything())
if err != nil { if err != nil {
app.Flash().Err(err) app.Flash().Err(err)
return return

View File

@ -85,8 +85,8 @@ func (d *Details) Hints() model.MenuHints {
func (d *Details) bindKeys() { func (d *Details) bindKeys() {
d.actions.Set(ui.KeyActions{ d.actions.Set(ui.KeyActions{
tcell.KeyEscape: ui.NewKeyAction("Back", d.app.PrevCmd, true), tcell.KeyEscape: ui.NewKeyAction("Back", d.app.PrevCmd, false),
tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false),
ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true), ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true),
}) })
} }

View File

@ -13,6 +13,6 @@ func TestDeploy(t *testing.T) {
assert.Nil(t, v.Init(makeCtx())) assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "Deployments", v.Name()) assert.Equal(t, "Deployments", v.Name())
assert.Equal(t, 16, len(v.Hints())) assert.Equal(t, 17, len(v.Hints()))
} }

View File

@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) {
assert.Nil(t, v.Init(makeCtx())) assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "DaemonSets", v.Name()) assert.Equal(t, "DaemonSets", v.Name())
assert.Equal(t, 15, len(v.Hints())) assert.Equal(t, 16, len(v.Hints()))
} }

View File

@ -17,31 +17,33 @@ type Group struct {
// NewGroup returns a new subject viewer. // NewGroup returns a new subject viewer.
func NewGroup(gvr client.GVR) ResourceViewer { func NewGroup(gvr client.GVR) ResourceViewer {
s := Group{ResourceViewer: NewBrowser(gvr)} g := Group{ResourceViewer: NewBrowser(gvr)}
s.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) g.GetTable().SetColorerFn(render.Subject{}.ColorerFunc())
s.SetBindKeysFn(s.bindKeys) g.SetBindKeysFn(g.bindKeys)
s.SetContextFn(s.subjectCtx) g.SetContextFn(g.subjectCtx)
return &s
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.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Rules", s.policyCmd, true), tcell.KeyEnter: ui.NewKeyAction("Rules", g.policyCmd, true),
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.GetTable().SortColCmd(1, true), false), 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") return context.WithValue(ctx, internal.KeySubjectKind, "Group")
} }
func (s *Group) policyCmd(evt *tcell.EventKey) *tcell.EventKey { func (g *Group) policyCmd(evt *tcell.EventKey) *tcell.EventKey {
if !s.GetTable().RowSelected() { path := g.GetTable().GetSelectedItem()
if path == "" {
return evt return evt
} }
if err := s.App().inject(NewPolicy(s.App(), "Group", s.GetTable().GetSelectedItem())); err != nil { if err := g.App().inject(NewPolicy(g.App(), "Group", path)); err != nil {
s.App().Flash().Err(err) g.App().Flash().Err(err)
} }
return nil return nil

View File

@ -116,7 +116,7 @@ func (v *Help) showGeneral() model.MenuHints {
}, },
{ {
Mnemonic: "esc", Mnemonic: "esc",
Description: "Clear filter", Description: "Back/Clear",
}, },
{ {
Mnemonic: "tab", Mnemonic: "tab",
@ -138,6 +138,18 @@ func (v *Help) showGeneral() model.MenuHints {
Mnemonic: ":q", Mnemonic: ":q",
Description: "Quit", Description: "Quit",
}, },
{
Mnemonic: "space",
Description: "Mark",
},
{
Mnemonic: "Ctrl-space",
Description: "Clear Marks",
},
{
Mnemonic: "Ctrl-s",
Description: "Save",
},
} }
} }

View File

@ -20,7 +20,7 @@ func TestHelp(t *testing.T) {
v := view.NewHelp() v := view.NewHelp()
assert.Nil(t, v.Init(ctx)) 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, 10, v.GetColumnCount())
assert.Equal(t, "<backspace>", v.GetCell(1, 0).Text) assert.Equal(t, "<backspace>", v.GetCell(1, 0).Text)
assert.Equal(t, "Erase", v.GetCell(1, 1).Text) assert.Equal(t, "Erase", v.GetCell(1, 1).Text)

View File

@ -23,9 +23,8 @@ func NewJob(gvr client.GVR) ResourceViewer {
return &j return &j
} }
// TODO!! Change enter signature? func (*Job) showPods(app *App, _, gvr, path string) {
func (*Job) showPods(app *App, _, res, path string) { o, err := app.factory.Get(gvr, path, labels.Everything())
o, err := app.factory.Get("batch/v1/jobs", path, labels.Everything())
if err != nil { if err != nil {
app.Flash().Err(err) app.Flash().Err(err)
return return

View File

@ -40,7 +40,8 @@ func (n *Node) showPods(app *App, ns, res, sel string) {
} }
func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey { func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
if !n.GetTable().RowSelected() { path := n.GetTable().GetSelectedItem()
if path == "" {
return evt return evt
} }

View File

@ -52,8 +52,6 @@ func (n *Namespace) useNsCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
n.useNamespace(path) n.useNamespace(path)
log.Debug().Msgf("NS TABLE %#v", n.GetTable().Data)
return nil return nil
} }
@ -71,13 +69,10 @@ func (n *Namespace) useNamespace(ns string) {
} }
func (n *Namespace) decorate(data render.TableData) render.TableData { func (n *Namespace) decorate(data render.TableData) render.TableData {
if n.App().Conn() == nil { if n.App().Conn() == nil || len(data.RowEvents) == 0 {
return render.TableData{} 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. // checks if all ns is in the list if not add it.
if _, ok := data.RowEvents.FindIndex(render.NamespaceAll); !ok { if _, ok := data.RowEvents.FindIndex(render.NamespaceAll); !ok {
data.RowEvents = append(data.RowEvents, data.RowEvents = append(data.RowEvents,

View File

@ -13,5 +13,5 @@ func TestNSCleanser(t *testing.T) {
assert.Nil(t, ns.Init(makeCtx())) assert.Nil(t, ns.Init(makeCtx()))
assert.Equal(t, "Namespaces", ns.Name()) assert.Equal(t, "Namespaces", ns.Name())
assert.Equal(t, 12, len(ns.Hints())) assert.Equal(t, 13, len(ns.Hints()))
} }

View File

@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) {
assert.Nil(t, po.Init(makeCtx())) assert.Nil(t, po.Init(makeCtx()))
assert.Equal(t, "Pods", po.Name()) assert.Equal(t, "Pods", po.Name())
assert.Equal(t, 24, len(po.Hints())) assert.Equal(t, 25, len(po.Hints()))
} }
// Helpers... // Helpers...

View File

@ -51,8 +51,6 @@ func (p *PortForward) bindKeys(aa ui.KeyActions) {
tcell.KeyCtrlB: ui.NewKeyAction("Bench", p.benchCmd, true), tcell.KeyCtrlB: ui.NewKeyAction("Bench", p.benchCmd, true),
tcell.KeyCtrlK: ui.NewKeyAction("Bench Stop", p.benchStopCmd, true), tcell.KeyCtrlK: ui.NewKeyAction("Bench Stop", p.benchStopCmd, true),
tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, 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.KeyShiftP: ui.NewKeyAction("Sort Ports", p.GetTable().SortColCmd(2, true), false),
ui.KeyShiftU: ui.NewKeyAction("Sort URL", p.GetTable().SortColCmd(4, 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 { func (p *PortForward) benchCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := p.getSelectedItem() sel := p.GetTable().GetSelectedItem()
if sel == "" { if sel == "" {
return nil return nil
} }
@ -89,8 +87,8 @@ func (p *PortForward) benchCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
r, _ := p.GetTable().GetSelection() r, _ := p.GetTable().GetSelection()
cfg, co := defaultConfig(), ui.TrimCell(p.GetTable().SelectTable, r, 2) cfg := defaultConfig()
if b, ok := p.App().Bench.Benchmarks.Containers[containerID(sel, co)]; ok { if b, ok := p.App().Bench.Benchmarks.Containers[sel]; ok {
cfg = b cfg = b
} }
cfg.Name = sel 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 { func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
if !p.GetTable().SearchBuff().Empty() { if !p.GetTable().SearchBuff().Empty() {
p.GetTable().SearchBuff().Reset() p.GetTable().SearchBuff().Reset()
return nil return nil
} }
sel := p.getSelectedItem() sel := p.GetTable().GetSelectedItem()
if sel == "" { if sel == "" {
return nil return nil
} }
log.Debug().Msgf("PF DELETE %q", sel)
showModal(p.App().Content.Pages, fmt.Sprintf("Delete PortForward `%s?", sel), func() { showModal(p.App().Content.Pages, fmt.Sprintf("Delete PortForward `%s?", sel), func() {
p.App().factory.DeleteForwarder(sel) p.App().factory.DeleteForwarder(sel)

View File

@ -13,5 +13,5 @@ func TestRbacNew(t *testing.T) {
assert.Nil(t, v.Init(makeCtx())) assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "Rbac", v.Name()) assert.Equal(t, "Rbac", v.Name())
assert.Equal(t, 9, len(v.Hints())) assert.Equal(t, 10, len(v.Hints()))
} }

View File

@ -1,5 +1,12 @@
package view package view
import (
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
)
func loadCustomViewers() MetaViewers { func loadCustomViewers() MetaViewers {
m := make(MetaViewers, 30) m := make(MetaViewers, 30)
coreRes(m) coreRes(m)
@ -7,6 +14,7 @@ func loadCustomViewers() MetaViewers {
appsRes(m) appsRes(m)
rbacRes(m) rbacRes(m)
batchRes(m) batchRes(m)
extRes(m)
return m return m
} }
@ -100,3 +108,22 @@ func batchRes(vv MetaViewers) {
viewerFn: NewJob, 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)
}
}

View File

@ -13,5 +13,5 @@ func TestScreenDumpNew(t *testing.T) {
assert.Nil(t, po.Init(makeCtx())) assert.Nil(t, po.Init(makeCtx()))
assert.Equal(t, "ScreenDumps", po.Name()) assert.Equal(t, "ScreenDumps", po.Name())
assert.Equal(t, 11, len(po.Hints())) assert.Equal(t, 12, len(po.Hints()))
} }

View File

@ -13,5 +13,5 @@ func TestSecretNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx())) assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "Secrets", s.Name()) assert.Equal(t, "Secrets", s.Name())
assert.Equal(t, 12, len(s.Hints())) assert.Equal(t, 13, len(s.Hints()))
} }

View File

@ -13,5 +13,5 @@ func TestStatefulSetNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx())) assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "StatefulSets", s.Name()) assert.Equal(t, "StatefulSets", s.Name())
assert.Equal(t, 16, len(s.Hints())) assert.Equal(t, 17, len(s.Hints()))
} }

View File

@ -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
}

View File

@ -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()))
}

View File

@ -132,5 +132,5 @@ func TestServiceNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx())) assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "Services", s.Name()) assert.Equal(t, "Services", s.Name())
assert.Equal(t, 16, len(s.Hints())) assert.Equal(t, 17, len(s.Hints()))
} }

View File

@ -2,6 +2,7 @@ package view
import ( import (
"context" "context"
"time"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@ -16,15 +17,14 @@ type Table struct {
enterFn EnterFunc enterFn EnterFunc
} }
func NewTable(title string) *Table { func NewTable(gvr string) *Table {
return &Table{ return &Table{
Table: ui.NewTable(title), Table: ui.NewTable(gvr),
} }
} }
// Init initializes the component // Init initializes the component
func (t *Table) Init(ctx context.Context) (err error) { 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 { if t.app, err = extractApp(ctx); err != nil {
return err return err
} }
@ -32,6 +32,8 @@ func (t *Table) Init(ctx context.Context) (err error) {
t.Table.Init(ctx) t.Table.Init(ctx)
t.bindKeys() t.bindKeys()
t.GetModel().SetRefreshRate(time.Duration(t.app.Config.K9s.GetRefreshRate()) * time.Second)
return nil return nil
} }
@ -45,14 +47,13 @@ func (t *Table) App() *App {
// Start runs the component. // Start runs the component.
func (t *Table) Start() { func (t *Table) Start() {
log.Debug().Msgf("Table START %s", t.BaseTitle) t.Stop()
t.SearchBuff().AddListener(t.app.Cmd()) t.SearchBuff().AddListener(t.app.Cmd())
t.SearchBuff().AddListener(t) t.SearchBuff().AddListener(t)
} }
// Stop terminates the component. // Stop terminates the component.
func (t *Table) Stop() { func (t *Table) Stop() {
log.Debug().Msgf("TABLE <STOP> %s", t.BaseTitle)
t.SearchBuff().RemoveListener(t.app.Cmd()) t.SearchBuff().RemoveListener(t.app.Cmd())
t.SearchBuff().RemoveListener(t) t.SearchBuff().RemoveListener(t)
} }
@ -85,9 +86,9 @@ func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
func (t *Table) bindKeys() { func (t *Table) bindKeys() {
t.Actions().Add(ui.KeyActions{ t.Actions().Add(ui.KeyActions{
ui.KeySpace: ui.NewKeyAction("Mark", t.markCmd, true), ui.KeySpace: ui.NewKeyAction("Mark", t.markCmd, false),
tcell.KeyCtrlSpace: ui.NewKeyAction("Marks Clear", t.clearMarksCmd, true), tcell.KeyCtrlSpace: ui.NewKeyAction("Marks Clear", t.clearMarksCmd, false),
tcell.KeyCtrlS: ui.NewKeyAction("Save", t.saveCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", t.saveCmd, false),
ui.KeySlash: ui.NewKeyAction("Filter Mode", t.activateCmd, false), ui.KeySlash: ui.NewKeyAction("Filter Mode", t.activateCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Filter Reset", t.resetCmd, false), tcell.KeyEscape: ui.NewKeyAction("Filter Reset", t.resetCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Filter", t.filterCmd, 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 { func (t *Table) markCmd(evt *tcell.EventKey) *tcell.EventKey {
if !t.RowSelected() { path := t.GetSelectedItem()
if path == "" {
return evt return evt
} }
t.ToggleMark() t.ToggleMark()
@ -110,7 +112,8 @@ func (t *Table) markCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
func (t *Table) clearMarksCmd(evt *tcell.EventKey) *tcell.EventKey { func (t *Table) clearMarksCmd(evt *tcell.EventKey) *tcell.EventKey {
if !t.RowSelected() { path := t.GetSelectedItem()
if path == "" {
return evt return evt
} }
t.ClearMarks() t.ClearMarks()
@ -147,6 +150,7 @@ func (t *Table) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
t.SearchBuff().Reset() t.SearchBuff().Reset()
return t.app.PrevCmd(evt) return t.app.PrevCmd(evt)
} }
if ui.IsLabelSelector(t.SearchBuff().String()) { if ui.IsLabelSelector(t.SearchBuff().String()) {
t.filterFn("") t.filterFn("")
} }

View File

@ -5,8 +5,10 @@ import (
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview" "github.com/derailed/tview"
@ -59,29 +61,7 @@ func TestTableNew(t *testing.T) {
func TestTableViewFilter(t *testing.T) { func TestTableViewFilter(t *testing.T) {
v := NewTable("test") v := NewTable("test")
v.Init(makeContext()) v.Init(makeContext())
v.SetModel(&testTableModel{})
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.SearchBuff().SetActive(true) v.SearchBuff().SetActive(true)
v.SearchBuff().Set("blee") v.SearchBuff().Set("blee")
v.filterCmd(nil) v.filterCmd(nil)
@ -93,8 +73,35 @@ func TestTableViewFilter(t *testing.T) {
func TestTableViewSort(t *testing.T) { func TestTableViewSort(t *testing.T) {
v := NewTable("test") v := NewTable("test")
v.Init(makeContext()) 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{ Header: render.HeaderRow{
render.Header{Name: "NAMESPACE"}, render.Header{Name: "NAMESPACE"},
render.Header{Name: "NAME", Align: tview.AlignRight}, render.Header{Name: "NAME", Align: tview.AlignRight},
@ -116,18 +123,8 @@ func TestTableViewSort(t *testing.T) {
}, },
Namespace: "", 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 { func makeContext() context.Context {
a := NewApp(config.NewConfig(ks{})) a := NewApp(config.NewConfig(ks{}))
ctx := context.WithValue(context.Background(), ui.KeyApp, a) ctx := context.WithValue(context.Background(), ui.KeyApp, a)

View File

@ -17,31 +17,33 @@ type User struct {
// NewUser returns a new subject viewer. // NewUser returns a new subject viewer.
func NewUser(gvr client.GVR) ResourceViewer { func NewUser(gvr client.GVR) ResourceViewer {
s := User{ResourceViewer: NewBrowser(gvr)} u := User{ResourceViewer: NewBrowser(gvr)}
s.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) u.GetTable().SetColorerFn(render.Subject{}.ColorerFunc())
s.SetBindKeysFn(s.bindKeys) u.SetBindKeysFn(u.bindKeys)
s.SetContextFn(s.subjectCtx) u.SetContextFn(u.subjectCtx)
return &s
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.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Rules", s.policyCmd, true), tcell.KeyEnter: ui.NewKeyAction("Rules", u.policyCmd, true),
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.GetTable().SortColCmd(1, true), false), 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") return context.WithValue(ctx, internal.KeySubjectKind, "User")
} }
func (s *User) policyCmd(evt *tcell.EventKey) *tcell.EventKey { func (u *User) policyCmd(evt *tcell.EventKey) *tcell.EventKey {
if !s.GetTable().RowSelected() { path := u.GetTable().GetSelectedItem()
if path == "" {
return evt return evt
} }
if err := s.App().inject(NewPolicy(s.App(), "User", s.GetTable().GetSelectedItem())); err != nil { if err := u.App().inject(NewPolicy(u.App(), "User", path)); err != nil {
s.App().Flash().Err(err) u.App().Flash().Err(err)
} }
return nil return nil

View File

@ -15,7 +15,6 @@ import (
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
) )
// Factory - *factories(ns) -> *informers
const ( const (
defaultResync = 10 * time.Minute defaultResync = 10 * time.Minute
allNamespaces = "" allNamespaces = ""
@ -41,18 +40,15 @@ func NewFactory(client client.Connection) *Factory {
} }
} }
func (f *Factory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error) { func (f *Factory) String() string {
auth, err := f.Client().CanI(ns, gvr, []string{"list"}) return fmt.Sprintf("Factory ActiveNS %s", f.activeNS)
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) // List returns a resource collection.
if inf == nil { func (f *Factory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, error) {
return nil, fmt.Errorf("No resource for GVR %s", gvr) inf, err := f.CanForResource(ns, gvr, "list")
if err != nil {
return nil, err
} }
if ns == clusterScope { if ns == clusterScope {
return inf.Lister().List(sel) 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) 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) { func (f *Factory) Get(gvr, path string, sel labels.Selector) (runtime.Object, error) {
ns, n := namespaced(path) ns, n := namespaced(path)
auth, err := f.Client().CanI(ns, gvr, []string{"get"}) inf, err := f.CanForResource(ns, gvr, "get")
if err != nil { if err != nil {
return nil, err 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 { if ns == clusterScope {
return inf.Lister().Get(n) return inf.Lister().Get(n)
} }
log.Debug().Msgf("GET %q--%q:%q", gvr, ns, path)
return inf.Lister().ByNamespace(ns).Get(n) return inf.Lister().ByNamespace(ns).Get(n)
} }
// WaitForCachesync waits for all factories to update their cache.
func (f *Factory) WaitForCacheSync() { func (f *Factory) WaitForCacheSync() {
for _, fac := range f.factories { for _, fac := range f.factories {
fac.WaitForCacheSync(f.stopChan) fac.WaitForCacheSync(f.stopChan)
} }
} }
// Init starts a factory.
func (f *Factory) Init() { func (f *Factory) Init() {
f.Start(f.stopChan) f.Start(f.stopChan)
} }
// Terminate terminates all watchers and forwards.
func (f *Factory) Terminate() { func (f *Factory) Terminate() {
if f.stopChan != nil { if f.stopChan != nil {
close(f.stopChan) close(f.stopChan)
@ -104,17 +95,23 @@ func (f *Factory) Terminate() {
f.forwarders.DeleteAll() f.forwarders.DeleteAll()
} }
// DeleteForwarder deletes portforward for a given container. // RegisterForwarder registers a new portforward for a given container.
func (f *Factory) DeleteForwarder(path string) { func (f *Factory) AddForwarder(pf Forwarder) {
if fwd, ok := f.forwarders[path]; ok { f.forwarders[pf.Path()] = pf
fwd.Stop() f.forwarders.Dump()
delete(f.forwarders, path)
}
} }
// RegisterForwarder registers a new portforward for a given container. // DeleteForwarder deletes portforward for a given container.
func (f *Factory) RegisterForwarder(pf Forwarder) { func (f *Factory) DeleteForwarder(path string) {
f.forwarders[pf.Path()] = pf 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. // Forwards returns all portforwards.
@ -150,20 +147,32 @@ func (f *Factory) isClusterWide() bool {
} }
func (f *Factory) preload(ns string) { func (f *Factory) preload(ns string) {
f.ForResource(ns, "v1/pods") verbs := []string{"get", "list", "watch"}
f.ForResource(allNamespaces, "apiextensions.k8s.io/v1beta1/customresourcedefinitions") _, _ = f.CanForResource(ns, "v1/pods", verbs...)
f.ForResource(clusterScope, "rbac.authorization.k8s.io/v1/clusterroles") _, _ = f.CanForResource(allNamespaces, "apiextensions.k8s.io/v1beta1/customresourcedefinitions", verbs...)
f.ForResource(allNamespaces, "rbac.authorization.k8s.io/v1/roles") _, _ = 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 { func (f *Factory) FactoryFor(ns string) di.DynamicSharedInformerFactory {
return f.factories[ns] return f.factories[ns]
} }
func (f *Factory) Preload(ns, gvr string) { // ForResource returns an informer for a given resource.
_ = f.ForResource(ns, gvr)
}
func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer { func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer {
fact := f.ensureFactory(ns) fact := f.ensureFactory(ns)
inf := fact.ForResource(toGVR(gvr)) inf := fact.ForResource(toGVR(gvr))
@ -192,7 +201,6 @@ func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory {
} }
func toGVR(gvr string) schema.GroupVersionResource { func toGVR(gvr string) schema.GroupVersionResource {
log.Debug().Msgf("GVR -- %q", gvr)
tokens := strings.Split(gvr, "/") tokens := strings.Split(gvr, "/")
if len(tokens) < 3 { if len(tokens) < 3 {
tokens = append([]string{""}, tokens...) tokens = append([]string{""}, tokens...)

View File

@ -50,18 +50,18 @@ func (ff Forwarders) DeleteAll() {
} }
// Kill stops and delete a port-forwards associated with pod. // 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() ff.Dump()
log.Debug().Msgf("Delete port-forward %q", pod) log.Debug().Msgf("Delete port-forward %q", path)
hasContainer := strings.Contains(pod, ":") hasContainer := strings.Contains(path, ":")
var stats int var stats int
for k, f := range ff { for k, f := range ff {
victim := k victim := k
if !hasContainer { if !hasContainer {
victim = strings.Split(k, ":")[0] victim = strings.Split(k, ":")[0]
} }
if victim == pod { if victim == path {
stats++ stats++
log.Debug().Msgf("Stopping and delete port-forward %s", k) log.Debug().Msgf("Stopping and delete port-forward %s", k)
f.Stop() f.Stop()