checkpoint
parent
99fa0e9952
commit
c26c80e170
1
go.mod
1
go.mod
|
|
@ -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
1
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=
|
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=
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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"])
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 == "" {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: "",
|
||||||
|
|
|
||||||
|
|
@ -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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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...
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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()))
|
|
||||||
}
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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("")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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...)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue