added support for custom columns

mine
derailed 2020-02-22 18:30:30 -07:00
parent c4305e38ee
commit 42e2e98e4d
127 changed files with 2994 additions and 1396 deletions

2
go.sum
View File

@ -841,8 +841,10 @@ gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
helm.sh/helm v2.16.3+incompatible h1:a7P7FSGTBdK6ZsAcWWZZQXPIdzkgybD8CWd/Dy+jwf4=
helm.sh/helm/v3 v3.0.2 h1:BggvLisIMrAc+Is5oAHVrlVxgwOOrMN8nddfQbm5gKo=
helm.sh/helm/v3 v3.0.2/go.mod h1:KBxE6XWO57XSNA1PA9CvVLYRY0zWqYQTad84bNXp1lw=
helm.sh/helm/v3 v3.1.1 h1:aykwPMVyQyncZ8iLNVMXgJ1l3c6W0+LSOPmqp8JdCjs=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -25,6 +25,7 @@ const (
cacheSize = 100
cacheExpiry = 5 * time.Minute
cacheMXKey = "metrics"
cacheMXAPIKey = "metricsAPI"
checkConnTimeout = 10 * time.Second
)
@ -41,6 +42,15 @@ type APIClient struct {
config *Config
mx sync.Mutex
cache *cache.LRUExpireCache
metricsAPI bool
}
// NewTestAPIClient for testing ONLY!!
func NewTestClient() *APIClient {
return &APIClient{
config: NewConfig(nil),
cache: cache.NewLRUExpireCache(cacheSize),
}
}
// InitConnectionOrDie initialize connection from command line args.
@ -50,8 +60,7 @@ func InitConnectionOrDie(config *Config) *APIClient {
config: config,
cache: cache.NewLRUExpireCache(cacheSize),
}
a.HasMetrics()
a.metricsAPI = a.supportsMetricsResources()
return &a
}
@ -174,6 +183,9 @@ func (a *APIClient) Config() *Config {
// HasMetrics returns true if the cluster supports metrics.
func (a *APIClient) HasMetrics() bool {
if !a.supportsMetricsResources() {
return false
}
v, ok := a.cache.Get(cacheMXKey)
if ok {
flag, k := v.(bool)
@ -186,7 +198,6 @@ func (a *APIClient) HasMetrics() bool {
a.cache.Add(cacheMXKey, flag, cacheExpiry)
return flag
}
if _, err := dial.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{Limit: 1}); err == nil {
flag = true
}
@ -282,7 +293,8 @@ func (a *APIClient) SwitchContext(ctx string) error {
}
a.clearCache()
a.reset()
_ = a.supportsMxServer()
a.metricsAPI = a.supportsMetricsResources()
ResetMetrics()
return nil
}
@ -295,11 +307,20 @@ func (a *APIClient) reset() {
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
}
func (a *APIClient) supportsMxServer() (supported bool) {
func (a *APIClient) supportsMetricsResources() (supported bool) {
defer func() {
a.cache.Add(cacheMXKey, supported, cacheExpiry)
a.cache.Add(cacheMXAPIKey, supported, cacheExpiry)
}()
if v, ok := a.cache.Get(cacheMXAPIKey); ok {
flag, k := v.(bool)
supported = k && flag
return
}
if a.config == nil || a.config.flags == nil {
return
}
apiGroups, err := a.CachedDiscoveryOrDie().ServerGroups()
if err != nil {
return

View File

@ -25,21 +25,25 @@ const (
// NotNamespaced designates a non resource namespace.
NotNamespaced = "*"
)
const (
// CreateVerb represents create access on a resource.
CreateVerb = "create"
// UpdateVerb represents an update access on a resource.
UpdateVerb = "update"
// PatchVerb represents a patch access on a resource.
PatchVerb = "patch"
// DeleteVerb represents a delete access on a resource.
DeleteVerb = "delete"
// GetVerb represents a get access on a resource.
GetVerb = "get"
// ListVerb represents a list access on a resource.
ListVerb = "list"
// WatchVerb represents a watch access on a resource.
WatchVerb = "watch"
)
@ -65,16 +69,37 @@ type Authorizer interface {
type Connection interface {
Authorizer
// Config returns current config.
Config() *Config
// DialOrDie connects to api server.
DialOrDie() kubernetes.Interface
// SwitchContext switches cluster based on context.
SwitchContext(ctx string) error
// CachedDiscoveryOrDie connects to discovery client.
CachedDiscoveryOrDie() *disk.CachedDiscoveryClient
// RestConfigOrDie connects to rest client.
RestConfigOrDie() *restclient.Config
// MXDial connects to metrics server.
MXDial() (*versioned.Clientset, error)
// DynDialOrDie connects to dynamic client.
DynDialOrDie() dynamic.Interface
// HasMetrics checks if metrics server is available.
HasMetrics() bool
// ValidNamespaces returns all available namespaces.
ValidNamespaces() ([]v1.Namespace, error)
// ServerVersion returns current server version.
ServerVersion() (*version.Info, error)
// CheckConnectivity checks if api server connection is happy or not.
CheckConnectivity() bool
}

View File

@ -195,6 +195,7 @@ func (c *Config) Load(path string) error {
func (c *Config) Save() error {
log.Debug().Msg("[Config] Saving configuration...")
c.Validate()
return c.SaveFile(K9sConfigFile)
}

View File

@ -9,10 +9,8 @@ import (
"gopkg.in/yaml.v2"
)
var (
// K9sStylesFile represents K9s skins file location.
K9sStylesFile = filepath.Join(K9sHome, "skin.yml")
)
// K9sStylesFile represents K9s skins file location.
var K9sStylesFile = filepath.Join(K9sHome, "skin.yml")
// StyleListener represents a skin's listener.
type StyleListener interface {

View File

@ -0,0 +1,8 @@
k9s:
views:
v1/pods:
columns:
- NAMESPACE
- NAME
- AGE
- IP

88
internal/config/views.go Normal file
View File

@ -0,0 +1,88 @@
package config
import (
"io/ioutil"
"path/filepath"
"gopkg.in/yaml.v2"
)
var K9sViewConfigFile = filepath.Join(K9sHome, "views_config.yml")
// ViewConfigListener represents a view config listener.
type ViewConfigListener interface {
// ConfigChanged notifies listener the view configuration changed.
ViewSettingsChanged(ViewSetting)
}
type ViewSetting struct {
Columns []string `yaml:"columns"`
}
type ViewSettings struct {
Fuck string `yaml:"fuck"`
Views map[string]ViewSetting `yaml:"views"`
}
func NewViewSettings() ViewSettings {
return ViewSettings{
Views: make(map[string]ViewSetting),
}
}
// CustomView represents a collection of view customization.
type CustomView struct {
K9s ViewSettings `yaml:"k9s"`
listeners map[string]ViewConfigListener
}
func NewCustomView() *CustomView {
return &CustomView{
K9s: NewViewSettings(),
listeners: make(map[string]ViewConfigListener),
}
}
// Reset clears out configurations.
func (v *CustomView) Reset() {
for k := range v.K9s.Views {
delete(v.K9s.Views, k)
}
}
// Load loads view configurations.
func (v *CustomView) Load(path string) error {
raw, err := ioutil.ReadFile(path)
if err != nil {
return err
}
var in CustomView
if err := yaml.Unmarshal(raw, &in); err != nil {
return err
}
v.K9s = in.K9s
v.fireConfigChanged()
return nil
}
// AddListener registers a new listener.
func (c *CustomView) AddListener(gvr string, l ViewConfigListener) {
c.listeners[gvr] = l
c.fireConfigChanged()
}
// RemoveListener unregister a listener.
func (c *CustomView) RemoveListener(gvr string) {
delete(c.listeners, gvr)
}
func (c *CustomView) fireConfigChanged() {
for gvr, list := range c.listeners {
if v, ok := c.K9s.Views[gvr]; ok {
list.ViewSettingsChanged(v)
}
}
}

View File

@ -0,0 +1,16 @@
package config_test
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert"
)
func TestViewSettingsLoad(t *testing.T) {
cfg := config.NewCustomView()
assert.Nil(t, cfg.Load("testdata/view_settings.yml"))
assert.Equal(t, 1, len(cfg.K9s.Views))
assert.Equal(t, 4, len(cfg.K9s.Views["v1/pods"].Columns))
}

View File

@ -63,7 +63,7 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
var pmx *mv1beta1.PodMetrics
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if pmx, err = client.DialMetrics(p.Client()).FetchPodMetrics(path); err != nil {
log.Warn().Err(err).Msgf("No pod metrics")
log.Debug().Err(err).Msgf("No pod metrics")
}
}
@ -90,7 +90,7 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
var pmx *mv1beta1.PodMetricsList
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if pmx, err = client.DialMetrics(p.Client()).FetchPodsMetrics(ns); err != nil {
log.Warn().Err(err).Msgf("No pods metrics")
log.Debug().Err(err).Msgf("No pods metrics")
}
}

View File

@ -252,7 +252,7 @@ func loadRBAC(m ResourceMetas) {
func loadPreferred(f Factory, m ResourceMetas) error {
rr, err := f.Client().CachedDiscoveryOrDie().ServerPreferredResources()
if err != nil {
log.Warn().Err(err).Msgf("Failed to load preferred resources")
log.Debug().Err(err).Msgf("Failed to load preferred resources")
}
for _, r := range rr {
for _, res := range r.APIResources {

View File

@ -27,4 +27,5 @@ const (
KeyMetrics ContextKey = "metrics"
KeyToast ContextKey = "toast"
KeyWithMetrics ContextKey = "withMetrics"
KeyViewConfig ContextKey = "viewConfig"
)

View File

@ -106,7 +106,7 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check,
if err := re.Render(o, ns, &rr[i]); err != nil {
return nil, err
}
if !render.Happy(ns, rr[i]) {
if !render.Happy(ns, re.Header(ns), rr[i]) {
c.Inc(health.Toast)
} else {
c.Inc(health.OK)

View File

@ -29,7 +29,7 @@ type TableListener interface {
// Table represents a table model.
type Table struct {
gvr string
gvr client.GVR
namespace string
data *render.TableData
listeners []TableListener
@ -37,10 +37,11 @@ type Table struct {
refreshRate time.Duration
instance string
mx sync.RWMutex
hasMetrics bool
}
// NewTable returns a new table model.
func NewTable(gvr string) *Table {
func NewTable(gvr client.GVR) *Table {
return &Table{
gvr: gvr,
data: render.NewTableData(),
@ -48,6 +49,11 @@ func NewTable(gvr string) *Table {
}
}
// HasMetrics determines if metrics are available on cluster.
func (t *Table) HasMetrics() bool {
return t.hasMetrics
}
// SetInstance sets a single entry table.
func (t *Table) SetInstance(path string) {
t.instance = path
@ -162,7 +168,6 @@ func (t *Table) SetRefreshRate(d time.Duration) {
// ClusterWide checks if resource is scope for all namespaces.
func (t *Table) ClusterWide() bool {
log.Debug().Msgf("CLUSTER-WIDE %q", t.namespace)
return client.IsClusterWide(t.namespace)
}
@ -214,8 +219,9 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
if !ok {
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
a.Init(factory, client.NewGVR(t.gvr))
a.Init(factory, t.gvr)
t.hasMetrics = factory.Client().HasMetrics()
ns := client.CleanseNamespace(t.namespace)
if client.IsClusterScoped(t.namespace) {
ns = client.AllNamespaces
@ -278,13 +284,13 @@ func (t *Table) getMeta(ctx context.Context) (ResourceMeta, error) {
if !ok {
return ResourceMeta{}, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
meta.DAO.Init(factory, client.NewGVR(t.gvr))
meta.DAO.Init(factory, t.gvr)
return meta, nil
}
func (t *Table) resourceMeta() ResourceMeta {
meta, ok := Registry[t.gvr]
meta, ok := Registry[t.gvr.String()]
if !ok {
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
meta = ResourceMeta{

View File

@ -22,7 +22,7 @@ import (
)
func TestTableReconcile(t *testing.T) {
ta := NewTable("v1/pods")
ta := NewTable(client.NewGVR("v1/pods"))
ta.SetNamespace(client.NamespaceAll)
f := makeFactory()
@ -39,7 +39,7 @@ func TestTableReconcile(t *testing.T) {
}
func TestTableList(t *testing.T) {
ta := NewTable("v1/pods")
ta := NewTable(client.NewGVR("v1/pods"))
ta.SetNamespace("blee")
acc := accessor{}
@ -50,7 +50,7 @@ func TestTableList(t *testing.T) {
}
func TestTableGet(t *testing.T) {
ta := NewTable("v1/pods")
ta := NewTable(client.NewGVR("v1/pods"))
ta.SetNamespace("blee")
f := makeFactory()
@ -90,7 +90,7 @@ func TestTableMeta(t *testing.T) {
for k := range uu {
u := uu[k]
ta := NewTable(u.gvr)
ta := NewTable(client.NewGVR(u.gvr))
m := ta.resourceMeta()
assert.Equal(t, u.accessor, m.DAO)
@ -106,7 +106,7 @@ func TestTableHydrate(t *testing.T) {
assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
assert.Equal(t, 1, len(rr))
assert.Equal(t, 16, len(rr[0].Fields))
assert.Equal(t, 17, len(rr[0].Fields))
}
func TestTableGenericHydrate(t *testing.T) {
@ -179,7 +179,7 @@ type testFactory struct {
var _ dao.Factory = testFactory{}
func (f testFactory) Client() client.Connection {
return nil
return client.NewTestClient()
}
func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
if len(f.rows) > 0 {

View File

@ -21,7 +21,7 @@ import (
)
func TestTableRefresh(t *testing.T) {
ta := model.NewTable("v1/pods")
ta := model.NewTable(client.NewGVR("v1/pods"))
ta.SetNamespace(client.NamespaceAll)
l := tableListener{}
@ -41,7 +41,7 @@ func TestTableRefresh(t *testing.T) {
}
func TestTableNS(t *testing.T) {
ta := model.NewTable("v1/pods")
ta := model.NewTable(client.NewGVR("v1/pods"))
ta.SetNamespace("blee")
assert.Equal(t, "blee", ta.GetNamespace())
@ -50,7 +50,7 @@ func TestTableNS(t *testing.T) {
}
func TestTableAddListener(t *testing.T) {
ta := model.NewTable("v1/pods")
ta := model.NewTable(client.NewGVR("v1/pods"))
ta.SetNamespace("blee")
assert.True(t, ta.Empty())
@ -59,7 +59,7 @@ func TestTableAddListener(t *testing.T) {
}
func TestTableRmListener(t *testing.T) {
ta := model.NewTable("v1/pods")
ta := model.NewTable(client.NewGVR("v1/pods"))
ta.SetNamespace("blee")
l := tableListener{}
@ -86,7 +86,7 @@ type tableFactory struct {
var _ dao.Factory = tableFactory{}
func (f tableFactory) Client() client.Connection {
return nil
return client.NewTestClient()
}
func (f tableFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
if len(f.rows) > 0 {

View File

@ -31,7 +31,7 @@ type TreeListener interface {
// Tree represents a tree model.
type Tree struct {
gvr string
gvr client.GVR
namespace string
root *xray.TreeNode
listeners []TreeListener
@ -41,7 +41,7 @@ type Tree struct {
}
// NewTree returns a new model.
func NewTree(gvr string) *Tree {
func NewTree(gvr client.GVR) *Tree {
return &Tree{
gvr: gvr,
refreshRate: 2 * time.Second,
@ -193,7 +193,7 @@ func (t *Tree) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, erro
if !ok {
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
a.Init(factory, client.NewGVR(t.gvr))
a.Init(factory, t.gvr)
return a.List(ctx, client.CleanseNamespace(t.namespace))
}
@ -206,7 +206,7 @@ func (t *Tree) reconcile(ctx context.Context) error {
}
ns := client.CleanseNamespace(t.namespace)
res := client.NewGVR(t.gvr).R()
res := t.gvr.R()
root := xray.NewTreeNode(res, res)
ctx = context.WithValue(ctx, xray.KeyParent, root)
if _, ok := meta.TreeRenderer.(*xray.Generic); ok {
@ -236,7 +236,7 @@ func (t *Tree) reconcile(ctx context.Context) error {
}
func (t *Tree) resourceMeta() ResourceMeta {
meta, ok := Registry[t.gvr]
meta, ok := Registry[t.gvr.String()]
if !ok {
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
meta = ResourceMeta{

View File

@ -52,7 +52,7 @@ type Renderer interface {
Render(o interface{}, ns string, row *render.Row) error
// Header returns the resource header.
Header(ns string) render.HeaderRow
Header(ns string) render.Header
// ColorerFunc returns a row colorer function.
ColorerFunc() render.ColorerFunc

View File

@ -15,17 +15,17 @@ type Alias struct{}
// ColorerFunc colors a resource row.
func (Alias) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
return func(ns string, _ Header, re RowEvent) tcell.Color {
return tcell.ColorMediumSpringGreen
}
}
// Header returns a header row.
func (Alias) Header(ns string) HeaderRow {
return HeaderRow{
Header{Name: "RESOURCE"},
Header{Name: "COMMAND"},
Header{Name: "APIGROUP"},
func (Alias) Header(ns string) Header {
return Header{
HeaderColumn{Name: "RESOURCE"},
HeaderColumn{Name: "COMMAND"},
HeaderColumn{Name: "APIGROUP"},
}
}

View File

@ -11,7 +11,11 @@ import (
func TestAliasColorer(t *testing.T) {
var a render.Alias
h := render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
}
r := render.Row{ID: "g/v/r", Fields: render.Fields{"r", "blee", "g"}}
uu := map[string]struct {
ns string
@ -36,16 +40,16 @@ func TestAliasColorer(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, a.ColorerFunc()(u.ns, u.re))
assert.Equal(t, u.e, a.ColorerFunc()(u.ns, h, u.re))
})
}
}
func TestAliasHeader(t *testing.T) {
h := render.HeaderRow{
render.Header{Name: "RESOURCE"},
render.Header{Name: "COMMAND"},
render.Header{Name: "APIGROUP"},
h := render.Header{
render.HeaderColumn{Name: "RESOURCE"},
render.HeaderColumn{Name: "COMMAND"},
render.HeaderColumn{Name: "APIGROUP"},
}
var a render.Alias

View File

@ -31,28 +31,27 @@ type Benchmark struct{}
// ColorerFunc colors a resource row.
func (b Benchmark) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
c := tcell.ColorPaleGreen
if !Happy(ns, re.Row) {
c = ErrColor
return func(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) {
return ErrColor
}
return c
return tcell.ColorPaleGreen
}
}
// Header returns a header row.
func (Benchmark) Header(ns string) HeaderRow {
return HeaderRow{
Header{Name: "NAMESPACE"},
Header{Name: "NAME"},
Header{Name: "STATUS"},
Header{Name: "TIME"},
Header{Name: "REQ/S", Align: tview.AlignRight},
Header{Name: "2XX", Align: tview.AlignRight},
Header{Name: "4XX/5XX", Align: tview.AlignRight},
Header{Name: "REPORT"},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (Benchmark) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "STATUS"},
HeaderColumn{Name: "TIME"},
HeaderColumn{Name: "REQ/S", Align: tview.AlignRight},
HeaderColumn{Name: "2XX", Align: tview.AlignRight},
HeaderColumn{Name: "4XX/5XX", Align: tview.AlignRight},
HeaderColumn{Name: "REPORT"},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}

View File

@ -17,8 +17,8 @@ type Chart struct{}
// ColorerFunc colors a resource row.
func (Chart) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
if !Happy(ns, re.Row) {
return func(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) {
return ErrColor
}
@ -27,21 +27,17 @@ func (Chart) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (Chart) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (Chart) Header(_ string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "REVISION"},
HeaderColumn{Name: "STATUS"},
HeaderColumn{Name: "CHART"},
HeaderColumn{Name: "APP VERSION"},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "REVISION"},
Header{Name: "STATUS"},
Header{Name: "CHART"},
Header{Name: "APP VERSION"},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a chart to screen.
@ -52,19 +48,16 @@ func (c Chart) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.FQN(h.Release.Namespace, h.Release.Name)
r.Fields = make(Fields, 0, len(c.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, h.Release.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
h.Release.Namespace,
h.Release.Name,
strconv.Itoa(h.Release.Version),
h.Release.Info.Status.String(),
h.Release.Chart.Metadata.Name+"-"+h.Release.Chart.Metadata.Version,
h.Release.Chart.Metadata.Name + "-" + h.Release.Chart.Metadata.Version,
h.Release.Chart.Metadata.AppVersion,
asStatus(c.diagnose(h.Release.Info.Status.String())),
toAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}),
)
}
return nil
}

View File

@ -5,34 +5,43 @@ 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
type ColorerFunc func(ns string, h Header, re 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
func DefaultColorer(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) {
return ErrColor
}
return col
switch re.Kind {
case EventAdd:
return AddColor
case EventUpdate:
return ModColor
case EventDelete:
return KillColor
default:
return StdColor
}
}

View File

@ -6,7 +6,6 @@ import (
"strconv"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
@ -37,17 +36,17 @@ type ContainerWithMetrics interface {
// Container renders a K8s Container to screen.
type Container struct{}
const readyCol = 2
// ColorerFunc colors a resource row.
func (c Container) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
color := DefaultColorer(ns, re)
if !Happy(ns, re.Row) {
color = ErrColor
return func(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) {
return ErrColor
}
stateCol := h.IndexOf("STATE", true)
if stateCol == -1 {
return DefaultColorer(ns, h, re)
}
stateCol := readyCol + 1
switch strings.TrimSpace(re.Row.Fields[stateCol]) {
case ContainerCreating, PodInitializing:
return AddColor
@ -56,33 +55,32 @@ func (c Container) ColorerFunc() ColorerFunc {
case Completed:
return CompletedColor
case Running:
return DefaultColorer(ns, h, re)
default:
color = ErrColor
return ErrColor
}
return color
}
}
// Header returns a header row.
func (Container) Header(ns string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "IMAGE"},
Header{Name: "READY"},
Header{Name: "STATE"},
Header{Name: "INIT"},
Header{Name: "RS", Align: tview.AlignRight},
Header{Name: "PROBES(L:R)"},
Header{Name: "CPU", Align: tview.AlignRight},
Header{Name: "MEM", Align: tview.AlignRight},
Header{Name: "%CPU/R", Align: tview.AlignRight},
Header{Name: "%MEM/R", Align: tview.AlignRight},
Header{Name: "%CPU/L", Align: tview.AlignRight},
Header{Name: "%MEM/L", Align: tview.AlignRight},
Header{Name: "PORTS"},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (Container) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "IMAGE"},
HeaderColumn{Name: "READY"},
HeaderColumn{Name: "STATE"},
HeaderColumn{Name: "INIT"},
HeaderColumn{Name: "RS", Align: tview.AlignRight},
HeaderColumn{Name: "PROBES(L:R)"},
HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "PORTS"},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}
@ -96,29 +94,28 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
cur, perc, limit := gatherMetrics(co.Container, co.MX)
ready, state, restarts := "false", MissingValue, "0"
if co.Status != nil {
ready, state, restarts = boolToStr(co.Status.Ready), toState(co.Status.State), strconv.Itoa(int(co.Status.RestartCount))
ready, state, restarts = boolToStr(co.Status.Ready), ToContainerState(co.Status.State), strconv.Itoa(int(co.Status.RestartCount))
}
r.ID = co.Container.Name
r.Fields = make(Fields, 0, len(c.Header(client.AllNamespaces)))
r.Fields = append(r.Fields,
r.Fields = Fields{
co.Container.Name,
co.Container.Image,
ready,
state,
boolToStr(co.IsInit),
restarts,
probe(co.Container.LivenessProbe)+":"+probe(co.Container.ReadinessProbe),
probe(co.Container.LivenessProbe) + ":" + probe(co.Container.ReadinessProbe),
cur.cpu,
cur.mem,
perc.cpu,
perc.mem,
limit.cpu,
limit.mem,
toStrPorts(co.Container.Ports),
ToContainerPorts(co.Container.Ports),
asStatus(c.diagnose(state, ready)),
toAge(co.Age),
)
}
return nil
}
@ -170,7 +167,7 @@ func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, p, l met
return
}
func toStrPorts(pp []v1.ContainerPort) string {
func ToContainerPorts(pp []v1.ContainerPort) string {
ports := make([]string, len(pp))
for i, p := range pp {
if len(p.Name) > 0 {
@ -185,7 +182,7 @@ func toStrPorts(pp []v1.ContainerPort) string {
return strings.Join(ports, ",")
}
func toState(s v1.ContainerState) string {
func ToContainerState(s v1.ContainerState) string {
switch {
case s.Waiting != nil:
if s.Waiting.Reason != "" {

View File

@ -16,13 +16,13 @@ type Context struct{}
// ColorerFunc colors a resource row.
func (Context) ColorerFunc() ColorerFunc {
return func(ns string, r RowEvent) tcell.Color {
c := DefaultColorer(ns, r)
return func(ns string, h Header, r RowEvent) tcell.Color {
c := DefaultColorer(ns, h, r)
if r.Kind == EventAdd || r.Kind == EventUpdate {
return c
}
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
c = HighlightColor
return HighlightColor
}
return c
@ -30,12 +30,12 @@ func (Context) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (Context) Header(ns string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "CLUSTER"},
Header{Name: "AUTHINFO"},
Header{Name: "NAMESPACE"},
func (Context) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "CLUSTER"},
HeaderColumn{Name: "AUTHINFO"},
HeaderColumn{Name: "NAMESPACE"},
}
}

View File

@ -18,11 +18,11 @@ func (ClusterRole) ColorerFunc() ColorerFunc {
}
// Header returns a header rbw.
func (ClusterRole) Header(string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "LABELS", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (ClusterRole) Header(string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}

View File

@ -18,14 +18,14 @@ func (ClusterRoleBinding) ColorerFunc() ColorerFunc {
}
// Header returns a header rbw.
func (ClusterRoleBinding) Header(string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "CLUSTERROLE"},
Header{Name: "SUBJECT-KIND"},
Header{Name: "SUBJECTS"},
Header{Name: "LABELS", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (ClusterRoleBinding) Header(string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "CLUSTERROLE"},
HeaderColumn{Name: "SUBJECT-KIND"},
HeaderColumn{Name: "SUBJECTS"},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}

View File

@ -19,11 +19,11 @@ func (CustomResourceDefinition) ColorerFunc() ColorerFunc {
}
// Header returns a header rbw.
func (CustomResourceDefinition) Header(string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "LABELS", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (CustomResourceDefinition) Header(string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}

View File

@ -22,25 +22,21 @@ func (CronJob) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (CronJob) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (CronJob) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "SCHEDULE"},
HeaderColumn{Name: "SUSPEND"},
HeaderColumn{Name: "ACTIVE"},
HeaderColumn{Name: "LAST_SCHEDULE"},
HeaderColumn{Name: "SELECTOR", Wide: true},
HeaderColumn{Name: "CONTAINERS", Wide: true},
HeaderColumn{Name: "IMAGES", Wide: true},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "SCHEDULE"},
Header{Name: "SUSPEND"},
Header{Name: "ACTIVE"},
Header{Name: "LAST_SCHEDULE"},
Header{Name: "SELECTOR", Wide: true},
Header{Name: "CONTAINERS", Wide: true},
Header{Name: "IMAGES", Wide: true},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -61,11 +57,8 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(cj.ObjectMeta)
r.Fields = make(Fields, 0, len(c.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, cj.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
cj.Namespace,
cj.Name,
cj.Spec.Schedule,
boolPtrToStr(cj.Spec.Suspend),
@ -77,7 +70,7 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error {
mapToStr(cj.Labels),
"",
toAge(cj.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -1,5 +1,9 @@
package render
import (
"reflect"
)
// DeltaRow represents a collection of row detlas between old and new row.
type DeltaRow []string
@ -20,6 +24,38 @@ func NewDeltaRow(o, n Row, excludeLast bool) DeltaRow {
return deltas
}
// Diff returns true if deltas differ or false otherwise.
func (d DeltaRow) Diff(r DeltaRow, ageCol int) bool {
if len(d) != len(r) {
return true
}
if ageCol < 0 || ageCol >= len(d) {
return !reflect.DeepEqual(d, r)
}
if !reflect.DeepEqual(d[:ageCol], r[:ageCol]) {
return true
}
if ageCol+1 >= len(d) {
return false
}
return !reflect.DeepEqual(d[ageCol+1:], r[ageCol+1:])
}
// Customize returns a subset of deltas.
func (d DeltaRow) Customize(cols []int, out DeltaRow) {
if d.IsBlank() {
return
}
for i, c := range cols {
if c < len(d) {
out[i] = d[c]
}
}
}
// IsBlank asserts a row has no values in it.
func (d DeltaRow) IsBlank() bool {
if len(d) == 0 {

View File

@ -7,7 +7,85 @@ import (
"github.com/stretchr/testify/assert"
)
func TestDelta(t *testing.T) {
func TestDeltaCustomize(t *testing.T) {
uu := map[string]struct {
r1, r2 render.Row
cols []int
e render.DeltaRow
}{
"same": {
r1: render.Row{
Fields: render.Fields{"a", "b", "c"},
},
r2: render.Row{
Fields: render.Fields{"a", "b", "c"},
},
cols: []int{0, 1, 2},
e: render.DeltaRow{"", "", ""},
},
"empty": {
r1: render.Row{
Fields: render.Fields{"a", "b", "c"},
},
r2: render.Row{
Fields: render.Fields{"a", "b", "c"},
},
e: render.DeltaRow{},
},
"diff-full": {
r1: render.Row{
Fields: render.Fields{"a", "b", "c"},
},
r2: render.Row{
Fields: render.Fields{"a1", "b1", "c1"},
},
cols: []int{0, 1, 2},
e: render.DeltaRow{"a", "b", "c"},
},
"diff-reverse": {
r1: render.Row{
Fields: render.Fields{"a", "b", "c"},
},
r2: render.Row{
Fields: render.Fields{"a1", "b1", "c1"},
},
cols: []int{2, 1, 0},
e: render.DeltaRow{"c", "b", "a"},
},
"diff-skip": {
r1: render.Row{
Fields: render.Fields{"a", "b", "c"},
},
r2: render.Row{
Fields: render.Fields{"a1", "b1", "c1"},
},
cols: []int{2, 0},
e: render.DeltaRow{"c", "a"},
},
"diff-missing": {
r1: render.Row{
Fields: render.Fields{"a", "b", "c"},
},
r2: render.Row{
Fields: render.Fields{"a1", "b1", "c1"},
},
cols: []int{2, 10, 0},
e: render.DeltaRow{"c", "", "a"},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
d := render.NewDeltaRow(u.r1, u.r2, false)
out := make(render.DeltaRow, len(u.cols))
d.Customize(u.cols, out)
assert.Equal(t, u.e, out)
})
}
}
func TestDeltaNew(t *testing.T) {
uu := map[string]struct {
o render.Row
n render.Row
@ -54,11 +132,11 @@ func TestDelta(t *testing.T) {
}
for k := range uu {
uc := uu[k]
u := uu[k]
t.Run(k, func(t *testing.T) {
d := render.NewDeltaRow(uc.o, uc.n, false)
assert.Equal(t, uc.e, d)
assert.Equal(t, uc.blank, d.IsBlank())
d := render.NewDeltaRow(u.o, u.n, false)
assert.Equal(t, u.e, d)
assert.Equal(t, u.blank, d.IsBlank())
})
}
}
@ -82,9 +160,52 @@ func TestDeltaBlank(t *testing.T) {
}
for k := range uu {
uc := uu[k]
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, uc.e, uc.r.IsBlank())
assert.Equal(t, u.e, u.r.IsBlank())
})
}
}
func TestDeltaDiff(t *testing.T) {
uu := map[string]struct {
d1, d2 render.DeltaRow
ageCol int
e bool
}{
"empty": {
d1: render.DeltaRow{"f1", "f2", "f3"},
ageCol: 2,
e: true,
},
"same": {
d1: render.DeltaRow{"f1", "f2", "f3"},
d2: render.DeltaRow{"f1", "f2", "f3"},
ageCol: -1,
},
"diff": {
d1: render.DeltaRow{"f1", "f2", "f3"},
d2: render.DeltaRow{"f1", "f2", "f13"},
ageCol: -1,
e: true,
},
"diff-age-first": {
d1: render.DeltaRow{"f1", "f2", "f3"},
d2: render.DeltaRow{"f1", "f2", "f13"},
ageCol: 0,
e: true,
},
"diff-age-last": {
d1: render.DeltaRow{"f1", "f2", "f3"},
d2: render.DeltaRow{"f1", "f2", "f13"},
ageCol: 2,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.d1.Diff(u.d2, u.ageCol))
})
}
}

View File

@ -6,7 +6,6 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -17,36 +16,22 @@ type Deployment struct{}
// ColorerFunc colors a resource row.
func (d Deployment) ColorerFunc() ColorerFunc {
return func(ns string, r RowEvent) tcell.Color {
c := DefaultColorer(ns, r)
if r.Kind == EventAdd || r.Kind == EventUpdate {
return c
}
if !Happy(ns, r.Row) {
return ErrColor
}
return StdColor
}
return DefaultColorer
}
// Header returns a header row.
func (Deployment) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (Deployment) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "READY"},
HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight},
HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
HeaderColumn{Name: "READY", Align: tview.AlignRight},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "READY"},
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
Header{Name: "AVAILABLE", Align: tview.AlignRight},
Header{Name: "READY", Align: tview.AlignRight},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -63,20 +48,17 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(dp.ObjectMeta)
r.Fields = make(Fields, 0, len(d.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, dp.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
dp.Namespace,
dp.Name,
strconv.Itoa(int(dp.Status.AvailableReplicas))+"/"+strconv.Itoa(int(dp.Status.Replicas)),
strconv.Itoa(int(dp.Status.AvailableReplicas)) + "/" + strconv.Itoa(int(dp.Status.Replicas)),
strconv.Itoa(int(dp.Status.UpdatedReplicas)),
strconv.Itoa(int(dp.Status.AvailableReplicas)),
strconv.Itoa(int(dp.Status.ReadyReplicas)),
mapToStr(dp.Labels),
asStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)),
toAge(dp.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -6,7 +6,6 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -17,38 +16,23 @@ type DaemonSet struct{}
// ColorerFunc colors a resource row.
func (d DaemonSet) ColorerFunc() ColorerFunc {
return func(ns string, r RowEvent) tcell.Color {
c := DefaultColorer(ns, r)
if r.Kind == EventAdd || r.Kind == EventUpdate {
return c
}
if !Happy(ns, r.Row) {
return ErrColor
}
return StdColor
}
return DefaultColorer
}
// Header returns a header row.
func (DaemonSet) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (DaemonSet) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
HeaderColumn{Name: "READY", Align: tview.AlignRight},
HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight},
HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "DESIRED", Align: tview.AlignRight},
Header{Name: "CURRENT", Align: tview.AlignRight},
Header{Name: "READY", Align: tview.AlignRight},
Header{Name: "UP-TO-DATE", Align: tview.AlignRight},
Header{Name: "AVAILABLE", Align: tview.AlignRight},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -64,11 +48,8 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(ds.ObjectMeta)
r.Fields = make(Fields, 0, len(d.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, ds.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
ds.Namespace,
ds.Name,
strconv.Itoa(int(ds.Status.DesiredNumberScheduled)),
strconv.Itoa(int(ds.Status.CurrentNumberScheduled)),
@ -78,7 +59,7 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
mapToStr(ds.Labels),
asStatus(d.diagnose(ds.Status.DesiredNumberScheduled, ds.Status.NumberReady)),
toAge(ds.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -20,17 +20,13 @@ func (Endpoints) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (Endpoints) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (Endpoints) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "ENDPOINTS"},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "ENDPOINTS"},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -47,14 +43,12 @@ func (e Endpoints) Render(o interface{}, ns string, r *Row) error {
r.ID = client.MetaFQN(ep.ObjectMeta)
r.Fields = make(Fields, 0, len(e.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, ep.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
ep.Namespace,
ep.Name,
missing(toEPs(ep.Subsets)),
toAge(ep.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -19,42 +19,35 @@ type Event struct{}
// ColorerFunc colors a resource row.
func (e Event) ColorerFunc() ColorerFunc {
return func(ns string, r RowEvent) tcell.Color {
c := DefaultColorer(ns, r)
if !Happy(ns, r.Row) {
return func(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) {
return ErrColor
}
markCol := 3
if !client.IsAllNamespaces(ns) {
markCol = 2
reasonCol := h.IndexOf("REASON", true)
if reasonCol == -1 {
return DefaultColorer(ns, h, re)
}
if strings.TrimSpace(r.Row.Fields[markCol]) == "Killing" {
if strings.TrimSpace(re.Row.Fields[reasonCol]) == "Killing" {
return KillColor
}
return c
return DefaultColorer(ns, h, re)
}
}
// Header returns a header rbw.
func (Event) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (Event) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "TYPE"},
HeaderColumn{Name: "REASON"},
HeaderColumn{Name: "SOURCE"},
HeaderColumn{Name: "COUNT", Align: tview.AlignRight},
HeaderColumn{Name: "MESSAGE", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "TYPE"},
Header{Name: "REASON"},
Header{Name: "SOURCE"},
Header{Name: "COUNT", Align: tview.AlignRight},
Header{Name: "MESSAGE", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -70,11 +63,8 @@ func (e Event) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(ev.ObjectMeta)
r.Fields = make(Fields, 0, len(e.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, ev.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
ev.Namespace,
asRef(ev.InvolvedObject),
ev.Type,
ev.Reason,
@ -82,7 +72,8 @@ func (e Event) Render(o interface{}, ns string, r *Row) error {
strconv.Itoa(int(ev.Count)),
ev.Message,
asStatus(e.diagnose(ev.Type)),
toAge(ev.LastTimestamp))
toAge(ev.LastTimestamp),
}
return nil
}

View File

@ -35,24 +35,20 @@ func (Generic) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (g *Generic) Header(ns string) HeaderRow {
func (g *Generic) Header(ns string) Header {
if g.table == nil {
return HeaderRow{}
}
h := make(HeaderRow, 0, len(g.table.ColumnDefinitions))
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
return Header{}
}
h := make(Header, 0, len(g.table.ColumnDefinitions))
for i, c := range g.table.ColumnDefinitions {
if c.Name == ageTableCol {
g.ageIndex = i
continue
}
h = append(h, Header{Name: strings.ToUpper(c.Name)})
h = append(h, HeaderColumn{Name: strings.ToUpper(c.Name)})
}
if g.ageIndex > 0 {
h = append(h, Header{Name: "AGE"})
h = append(h, HeaderColumn{Name: "AGE", Time: true,})
}
return h

View File

@ -16,41 +16,41 @@ func TestGenericRender(t *testing.T) {
table *metav1beta1.Table
eID string
eFields render.Fields
eHeader render.HeaderRow
eHeader render.Header
}{
"withNS": {
ns: "ns1",
table: makeNSGeneric(),
eID: "ns1/c1",
eFields: render.Fields{"c1", "c2", "c3"},
eHeader: render.HeaderRow{
render.Header{Name: "A"},
render.Header{Name: "B"},
render.Header{Name: "C"},
eHeader: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
},
"nsAll": {
"all": {
ns: client.NamespaceAll,
table: makeNSGeneric(),
eID: "ns1/c1",
eFields: render.Fields{"ns1", "c1", "c2", "c3"},
eHeader: render.HeaderRow{
render.Header{Name: "NAMESPACE"},
render.Header{Name: "A"},
render.Header{Name: "B"},
render.Header{Name: "C"},
eHeader: render.Header{
// render.HeaderColumn{Name: "NAMESPACE"},
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
},
"AllNS": {
"allNS": {
ns: client.AllNamespaces,
table: makeNSGeneric(),
eID: "ns1/c1",
eFields: render.Fields{"ns1", "c1", "c2", "c3"},
eHeader: render.HeaderRow{
render.Header{Name: "NAMESPACE"},
render.Header{Name: "A"},
render.Header{Name: "B"},
render.Header{Name: "C"},
eHeader: render.Header{
// render.HeaderColumn{Name: "NAMESPACE"},
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
},
"clusterWide": {
@ -58,10 +58,10 @@ func TestGenericRender(t *testing.T) {
table: makeNoNSGeneric(),
eID: "-/c1",
eFields: render.Fields{"c1", "c2", "c3"},
eHeader: render.HeaderRow{
render.Header{Name: "A"},
render.Header{Name: "B"},
render.Header{Name: "C"},
eHeader: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
},
"age": {
@ -69,10 +69,10 @@ func TestGenericRender(t *testing.T) {
table: makeAgeGeneric(),
eID: "-/c1",
eFields: render.Fields{"c1", "c2", "Age"},
eHeader: render.HeaderRow{
render.Header{Name: "A"},
render.Header{Name: "C"},
render.Header{Name: "AGE"},
eHeader: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "C"},
render.HeaderColumn{Name: "AGE", Time: true,},
},
},
}

155
internal/render/header.go Normal file
View File

@ -0,0 +1,155 @@
package render
import (
"reflect"
"github.com/rs/zerolog/log"
)
const ageCol = "AGE"
// HeaderColumn represent a table header
type HeaderColumn struct {
Name string
Align int
Decorator DecoratorFunc
Hide bool
Wide bool
MX bool
Time bool
}
// Clone copies a header.
func (h HeaderColumn) Clone() HeaderColumn {
return h
}
// ----------------------------------------------------------------------------
// Header represents a table header.
type Header []HeaderColumn
// Clone duplicates a header.
func (h Header) Clone() Header {
header := make(Header, len(h))
for i, c := range h {
header[i] = c.Clone()
}
return header
}
// MapIndices returns a collection of mapped column indices based of the requested columns.
func (h Header) MapIndices(cols []string, wide bool, ii []int) {
cc := make(map[int]struct{}, len(cols))
var lastIndex int
log.Debug().Msgf("MAP %d -- %d ", len(cols), len(ii))
for i, col := range cols {
idx := h.IndexOf(col, true)
ii[i], cc[idx] = idx, struct{}{}
lastIndex = i
}
if !wide {
return
}
for i := range h {
if _, ok := cc[i]; ok {
continue
}
lastIndex++
if lastIndex < len(ii) {
ii[lastIndex] = i
}
}
}
// Customize builds a header from custom col definitions.
func (h Header) Customize(cols []string, wide bool) Header {
if len(cols) == 0 {
return h
}
cc := make(Header, 0, len(h))
xx := make(map[int]struct{}, len(h))
for _, c := range cols {
idx := h.IndexOf(c, true)
if idx == -1 {
log.Warn().Msgf("Column %s is not available on this resource", c)
continue
}
xx[idx] = struct{}{}
col := h[idx].Clone()
col.Wide = false
cc = append(cc, col)
}
if !wide {
return cc
}
for i, c := range h {
if _, ok := xx[i]; ok {
continue
}
col := c.Clone()
col.Wide = true
cc = append(cc, col)
}
return cc
}
// Diff returns true if the header changed.
func (h Header) Diff(header Header) bool {
if len(h) != len(header) {
return true
}
return !reflect.DeepEqual(h, header)
}
// Columns return header as a collection of strings.
func (h Header) Columns(wide bool) []string {
if len(h) == 0 {
return nil
}
var cc []string
for _, c := range h {
if !wide && c.Wide {
continue
}
cc = append(cc, c.Name)
}
return cc
}
// HasAge returns true if table has an age column.
func (h Header) HasAge() bool {
return h.IndexOf(ageCol, true) != -1
}
// AgeCol checks if given column index is the age column.
func (h Header) IsAgeCol(col int) bool {
if !h.HasAge() || col >= len(h) {
return false
}
return h[col].Time
}
// ValidColIndex returns the valid col index or -1 if none.
func (h Header) ValidColIndex() int {
return h.IndexOf("VALID", true)
}
// IndexOf returns the col index or -1 if none.
func (h Header) IndexOf(colName string, includeWide bool) int {
for i, c := range h {
if c.Wide && !includeWide {
continue
}
if c.Name == colName {
return i
}
}
return -1
}

View File

@ -0,0 +1,338 @@
package render_test
import (
"testing"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestHeaderMapIndices(t *testing.T) {
uu := map[string]struct {
h1 render.Header
cols []string
wide bool
e []int
}{
"all": {
h1: makeHeader(),
cols: []string{"A", "B", "C"},
e: []int{0, 1, 2},
},
"reverse": {
h1: makeHeader(),
cols: []string{"C", "B", "A"},
e: []int{2, 1, 0},
},
"missing": {
h1: makeHeader(),
cols: []string{"Duh", "B", "A"},
e: []int{-1, 1, 0},
},
"skip": {
h1: makeHeader(),
cols: []string{"C", "A"},
e: []int{2, 0},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
ii := make([]int, len(u.cols))
u.h1.MapIndices(u.cols, u.wide, ii)
assert.Equal(t, u.e, ii)
})
}
}
func TestHeaderIndexOf(t *testing.T) {
uu := map[string]struct {
h render.Header
name string
wide bool
e int
}{
"shown": {
h: makeHeader(),
name: "A",
e: 0,
},
"hidden": {
h: makeHeader(),
name: "B",
e: -1,
},
"hidden-wide": {
h: makeHeader(),
name: "B",
wide: true,
e: 1,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.h.IndexOf(u.name, u.wide))
})
}
}
func TestHeaderCustomize(t *testing.T) {
uu := map[string]struct {
h render.Header
cols []string
wide bool
e render.Header
}{
"default": {
h: makeHeader(),
e: makeHeader(),
},
"default-wide": {
h: makeHeader(),
wide: true,
e: makeHeader(),
},
"reverse": {
h: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "C"},
},
cols: []string{"C", "A"},
e: render.Header{
render.HeaderColumn{Name: "C"},
render.HeaderColumn{Name: "A"},
},
},
"reverse-wide": {
h: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "C"},
},
cols: []string{"C", "A"},
wide: true,
e: render.Header{
render.HeaderColumn{Name: "C"},
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
},
},
"toggle-wide": {
h: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "C"},
},
cols: []string{"C", "B"},
wide: true,
e: render.Header{
render.HeaderColumn{Name: "C"},
render.HeaderColumn{Name: "B", Wide: false},
render.HeaderColumn{Name: "A", Wide: true},
},
},
"missing": {
h: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "C"},
},
cols: []string{"BLEE", "A"},
wide: true,
e: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "C", Wide: true},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.h.Customize(u.cols, u.wide))
})
}
}
func TestHeaderDiff(t *testing.T) {
uu := map[string]struct {
h1, h2 render.Header
e bool
}{
"same": {
h1: makeHeader(),
h2: makeHeader(),
},
"size": {
h1: makeHeader(),
h2: makeHeader()[1:],
e: true,
},
"differ-wide": {
h1: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "C"},
},
h2: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
e: true,
},
"differ-order": {
h1: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "C"},
},
h2: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "C"},
render.HeaderColumn{Name: "B", Wide: true},
},
e: true,
},
"differ-name": {
h1: render.Header{
render.HeaderColumn{Name: "A"},
},
h2: render.Header{
render.HeaderColumn{Name: "B"},
},
e: true,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.h1.Diff(u.h2))
})
}
}
func TestHeaderHasAge(t *testing.T) {
uu := map[string]struct {
h render.Header
age, e bool
}{
"no-age": {
h: render.Header{},
},
"age": {
h: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "AGE", Time: true},
},
e: true,
age: true,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.h.HasAge())
assert.Equal(t, u.e, u.h.IsAgeCol(2))
})
}
}
func TestHeaderValidColIndex(t *testing.T) {
uu := map[string]struct {
h render.Header
e int
}{
"none": {
h: render.Header{},
e: -1,
},
"valid": {
h: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "VALID", Wide: true},
},
e: 2,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.h.ValidColIndex())
})
}
}
func TestHeaderColumns(t *testing.T) {
uu := map[string]struct {
h render.Header
wide bool
e []string
}{
"empty": {
h: render.Header{},
},
"regular": {
h: makeHeader(),
e: []string{"A", "C"},
},
"wide": {
h: makeHeader(),
e: []string{"A", "B", "C"},
wide: true,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.h.Columns(u.wide))
})
}
}
func TestHeaderClone(t *testing.T) {
uu := map[string]struct {
h render.Header
}{
"empty": {
h: render.Header{},
},
"full": {
h: makeHeader(),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c := u.h.Clone()
assert.Equal(t, len(u.h), len(c))
if len(u.h) > 0 {
u.h[0].Name = "blee"
assert.Equal(t, "A", c[0].Name)
}
})
}
}
// ----------------------------------------------------------------------------
// Helpers...
func makeHeader() render.Header {
return render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "C"},
}
}

View File

@ -14,8 +14,14 @@ import (
)
// Happy returns true if resoure is happy, false otherwise
func Happy(ns string, r Row) bool {
validCol := r.Len() - 2
func Happy(ns string, h Header, r Row) bool {
if len(r.Fields) == 0 {
return true
}
validCol := h.IndexOf("VALID", true)
if validCol < 0 {
return true
}
return strings.TrimSpace(r.Fields[validCol]) == ""
}

View File

@ -23,22 +23,18 @@ func (HorizontalPodAutoscaler) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (HorizontalPodAutoscaler) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (HorizontalPodAutoscaler) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "REFERENCE"},
HeaderColumn{Name: "TARGETS%"},
HeaderColumn{Name: "MINPODS", Align: tview.AlignRight},
HeaderColumn{Name: "MAXPODS", Align: tview.AlignRight},
HeaderColumn{Name: "REPLICAS", Align: tview.AlignRight},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "REFERENCE"},
Header{Name: "TARGETS%"},
Header{Name: "MINPODS", Align: tview.AlignRight},
Header{Name: "MAXPODS", Align: tview.AlignRight},
Header{Name: "REPLICAS", Align: tview.AlignRight},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -62,7 +58,7 @@ func (h HorizontalPodAutoscaler) Render(o interface{}, ns string, r *Row) error
}
}
func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, ns string, r *Row) error {
func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, _ string, r *Row) error {
var hpa autoscalingv1.HorizontalPodAutoscaler
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &hpa)
if err != nil {
@ -70,11 +66,8 @@ func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, ns str
}
r.ID = client.MetaFQN(hpa.ObjectMeta)
r.Fields = make(Fields, 0, len(h.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, hpa.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
hpa.Namespace,
hpa.ObjectMeta.Name,
hpa.Spec.ScaleTargetRef.Name,
toMetricsV1(hpa.Spec, hpa.Status),
@ -83,12 +76,12 @@ func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, ns str
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
"",
toAge(hpa.ObjectMeta.CreationTimestamp),
)
}
return nil
}
func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, ns string, r *Row) error {
func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, _ string, r *Row) error {
var hpa autoscalingv2beta1.HorizontalPodAutoscaler
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &hpa)
if err != nil {
@ -96,12 +89,8 @@ func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, ns s
}
r.ID = client.MetaFQN(hpa.ObjectMeta)
r.Fields = make(Fields, 0, len(h.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, hpa.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
hpa.Namespace,
hpa.ObjectMeta.Name,
hpa.Spec.ScaleTargetRef.Name,
toMetricsV2b1(hpa.Spec.Metrics, hpa.Status.CurrentMetrics),
@ -110,12 +99,12 @@ func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, ns s
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
"",
toAge(hpa.ObjectMeta.CreationTimestamp),
)
}
return nil
}
func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, ns string, r *Row) error {
func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, _ string, r *Row) error {
var hpa autoscalingv2beta2.HorizontalPodAutoscaler
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &hpa)
if err != nil {
@ -123,12 +112,8 @@ func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, ns s
}
r.ID = client.MetaFQN(hpa.ObjectMeta)
r.Fields = make(Fields, 0, len(h.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, hpa.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
hpa.Namespace,
hpa.ObjectMeta.Name,
hpa.Spec.ScaleTargetRef.Name,
toMetricsV2b2(hpa.Spec.Metrics, hpa.Status.CurrentMetrics),
@ -137,7 +122,7 @@ func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, ns s
strconv.Itoa(int(hpa.Status.CurrentReplicas)),
"",
toAge(hpa.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -20,20 +20,16 @@ func (Ingress) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (Ingress) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (Ingress) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "HOSTS"},
HeaderColumn{Name: "ADDRESS"},
HeaderColumn{Name: "PORT"},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "HOSTS"},
Header{Name: "ADDRESS"},
Header{Name: "PORT"},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -49,18 +45,15 @@ func (i Ingress) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(ing.ObjectMeta)
r.Fields = make(Fields, 0, len(i.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, ing.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
ing.Namespace,
ing.Name,
toHosts(ing.Spec.Rules),
toAddress(ing.Status.LoadBalancer),
toTLSPorts(ing.Spec.TLS),
"",
toAge(ing.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -24,22 +24,18 @@ func (Job) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (Job) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (Job) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "COMPLETIONS"},
HeaderColumn{Name: "DURATION"},
HeaderColumn{Name: "SELECTOR", Wide: true},
HeaderColumn{Name: "CONTAINERS", Wide: true},
HeaderColumn{Name: "IMAGES", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "COMPLETIONS"},
Header{Name: "DURATION"},
Header{Name: "SELECTOR", Wide: true},
Header{Name: "CONTAINERS", Wide: true},
Header{Name: "IMAGES", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -55,13 +51,11 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
}
ready := toCompletion(job.Spec, job.Status)
r.ID = client.MetaFQN(job.ObjectMeta)
r.Fields = make(Fields, 0, len(j.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, job.Namespace)
}
cc, ii := toContainers(job.Spec.Template.Spec)
r.Fields = append(r.Fields,
r.ID = client.MetaFQN(job.ObjectMeta)
r.Fields = Fields{
job.Namespace,
job.Name,
ready,
toDuration(job.Status),
@ -70,7 +64,7 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
ii,
asStatus(j.diagnose(ready, job.Status.CompletionTime)),
toAge(job.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -8,7 +8,6 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -26,36 +25,28 @@ type Node struct{}
// ColorerFunc colors a resource row.
func (n Node) ColorerFunc() ColorerFunc {
return func(ns string, r RowEvent) tcell.Color {
c := DefaultColorer(ns, r)
if !Happy(ns, r.Row) {
return ErrColor
}
return c
}
return DefaultColorer
}
// Header returns a header row.
func (Node) Header(_ string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "STATUS"},
Header{Name: "ROLE", Wide: true},
Header{Name: "VERSION", Wide: true},
Header{Name: "KERNEL", Wide: true},
Header{Name: "INTERNAL-IP", Wide: true},
Header{Name: "EXTERNAL-IP", Wide: true},
Header{Name: "CPU", Align: tview.AlignRight},
Header{Name: "MEM", Align: tview.AlignRight},
Header{Name: "%CPU", Align: tview.AlignRight},
Header{Name: "%MEM", Align: tview.AlignRight},
Header{Name: "ACPU", Align: tview.AlignRight},
Header{Name: "AMEM", Align: tview.AlignRight},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (Node) Header(_ string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "STATUS"},
HeaderColumn{Name: "ROLE"},
HeaderColumn{Name: "VERSION"},
HeaderColumn{Name: "KERNEL", Wide: true},
HeaderColumn{Name: "INTERNAL-IP", Wide: true},
HeaderColumn{Name: "EXTERNAL-IP", Wide: true},
HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "ACPU", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "AMEM", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}
@ -90,8 +81,7 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
sort.Sort(roles)
r.ID = client.FQN("", na)
r.Fields = make(Fields, 0, len(n.Header(ns)))
r.Fields = append(r.Fields,
r.Fields = Fields{
no.Name,
join(statuses, ","),
join(roles, ","),
@ -108,7 +98,7 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
mapToStr(no.Labels),
asStatus(n.diagnose(statuses)),
toAge(no.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -20,24 +20,20 @@ func (NetworkPolicy) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (NetworkPolicy) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (NetworkPolicy) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "ING-SELECTOR", Wide: true},
HeaderColumn{Name: "ING-PORTS"},
HeaderColumn{Name: "ING-BLOCK"},
HeaderColumn{Name: "EGR-SELECTOR", Wide: true},
HeaderColumn{Name: "EGR-PORTS"},
HeaderColumn{Name: "EGR-BLOCK"},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "ING-SELECTOR", Wide: true},
Header{Name: "ING-PORTS"},
Header{Name: "ING-BLOCK"},
Header{Name: "EGR-SELECTOR", Wide: true},
Header{Name: "EGR-PORTS"},
Header{Name: "EGR-BLOCK"},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -56,11 +52,8 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
ep, es, eb := egress(np.Spec.Egress)
r.ID = client.MetaFQN(np.ObjectMeta)
r.Fields = make(Fields, 0, len(n.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, np.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
np.Namespace,
np.Name,
is,
ip,
@ -71,7 +64,7 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
mapToStr(np.Labels),
"",
toAge(np.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -17,34 +17,32 @@ type Namespace struct{}
// ColorerFunc colors a resource row.
func (n Namespace) ColorerFunc() ColorerFunc {
return func(ns string, r RowEvent) tcell.Color {
c := DefaultColorer(ns, r)
if r.Kind == EventAdd {
return c
}
if r.Kind == EventUpdate {
c = StdColor
}
if !Happy(ns, r.Row) {
return func(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) {
return ErrColor
}
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
c = HighlightColor
if re.Kind == EventAdd {
return DefaultColorer(ns, h, re)
}
if re.Kind == EventUpdate {
return StdColor
}
if strings.Contains(strings.TrimSpace(re.Row.Fields[0]), "*") {
return HighlightColor
}
return c
return DefaultColorer(ns, h, re)
}
}
// Header returns a header rbw.
func (Namespace) Header(string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "STATUS"},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (Namespace) Header(string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "STATUS"},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}

View File

@ -27,10 +27,15 @@ func TestNSColorer(t *testing.T) {
{"", render.RowEvent{Kind: render.EventUnchanged, Row: dead}, render.ErrColor},
}
h := render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
}
var n render.Namespace
f := n.ColorerFunc()
for _, u := range uu {
assert.Equal(t, u.e, f(u.ns, u.r))
assert.Equal(t, u.e, f(u.ns, h, u.r))
}
}

View File

@ -25,8 +25,8 @@ type OpenFaas struct{}
// ColorerFunc colors a resource row.
func (o OpenFaas) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
if !Happy(ns, re.Row) {
return func(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) {
return ErrColor
}
@ -35,23 +35,19 @@ func (o OpenFaas) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (OpenFaas) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (OpenFaas) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "STATUS"},
HeaderColumn{Name: "IMAGE"},
HeaderColumn{Name: "LABELS"},
HeaderColumn{Name: "INVOCATIONS", Align: tview.AlignRight},
HeaderColumn{Name: "REPLICAS", Align: tview.AlignRight},
HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "STATUS"},
Header{Name: "IMAGE"},
Header{Name: "LABELS"},
Header{Name: "INVOCATIONS", Align: tview.AlignRight},
Header{Name: "REPLICAS", Align: tview.AlignRight},
Header{Name: "AVAILABLE", Align: tview.AlignRight},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a chart to screen.
@ -71,11 +67,8 @@ func (o OpenFaas) Render(i interface{}, ns string, r *Row) error {
}
r.ID = client.FQN(fn.Function.Namespace, fn.Function.Name)
r.Fields = make(Fields, 0, len(o.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, fn.Function.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
fn.Function.Namespace,
fn.Function.Name,
status,
fn.Function.Image,
@ -85,7 +78,7 @@ func (o OpenFaas) Render(i interface{}, ns string, r *Row) error {
strconv.Itoa(int(fn.Function.AvailableReplicas)),
asStatus(o.diagnose(status)),
toAge(metav1.Time{Time: time.Now()}),
)
}
return nil
}

View File

@ -6,7 +6,6 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
v1beta1 "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -18,39 +17,24 @@ type PodDisruptionBudget struct{}
// ColorerFunc colors a resource row.
func (p PodDisruptionBudget) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
c := DefaultColorer(ns, re)
if re.Kind == EventAdd || re.Kind == EventUpdate {
return c
}
if !Happy(ns, re.Row) {
return ErrColor
}
return StdColor
}
return DefaultColorer
}
// Header returns a header row.
func (PodDisruptionBudget) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (PodDisruptionBudget) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "MIN AVAILABLE", Align: tview.AlignRight},
HeaderColumn{Name: "MAX_ UNAVAILABLE", Align: tview.AlignRight},
HeaderColumn{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight},
HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
HeaderColumn{Name: "EXPECTED", Align: tview.AlignRight},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "MIN AVAILABLE", Align: tview.AlignRight},
Header{Name: "MAX_ UNAVAILABLE", Align: tview.AlignRight},
Header{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight},
Header{Name: "CURRENT", Align: tview.AlignRight},
Header{Name: "DESIRED", Align: tview.AlignRight},
Header{Name: "EXPECTED", Align: tview.AlignRight},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -66,11 +50,8 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(pdb.ObjectMeta)
r.Fields = make(Fields, 0, len(p.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, pdb.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
pdb.Namespace,
pdb.Name,
numbToStr(pdb.Spec.MinAvailable),
numbToStr(pdb.Spec.MaxUnavailable),
@ -81,7 +62,7 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
mapToStr(pdb.Labels),
asStatus(p.diagnose(pdb.Spec.MinAvailable, pdb.Status.CurrentHealthy)),
toAge(pdb.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -22,60 +22,53 @@ type Pod struct{}
// ColorerFunc colors a resource row.
func (p Pod) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
c := DefaultColorer(ns, re)
statusCol := 4
if !client.IsAllNamespaces(ns) {
statusCol--
return func(ns string, h Header, re RowEvent) tcell.Color {
statusCol := h.IndexOf("STATUS", true)
if statusCol == -1 {
return DefaultColorer(ns, h, re)
}
status := strings.TrimSpace(re.Row.Fields[statusCol])
switch status {
case ContainerCreating, PodInitializing:
c = AddColor
return AddColor
case Initialized:
c = HighlightColor
return HighlightColor
case Completed:
c = CompletedColor
return CompletedColor
case Running:
c = StdColor
return StdColor
case Terminating:
c = KillColor
return KillColor
default:
if !Happy(ns, re.Row) {
c = ErrColor
if !Happy(ns, h, re.Row) {
return ErrColor
}
return DefaultColorer(ns, h, re)
}
return c
}
}
// Header returns a header row.
func (Pod) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (Pod) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "READY"},
HeaderColumn{Name: "RS", Align: tview.AlignRight},
HeaderColumn{Name: "STATUS"},
HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true},
HeaderColumn{Name: "IP"},
HeaderColumn{Name: "NODE"},
HeaderColumn{Name: "QOS", Wide: true},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "READY"},
Header{Name: "RS", Align: tview.AlignRight},
Header{Name: "STATUS"},
Header{Name: "CPU", Align: tview.AlignRight},
Header{Name: "MEM", Align: tview.AlignRight},
Header{Name: "%CPU/R", Align: tview.AlignRight},
Header{Name: "%MEM/R", Align: tview.AlignRight},
Header{Name: "%CPU/L", Align: tview.AlignRight},
Header{Name: "%MEM/L", Align: tview.AlignRight},
Header{Name: "IP", Wide: true},
Header{Name: "NODE", Wide: true},
Header{Name: "QOS", Wide: true},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -96,13 +89,10 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
c, perc := p.gatherPodMX(&po, pwm.MX)
phase := p.Phase(&po)
r.ID = client.MetaFQN(po.ObjectMeta)
r.Fields = make(Fields, 0, len(p.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, po.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
po.Namespace,
po.ObjectMeta.Name,
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
strconv.Itoa(cr) + "/" + strconv.Itoa(len(ss)),
strconv.Itoa(rc),
phase,
c.cpu,
@ -117,7 +107,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error {
mapToStr(po.Labels),
asStatus(p.diagnose(phase, cr, len(ss))),
toAge(po.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -51,10 +51,19 @@ func TestPodColorer(t *testing.T) {
{"blee", render.RowEvent{Kind: render.EventUpdate, Row: notReady}, render.ErrColor},
}
h := render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
render.HeaderColumn{Name: "D"},
render.HeaderColumn{Name: "E"},
render.HeaderColumn{Name: "F"},
}
var p render.Pod
f := p.ColorerFunc()
for _, u := range uu {
assert.Equal(t, u.e, f(u.ns, u.r))
assert.Equal(t, u.e, f(u.ns, h, u.r))
}
}

View File

@ -9,17 +9,17 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
func rbacVerbHeader() HeaderRow {
return HeaderRow{
Header{Name: "GET "},
Header{Name: "LIST "},
Header{Name: "WATCH "},
Header{Name: "CREATE"},
Header{Name: "PATCH "},
Header{Name: "UPDATE"},
Header{Name: "DELETE"},
Header{Name: "DEL-LIST "},
Header{Name: "EXTRAS", Wide: true},
func rbacVerbHeader() Header {
return Header{
HeaderColumn{Name: "GET "},
HeaderColumn{Name: "LIST "},
HeaderColumn{Name: "WATCH "},
HeaderColumn{Name: "CREATE"},
HeaderColumn{Name: "PATCH "},
HeaderColumn{Name: "UPDATE"},
HeaderColumn{Name: "DELETE"},
HeaderColumn{Name: "DEL-LIST "},
HeaderColumn{Name: "EXTRAS", Wide: true},
}
}
@ -28,21 +28,21 @@ type Policy struct{}
// ColorerFunc colors a resource row.
func (Policy) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
return func(ns string, _ Header, re RowEvent) tcell.Color {
return tcell.ColorMediumSpringGreen
}
}
// Header returns a header row.
func (Policy) Header(ns string) HeaderRow {
h := HeaderRow{
Header{Name: "NAMESPACE"},
Header{Name: "NAME"},
Header{Name: "API GROUP"},
Header{Name: "BINDING"},
func (Policy) Header(ns string) Header {
h := Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "API GROUP"},
HeaderColumn{Name: "BINDING"},
}
h = append(h, rbacVerbHeader()...)
h = append(h, Header{Name: "VALID", Wide: true})
h = append(h, HeaderColumn{Name: "VALID", Wide: true})
return h
}

View File

@ -33,23 +33,23 @@ type PortForward struct{}
// ColorerFunc colors a resource row.
func (PortForward) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
return func(ns string, _ Header, re RowEvent) tcell.Color {
return tcell.ColorSkyblue
}
}
// Header returns a header row.
func (PortForward) Header(ns string) HeaderRow {
return HeaderRow{
Header{Name: "NAMESPACE"},
Header{Name: "POD"},
Header{Name: "CONTAINER"},
Header{Name: "PORTS"},
Header{Name: "URL"},
Header{Name: "C"},
Header{Name: "N"},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (PortForward) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "CONTAINER"},
HeaderColumn{Name: "PORTS"},
HeaderColumn{Name: "URL"},
HeaderColumn{Name: "C"},
HeaderColumn{Name: "N"},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}

View File

@ -17,42 +17,45 @@ type PersistentVolume struct{}
// ColorerFunc colors a resource row.
func (p PersistentVolume) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
c := DefaultColorer(ns, re)
if re.Kind == EventAdd || re.Kind == EventUpdate {
return c
}
if !Happy(ns, re.Row) {
return func(ns string, h Header, re RowEvent) tcell.Color {
if !Happy(ns, h, re.Row) {
return ErrColor
}
switch strings.TrimSpace(re.Row.Fields[4]) {
case "Bound":
c = StdColor
case "Available":
c = tcell.ColorYellow
if re.Kind == EventAdd || re.Kind == EventUpdate {
return DefaultColorer(ns, h, re)
}
return c
statusCol := h.IndexOf("STATUS", true)
if statusCol == -1 {
return DefaultColorer(ns, h, re)
}
switch strings.TrimSpace(re.Row.Fields[statusCol]) {
case "Bound":
return StdColor
case "Available":
return tcell.ColorYellow
}
return DefaultColorer(ns, h, re)
}
}
// Header returns a header rbw.
func (PersistentVolume) Header(string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "CAPACITY"},
Header{Name: "ACCESS MODES"},
Header{Name: "RECLAIM POLICY"},
Header{Name: "STATUS"},
Header{Name: "CLAIM"},
Header{Name: "STORAGECLASS"},
Header{Name: "REASON"},
Header{Name: "VOLUMEMODE", Wide: true},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (PersistentVolume) Header(string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "CAPACITY"},
HeaderColumn{Name: "ACCESS MODES"},
HeaderColumn{Name: "RECLAIM POLICY"},
HeaderColumn{Name: "STATUS"},
HeaderColumn{Name: "CLAIM"},
HeaderColumn{Name: "STORAGECLASS"},
HeaderColumn{Name: "REASON"},
HeaderColumn{Name: "VOLUMEMODE", Wide: true},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}

View File

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -15,38 +14,23 @@ type PersistentVolumeClaim struct{}
// ColorerFunc colors a resource row.
func (p PersistentVolumeClaim) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
c := DefaultColorer(ns, re)
if re.Kind == EventAdd || re.Kind == EventUpdate {
return c
}
if !Happy(ns, re.Row) {
return ErrColor
}
return c
}
return DefaultColorer
}
// Header returns a header rbw.
func (PersistentVolumeClaim) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (PersistentVolumeClaim) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "STATUS"},
HeaderColumn{Name: "VOLUME"},
HeaderColumn{Name: "CAPACITY"},
HeaderColumn{Name: "ACCESS MODES"},
HeaderColumn{Name: "STORAGECLASS"},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "STATUS"},
Header{Name: "VOLUME"},
Header{Name: "CAPACITY"},
Header{Name: "ACCESS MODES"},
Header{Name: "STORAGECLASS"},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -81,11 +65,8 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(pvc.ObjectMeta)
r.Fields = make(Fields, 0, len(p.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, pvc.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
pvc.Namespace,
pvc.Name,
string(phase),
pvc.Spec.VolumeName,
@ -95,7 +76,7 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
mapToStr(pvc.Labels),
asStatus(p.diagnose(string(phase))),
toAge(pvc.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -34,38 +34,38 @@ type Rbac struct{}
// ColorerFunc colors a resource row.
func (Rbac) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
return func(_ string, _ Header, _re RowEvent) tcell.Color {
return tcell.ColorMediumSpringGreen
}
}
// Header returns a header row.
func (Rbac) Header(ns string) HeaderRow {
h := HeaderRow{
Header{Name: "NAME"},
Header{Name: "API GROUP"},
}
func (Rbac) Header(ns string) Header {
h := make(Header, 0, 10)
h = append(h,
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "APIGROUP"},
)
h = append(h, rbacVerbHeader()...)
h = append(h, Header{Name: "VALID", Wide: true})
return h
return append(h, HeaderColumn{Name: "VALID", Wide: true})
}
// Render renders a K8s resource to screen.
func (Rbac) Render(o interface{}, gvr string, r *Row) error {
func (r Rbac) Render(o interface{}, ns string, ro *Row) error {
p, ok := o.(PolicyRes)
if !ok {
return fmt.Errorf("expecting RuleRes but got %T", o)
}
r.ID = p.Resource
r.Fields = append(r.Fields,
ro.ID = p.Resource
ro.Fields = make(Fields, 0, len(r.Header(ns)))
ro.Fields = append(ro.Fields,
cleanseResource(p.Resource),
p.Group,
)
r.Fields = append(r.Fields, asVerbs(p.Verbs)...)
r.Fields = append(r.Fields, "")
ro.Fields = append(ro.Fields, asVerbs(p.Verbs)...)
ro.Fields = append(ro.Fields, "")
return nil
}

View File

@ -18,17 +18,17 @@ func (Role) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (Role) Header(ns string) HeaderRow {
var h HeaderRow
func (Role) Header(ns string) Header {
var h Header
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
h = append(h, HeaderColumn{Name: "NAMESPACE"})
}
return append(h,
Header{Name: "NAME"},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
)
}

View File

@ -19,20 +19,20 @@ func (RoleBinding) ColorerFunc() ColorerFunc {
}
// Header returns a header rbw.
func (RoleBinding) Header(ns string) HeaderRow {
var h HeaderRow
func (RoleBinding) Header(ns string) Header {
var h Header
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
h = append(h, HeaderColumn{Name: "NAMESPACE"})
}
return append(h,
Header{Name: "NAME"},
Header{Name: "ROLE"},
Header{Name: "KIND"},
Header{Name: "SUBJECTS"},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "ROLE"},
HeaderColumn{Name: "KIND"},
HeaderColumn{Name: "SUBJECTS"},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
)
}

View File

@ -1,6 +1,7 @@
package render
import (
"reflect"
"sort"
"time"
@ -10,6 +11,30 @@ import (
// Fields represents a collection of row fields.
type Fields []string
// Customize returns a subset of fields.
func (f Fields) Customize(cols []int, out Fields) {
for i, c := range cols {
if c < 0 {
out[i] = "<TOAST!>"
continue
}
if c < len(f) {
out[i] = f[c]
}
}
}
// Diff returns true if fields differ or false otherwise.
func (f Fields) Diff(ff Fields, ageCol int) bool {
if ageCol < 0 {
return !reflect.DeepEqual(f[:len(f)-1], ff[:len(ff)-1])
}
if !reflect.DeepEqual(f[:ageCol], ff[:ageCol]) {
return true
}
return !reflect.DeepEqual(f[ageCol+1:], ff[ageCol+1:])
}
// Clone returns a copy of the fields.
func (f Fields) Clone() Fields {
cp := make(Fields, len(f))
@ -27,8 +52,25 @@ type Row struct {
}
// NewRow returns a new row with initialized fields.
func NewRow(cols int) Row {
return Row{Fields: make([]string, cols)}
func NewRow(size int) Row {
return Row{Fields: make([]string, size)}
}
// Customize returns a row subset based on given col indices.
func (r Row) Customize(cols []int) Row {
out := NewRow(len(cols))
r.Fields.Customize(cols, out.Fields)
out.ID = r.ID
return out
}
// Diff returns true if row differ or false otherwise.
func (r Row) Diff(ro Row, ageCol int) bool {
if r.ID != ro.ID {
return true
}
return r.Fields.Diff(ro.Fields, ageCol)
}
// Clone copies a row.

View File

@ -2,7 +2,6 @@ package render
import (
"fmt"
"reflect"
"sort"
"time"
@ -63,17 +62,30 @@ func (r RowEvent) Clone() RowEvent {
}
}
// Customize returns a new subset based on the given column indices.
func (r RowEvent) Customize(cols []int) RowEvent {
delta := r.Deltas
if !r.Deltas.IsBlank() {
delta = make(DeltaRow, len(cols))
r.Deltas.Customize(cols, delta)
}
return RowEvent{
Kind: r.Kind,
Deltas: delta,
Row: r.Row.Customize(cols),
}
}
// Diff returns true if the row changed.
func (r RowEvent) Diff(re RowEvent) bool {
func (r RowEvent) Diff(re RowEvent, ageCol int) bool {
if r.Kind != re.Kind {
return true
}
if !reflect.DeepEqual(r.Deltas, re.Deltas) {
if r.Deltas.Diff(re.Deltas, ageCol) {
return true
}
// BOZO!! Canned?? Skip age colum
return !reflect.DeepEqual(r.Row.Fields[:len(r.Row.Fields)-1], re.Row.Fields[:len(re.Row.Fields)-1])
return r.Row.Diff(re.Row, ageCol)
}
// ----------------------------------------------------------------------------
@ -81,14 +93,22 @@ func (r RowEvent) Diff(re RowEvent) bool {
// RowEvents a collection of row events.
type RowEvents []RowEvent
func (r RowEvents) Customize(cols []int) RowEvents {
ee := make(RowEvents, 0, len(cols))
for _, re := range r {
ee = append(ee, re.Customize(cols))
}
return ee
}
// Diff returns true if the event changed.
func (rr RowEvents) Diff(r RowEvents) bool {
if len(rr) != len(r) {
func (r RowEvents) Diff(re RowEvents, ageCol int) bool {
if len(r) != len(re) {
return true
}
for i := range rr {
if rr[i].Diff(r[i]) {
for i := range r {
if r[i].Diff(re[i], ageCol) {
return true
}
}
@ -97,43 +117,43 @@ func (rr RowEvents) Diff(r RowEvents) bool {
}
// Clone returns a rowevents deep copy.
func (rr RowEvents) Clone() RowEvents {
res := make(RowEvents, len(rr))
for i, r := range rr {
res[i] = r.Clone()
func (r RowEvents) Clone() RowEvents {
res := make(RowEvents, len(r))
for i, re := range r {
res[i] = re.Clone()
}
return res
}
// Upsert add or update a row if it exists.
func (rr RowEvents) Upsert(e RowEvent) RowEvents {
if idx, ok := rr.FindIndex(e.Row.ID); ok {
rr[idx] = e
func (r RowEvents) Upsert(re RowEvent) RowEvents {
if idx, ok := r.FindIndex(re.Row.ID); ok {
r[idx] = re
} else {
rr = append(rr, e)
r = append(r, re)
}
return rr
return r
}
// Delete removes an element by id.
func (rr RowEvents) Delete(id string) RowEvents {
victim, ok := rr.FindIndex(id)
func (r RowEvents) Delete(id string) RowEvents {
victim, ok := r.FindIndex(id)
if !ok {
return rr
return r
}
return append(rr[0:victim], rr[victim+1:]...)
return append(r[0:victim], r[victim+1:]...)
}
// Clear delete all row events
func (rr RowEvents) Clear() RowEvents {
func (r RowEvents) Clear() RowEvents {
return RowEvents{}
}
// FindIndex locates a row index by id. Returns false is not found.
func (rr RowEvents) FindIndex(id string) (int, bool) {
for i, e := range rr {
if e.Row.ID == id {
func (r RowEvents) FindIndex(id string) (int, bool) {
for i, re := range r {
if re.Row.ID == id {
return i, true
}
}
@ -141,43 +161,36 @@ func (rr RowEvents) FindIndex(id string) (int, bool) {
return 0, false
}
func (rr RowEvents) isAgeCol(col int) bool {
var age bool
if len(rr) == 0 {
return age
}
return col == len(rr[0].Row.Fields)-1
}
// Sort rows based on column index and order.
func (rr RowEvents) Sort(ns string, col int, asc bool) {
t := RowEventSorter{NS: ns, Events: rr, Index: col, Asc: asc}
func (r RowEvents) Sort(ns string, sortCol int, ageCol bool, asc bool) {
t := RowEventSorter{NS: ns, Events: r, Index: sortCol, Asc: asc}
sort.Sort(t)
ageCol := rr.isAgeCol(col)
gg, kk := map[string][]string{}, make(StringSet, 0, len(rr))
for _, r := range rr {
g := r.Row.Fields[col]
gg, kk := map[string][]string{}, make(StringSet, 0, len(r))
for _, re := range r {
g := re.Row.Fields[sortCol]
if ageCol {
g = toAgeDuration(g)
}
kk = kk.Add(g)
if ss, ok := gg[g]; ok {
gg[g] = append(ss, r.Row.ID)
gg[g] = append(ss, re.Row.ID)
} else {
gg[g] = []string{r.Row.ID}
gg[g] = []string{re.Row.ID}
}
}
ids := make([]string, 0, len(rr))
ids := make([]string, 0, len(r))
for _, k := range kk {
sort.StringSlice(gg[k]).Sort()
ids = append(ids, gg[k]...)
}
s := IdSorter{Ids: ids, Events: rr}
s := IdSorter{Ids: ids, Events: r}
sort.Sort(s)
}
// Helpers...
func toAgeDuration(dur string) string {
d, err := time.ParseDuration(dur)
if err != nil {

View File

@ -8,6 +8,354 @@ import (
"github.com/stretchr/testify/assert"
)
func TestRowEventCustomize(t *testing.T) {
uu := map[string]struct {
re1, e render.RowEvent
cols []int
}{
"empty": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
e: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{}},
},
},
"full": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
e: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
cols: []int{0, 1, 2},
},
"deltas": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
Deltas: render.DeltaRow{"a", "b", "c"},
},
e: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
Deltas: render.DeltaRow{"a", "b", "c"},
},
cols: []int{0, 1, 2},
},
"deltas-skip": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
Deltas: render.DeltaRow{"a", "b", "c"},
},
e: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"3", "1"}},
Deltas: render.DeltaRow{"c", "a"},
},
cols: []int{2, 0},
},
"reverse": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
e: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"3", "2", "1"}},
},
cols: []int{2, 1, 0},
},
"skip": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
e: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"3", "1"}},
},
cols: []int{2, 0},
},
"miss": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
e: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"3", "", "1"}},
},
cols: []int{2, 10, 0},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.re1.Customize(u.cols))
})
}
}
func TestRowEventDiff(t *testing.T) {
uu := map[string]struct {
re1, re2 render.RowEvent
e bool
}{
"same": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
re2: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
},
"diff-kind": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
re2: render.RowEvent{
Kind: render.EventDelete,
Row: render.Row{ID: "B", Fields: render.Fields{"1", "2", "3"}},
},
e: true,
},
"diff-delta": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
Deltas: render.DeltaRow{"1", "2", "3"},
},
re2: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
Deltas: render.DeltaRow{"10", "2", "3"},
},
e: true,
},
"diff-id": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
re2: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "B", Fields: render.Fields{"1", "2", "3"}},
},
e: true,
},
"diff-field": {
re1: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
},
re2: render.RowEvent{
Kind: render.EventAdd,
Row: render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}},
},
e: true,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.re1.Diff(u.re2, -1))
})
}
}
func TestRowEventsDiff(t *testing.T) {
uu := map[string]struct {
re1, re2 render.RowEvents
ageCol int
e bool
}{
"same": {
re1: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
re2: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
ageCol: -1,
},
"diff-len": {
re1: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
re2: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
ageCol: -1,
e: true,
},
"diff-id": {
re1: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
re2: render.RowEvents{
{Row: render.Row{ID: "D", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
ageCol: -1,
e: true,
},
"diff-order": {
re1: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
re2: render.RowEvents{
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
ageCol: -1,
e: true,
},
"diff-withAge": {
re1: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
re2: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "13"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
ageCol: 1,
e: true,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.re1.Diff(u.re2, u.ageCol))
})
}
}
func TestRowEventsUpsert(t *testing.T) {
uu := map[string]struct {
ee, e render.RowEvents
re render.RowEvent
}{
"add": {
ee: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
re: render.RowEvent{
Row: render.Row{ID: "D", Fields: render.Fields{"f1", "f2", "f3"}},
},
e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
{Row: render.Row{ID: "D", Fields: render.Fields{"f1", "f2", "f3"}}},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.ee.Upsert(u.re))
})
}
}
func TestRowEventsCustomize(t *testing.T) {
uu := map[string]struct {
re, e render.RowEvents
cols []int
}{
"same": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
cols: []int{0, 1, 2},
e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
"reverse": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
cols: []int{2, 1, 0},
e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"3", "2", "1"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"3", "2", "0"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"3", "2", "10"}}},
},
},
"skip": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
cols: []int{1, 0},
e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"2", "1"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"2", "0"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"2", "10"}}},
},
},
"missing": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
cols: []int{1, 0, 4},
e: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"2", "1", ""}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"2", "0", ""}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"2", "10", ""}}},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.re.Customize(u.cols))
})
}
}
func TestRowEventsDelete(t *testing.T) {
uu := map[string]struct {
re render.RowEvents
@ -60,7 +408,7 @@ func TestRowEventsDelete(t *testing.T) {
}
}
func TestSort(t *testing.T) {
func TestRowEventsSort(t *testing.T) {
uu := map[string]struct {
re render.RowEvents
col int
@ -106,12 +454,37 @@ func TestSort(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
u.re.Sort("", u.col, u.asc)
u.re.Sort("", u.col, false, u.asc)
assert.Equal(t, u.e, u.re)
})
}
}
func TestRowEventsClone(t *testing.T) {
uu := map[string]struct {
r render.RowEvents
}{
"empty": {
r: render.RowEvents{},
},
"full": {
r: makeRowEvents(),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c := u.r.Clone()
assert.Equal(t, len(u.r), len(c))
if len(u.r) > 0 {
u.r[0].Row.Fields[0] = "blee"
assert.Equal(t, "A", c[0].Row.Fields[0])
}
})
}
}
func TestDefaultColorer(t *testing.T) {
uu := map[string]struct {
k render.ResEvent
@ -123,10 +496,29 @@ func TestDefaultColorer(t *testing.T) {
"std": {100, render.StdColor},
}
h := render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, render.DefaultColorer("", render.RowEvent{}))
assert.Equal(t, u.e, render.DefaultColorer("", h, render.RowEvent{}))
})
}
}
// Helpers...
func makeRowEvents() render.RowEvents {
return render.RowEvents{
{Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}},
{Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}},
{Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}},
{Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}},
{Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}},
{Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}},
}
}

View File

@ -1,85 +0,0 @@
package render
import "reflect"
const ageCol = "AGE"
// Header represent a table header
type Header struct {
Name string
Align int
Decorator DecoratorFunc
Hide bool
Wide bool
}
// Clone copies a header.
func (h Header) Clone() Header {
return h
}
// ----------------------------------------------------------------------------
// HeaderRow represents a table header.
type HeaderRow []Header
// Clone duplicates a 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{}
}
// Diff returns true if the header changed.
func (hh HeaderRow) Diff(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 (hh HeaderRow) Columns() []string {
cc := make([]string, len(hh))
for i, c := range hh {
cc[i] = c.Name
}
return cc
}
// HasAge returns true if table has an age column.
func (hh HeaderRow) HasAge() bool {
return hh.IndexOf(ageCol) != -1
}
// AgeCol checks if given column index is the age column.
func (hh HeaderRow) AgeCol(col int) bool {
if !hh.HasAge() {
return false
}
return col == len(hh)-1
}
// ValidColIndex returns the valid col index or -1 if none.
func (hh HeaderRow) ValidColIndex() int {
return hh.IndexOf("VALID")
}
// IndexOf returns the col index or -1 if none.
func (hh HeaderRow) IndexOf(c string) int {
for i, h := range hh {
if h.Name == c {
return i
}
}
return -1
}

View File

@ -9,6 +9,54 @@ import (
"github.com/stretchr/testify/assert"
)
func BenchmarkRowCustomize(b *testing.B) {
row := render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}}
cols := []int{0, 1, 2}
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
_ = row.Customize(cols)
}
}
func TestFieldCustomize(t *testing.T) {
uu := map[string]struct {
fields render.Fields
cols []int
e render.Fields
}{
"empty": {
fields: render.Fields{},
cols: []int{0, 1, 2},
e: render.Fields{"", "", ""},
},
"no-cols": {
fields: render.Fields{"f1", "f2", "f3"},
cols: []int{},
e: render.Fields{},
},
"reverse": {
fields: render.Fields{"f1", "f2", "f3"},
cols: []int{1, 0},
e: render.Fields{"f2", "f1"},
},
"missing": {
fields: render.Fields{"f1", "f2", "f3"},
cols: []int{10, 0},
e: render.Fields{"", "f1"},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
ff := make(render.Fields, len(u.cols))
u.fields.Customize(u.cols, ff)
assert.Equal(t, u.e, ff)
})
}
}
func TestFieldClone(t *testing.T) {
f := render.Fields{"a", "b", "c"}
f1 := f.Clone()
@ -17,6 +65,38 @@ func TestFieldClone(t *testing.T) {
assert.NotEqual(t, fmt.Sprintf("%p", f), fmt.Sprintf("%p", f1))
}
func TestRowCustomize(t *testing.T) {
uu := map[string]struct {
row render.Row
cols []int
e render.Row
}{
"empty": {
row: render.Row{},
cols: []int{0, 1, 2},
e: render.Row{ID: "", Fields: render.Fields{"", "", ""}},
},
"no-cols-no-data": {
row: render.Row{},
cols: []int{},
e: render.Row{ID: "", Fields: render.Fields{}},
},
"no-cols-data": {
row: render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}},
cols: []int{},
e: render.Row{ID: "fred", Fields: render.Fields{}},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
row := u.row.Customize(u.cols)
assert.Equal(t, u.e, row)
})
}
}
func TestRowsDelete(t *testing.T) {
uu := map[string]struct {
rows render.Rows
@ -69,10 +149,50 @@ func TestRowsDelete(t *testing.T) {
}
for k := range uu {
uc := uu[k]
u := uu[k]
t.Run(k, func(t *testing.T) {
rows := uc.rows.Delete(uc.id)
assert.Equal(t, uc.e, rows)
rows := u.rows.Delete(u.id)
assert.Equal(t, u.e, rows)
})
}
}
func TestRowsUpsert(t *testing.T) {
uu := map[string]struct {
rows render.Rows
row render.Row
e render.Rows
}{
"add": {
rows: render.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
},
row: render.Row{ID: "c", Fields: []string{"f1", "f2"}},
e: render.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
{ID: "c", Fields: []string{"f1", "f2"}},
},
},
"update": {
rows: render.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
},
row: render.Row{ID: "a", Fields: []string{"f1", "f2"}},
e: render.Rows{
{ID: "a", Fields: []string{"f1", "f2"}},
{ID: "b", Fields: []string{"albert", "blee"}},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
rows := u.rows.Upsert(u.row)
assert.Equal(t, u.e, rows)
})
}
}
@ -147,10 +267,10 @@ func TestRowsSortText(t *testing.T) {
}
for k := range uu {
uc := uu[k]
u := uu[k]
t.Run(k, func(t *testing.T) {
uc.rows.Sort(uc.col, uc.asc)
assert.Equal(t, uc.e, uc.rows)
u.rows.Sort(u.col, u.asc)
assert.Equal(t, u.e, u.rows)
})
}
}
@ -188,10 +308,10 @@ func TestRowsSortDuration(t *testing.T) {
}
for k := range uu {
uc := uu[k]
u := uu[k]
t.Run(k, func(t *testing.T) {
uc.rows.Sort(uc.col, uc.asc)
assert.Equal(t, uc.e, uc.rows)
u.rows.Sort(u.col, u.asc)
assert.Equal(t, u.e, u.rows)
})
}
}
@ -230,10 +350,10 @@ func TestRowsSortMetrics(t *testing.T) {
}
for k := range uu {
uc := uu[k]
u := uu[k]
t.Run(k, func(t *testing.T) {
uc.rows.Sort(uc.col, uc.asc)
assert.Equal(t, uc.e, uc.rows)
u.rows.Sort(u.col, u.asc)
assert.Equal(t, u.e, u.rows)
})
}
}

View File

@ -6,7 +6,6 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -17,36 +16,21 @@ type ReplicaSet struct{}
// ColorerFunc colors a resource row.
func (r ReplicaSet) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
c := DefaultColorer(ns, re)
if re.Kind == EventAdd || re.Kind == EventUpdate {
return c
}
if !Happy(ns, re.Row) {
return ErrColor
}
return StdColor
}
return DefaultColorer
}
// Header returns a header row.
func (ReplicaSet) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (ReplicaSet) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
HeaderColumn{Name: "READY", Align: tview.AlignRight},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "DESIRED", Align: tview.AlignRight},
Header{Name: "CURRENT", Align: tview.AlignRight},
Header{Name: "READY", Align: tview.AlignRight},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -62,11 +46,8 @@ func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error {
}
row.ID = client.MetaFQN(rs.ObjectMeta)
row.Fields = make(Fields, 0, len(r.Header(ns)))
if client.IsAllNamespaces(ns) {
row.Fields = append(row.Fields, rs.Namespace)
}
row.Fields = append(row.Fields,
row.Fields = Fields{
rs.Namespace,
rs.Name,
strconv.Itoa(int(*rs.Spec.Replicas)),
strconv.Itoa(int(rs.Status.Replicas)),
@ -74,7 +55,7 @@ func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error {
mapToStr(rs.Labels),
asStatus(r.diagnose(rs)),
toAge(rs.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -19,19 +19,15 @@ func (ServiceAccount) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (ServiceAccount) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (ServiceAccount) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "SECRET"},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "SECRET"},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -47,17 +43,14 @@ func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(sa.ObjectMeta)
r.Fields = make(Fields, 0, len(s.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, sa.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
sa.Namespace,
sa.Name,
strconv.Itoa(len(sa.Secrets)),
mapToStr(sa.Labels),
"",
toAge(sa.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -18,13 +18,13 @@ func (StorageClass) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (StorageClass) Header(ns string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "PROVISIONER"},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (StorageClass) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "PROVISIONER"},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}

View File

@ -16,7 +16,7 @@ type ScreenDump struct{}
// ColorerFunc colors a resource row.
func (ScreenDump) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
return func(ns string, _ Header, re RowEvent) tcell.Color {
return tcell.ColorNavajoWhite
}
}
@ -30,12 +30,12 @@ var AgeDecorator = func(a string) string {
}
// Header returns a header row.
func (ScreenDump) Header(ns string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "DIR"},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
func (ScreenDump) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "DIR"},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
}

View File

@ -5,7 +5,6 @@ import (
"strconv"
"github.com/derailed/k9s/internal/client"
"github.com/gdamore/tcell"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -16,37 +15,23 @@ type StatefulSet struct{}
// ColorerFunc colors a resource row.
func (s StatefulSet) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
c := DefaultColorer(ns, re)
if re.Kind == EventAdd || re.Kind == EventUpdate {
return c
}
if !Happy(ns, re.Row) {
return ErrColor
}
return StdColor
}
return DefaultColorer
}
// Header returns a header row.
func (StatefulSet) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (StatefulSet) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "READY"},
HeaderColumn{Name: "SELECTOR", Wide: true},
HeaderColumn{Name: "SERVICE"},
HeaderColumn{Name: "CONTAINERS", Wide: true},
HeaderColumn{Name: "IMAGES", Wide: true},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "READY"},
Header{Name: "SELECTOR", Wide: true},
Header{Name: "SERVICE"},
Header{Name: "CONTAINERS", Wide: true},
Header{Name: "IMAGES", Wide: true},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -62,13 +47,10 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(sts.ObjectMeta)
r.Fields = make(Fields, 0, len(s.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, sts.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
sts.Namespace,
sts.Name,
strconv.Itoa(int(sts.Status.ReadyReplicas))+"/"+strconv.Itoa(int(sts.Status.Replicas)),
strconv.Itoa(int(sts.Status.ReadyReplicas)) + "/" + strconv.Itoa(int(sts.Status.Replicas)),
asSelector(sts.Spec.Selector),
na(sts.Spec.ServiceName),
podContainerNames(sts.Spec.Template.Spec, true),
@ -76,7 +58,7 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
mapToStr(sts.Labels),
asStatus(s.diagnose(sts.Status.Replicas, sts.Status.ReadyReplicas)),
toAge(sts.ObjectMeta.CreationTimestamp),
)
}
return nil
}

View File

@ -18,18 +18,18 @@ func (Subject) Happy(_ string, _ Row) bool {
// ColorerFunc colors a resource row.
func (Subject) ColorerFunc() ColorerFunc {
return func(ns string, re RowEvent) tcell.Color {
return func(ns string, _ Header, re RowEvent) tcell.Color {
return tcell.ColorMediumSpringGreen
}
}
// Header returns a header row.
func (Subject) Header(ns string) HeaderRow {
return HeaderRow{
Header{Name: "NAME"},
Header{Name: "KIND"},
Header{Name: "FIRST LOCATION"},
Header{Name: "VALID", Wide: true},
func (Subject) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "KIND"},
HeaderColumn{Name: "FIRST LOCATION"},
HeaderColumn{Name: "VALID", Wide: true},
}
}
@ -41,13 +41,12 @@ func (s Subject) Render(o interface{}, ns string, r *Row) error {
}
r.ID = res.Name
r.Fields = make(Fields, 0, len(s.Header(ns)))
r.Fields = append(r.Fields,
r.Fields = Fields{
res.Name,
res.Kind,
res.FirstLocation,
"",
)
}
return nil
}

View File

@ -21,23 +21,19 @@ func (Service) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
func (Service) Header(ns string) HeaderRow {
var h HeaderRow
if client.IsAllNamespaces(ns) {
h = append(h, Header{Name: "NAMESPACE"})
func (Service) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAMESPACE"},
HeaderColumn{Name: "NAME"},
HeaderColumn{Name: "TYPE"},
HeaderColumn{Name: "CLUSTER-IP"},
HeaderColumn{Name: "EXTERNAL-IP"},
HeaderColumn{Name: "SELECTOR", Wide: true},
HeaderColumn{Name: "PORTS", Wide: true},
HeaderColumn{Name: "LABELS", Wide: true},
HeaderColumn{Name: "VALID", Wide: true},
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
}
return append(h,
Header{Name: "NAME"},
Header{Name: "TYPE"},
Header{Name: "CLUSTER-IP"},
Header{Name: "EXTERNAL-IP"},
Header{Name: "SELECTOR", Wide: true},
Header{Name: "PORTS", Wide: true},
Header{Name: "LABELS", Wide: true},
Header{Name: "VALID", Wide: true},
Header{Name: "AGE", Decorator: AgeDecorator},
)
}
// Render renders a K8s resource to screen.
@ -53,21 +49,18 @@ func (s Service) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(svc.ObjectMeta)
r.Fields = make(Fields, 0, len(s.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, svc.Namespace)
}
r.Fields = append(r.Fields,
r.Fields = Fields{
svc.Namespace,
svc.ObjectMeta.Name,
string(svc.Spec.Type),
toIP(svc.Spec.ClusterIP),
toIPs(svc.Spec.Type, getSvcExtIPS(&svc)),
mapToStr(svc.Spec.Selector),
toPorts(svc.Spec.Ports),
ToPorts(svc.Spec.Ports),
mapToStr(svc.Labels),
asStatus(s.diagnose()),
toAge(svc.ObjectMeta.CreationTimestamp),
)
}
return nil
}
@ -138,7 +131,7 @@ func toIPs(svcType v1.ServiceType, ips []string) string {
return strings.Join(ips, ",")
}
func toPorts(pp []v1.ServicePort) string {
func ToPorts(pp []v1.ServicePort) string {
ports := make([]string, len(pp))
for i, p := range pp {
if len(p.Name) > 0 {

View File

@ -1,8 +1,10 @@
package render
import "github.com/rs/zerolog/log"
// TableData tracks a K8s resource for tabular display.
type TableData struct {
Header HeaderRow
Header Header
RowEvents RowEvents
Namespace string
}
@ -12,9 +14,22 @@ func NewTableData() *TableData {
return &TableData{}
}
func (t *TableData) Customize(cols []string, wide bool) TableData {
res := TableData{
Namespace: t.Namespace,
Header: t.Header.Customize(cols, wide),
}
ids := make([]int, len(t.Header))
t.Header.MapIndices(cols, wide, ids)
log.Debug().Msgf("INDICES %#v", ids)
res.RowEvents = t.RowEvents.Customize(ids)
return res
}
// Clear clears out the entire table.
func (t *TableData) Clear() {
t.Header, t.RowEvents = t.Header.Clear(), t.RowEvents.Clear()
t.Header, t.RowEvents = Header{}, RowEvents{}
}
// Clone returns a copy of the table
@ -27,7 +42,7 @@ func (t *TableData) Clone() TableData {
}
// SetHeader sets table header.
func (t *TableData) SetHeader(ns string, h HeaderRow) {
func (t *TableData) SetHeader(ns string, h Header) {
t.Namespace, t.Header = ns, h
}
@ -85,5 +100,5 @@ func (t *TableData) Diff(table TableData) bool {
return true
}
return t.RowEvents.Diff(table.RowEvents)
return t.RowEvents.Diff(table.RowEvents, t.Header.IndexOf("AGE", true))
}

View File

@ -7,6 +7,349 @@ import (
"github.com/stretchr/testify/assert"
)
func TestTableDataCustomize(t *testing.T) {
uu := map[string]struct {
t1 render.TableData
cols []string
wide bool
e render.TableData
}{
"same": {
t1: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
cols: []string{"A", "B", "C"},
e: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
},
"wide-col": {
t1: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
cols: []string{"A", "B", "C"},
e: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: false},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
},
"wide": {
t1: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B", Wide: true},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
wide: true,
cols: []string{"A", "C"},
e: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "C"},
render.HeaderColumn{Name: "B", Wide: true},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "3", "2"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "3", "2"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "3", "2"}}},
},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.t1.Customize(u.cols, u.wide))
})
}
}
func TestTableDataDiff(t *testing.T) {
uu := map[string]struct {
t1 render.TableData
t2 render.TableData
e bool
}{
"empty": {
t1: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
e: true,
},
"same": {
t1: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
t2: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
},
"ns-diff": {
t1: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
t2: render.TableData{
Namespace: "blee",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
e: true,
},
"header-diff": {
t1: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "D"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
t2: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
e: true,
},
"row-diff": {
t1: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
t2: render.TableData{
Namespace: "fred",
Header: render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
},
RowEvents: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"100", "2", "3"}}},
},
},
e: true,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.t1.Diff(u.t2))
})
}
}
func TestTableDataUpdate(t *testing.T) {
uu := map[string]struct {
re render.RowEvents
rr render.Rows
e render.RowEvents
}{
"no-change": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
rr: render.Rows{
render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}},
render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
},
e: render.RowEvents{
{Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
"add": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
rr: render.Rows{
render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}},
render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}},
},
e: render.RowEvents{
{Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
{Kind: render.EventAdd, Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}},
},
},
"delete": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
rr: render.Rows{
render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
},
e: render.RowEvents{
{Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
"update": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
rr: render.Rows{
render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}},
render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}},
render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
},
e: render.RowEvents{
{
Kind: render.EventUpdate,
Row: render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}},
Deltas: render.DeltaRow{"1", "", ""},
},
{Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
{Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
},
},
}
var table render.TableData
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
table.RowEvents = u.re
table.Update(u.rr)
assert.Equal(t, u.e, table.RowEvents)
})
}
}
func TestTableDataDelete(t *testing.T) {
uu := map[string]struct {
re render.RowEvents

View File

@ -20,10 +20,11 @@ type synchronizer interface {
// Configurator represents an application configurationa.
type Configurator struct {
skinFile string
Config *config.Config
Styles *config.Styles
BenchFile string
Config *config.Config
Styles *config.Styles
CustomView *config.CustomView
BenchFile string
skinFile string
}
// HasSkin returns true if a skin file was located.
@ -31,8 +32,55 @@ func (c *Configurator) HasSkin() bool {
return c.skinFile != ""
}
// StylesUpdater watches for skin file changes.
func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error {
// CustomViewsWatcher watches for view config file changes.
func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) error {
w, err := fsnotify.NewWatcher()
if err != nil {
return err
}
go func() {
for {
select {
case evt := <-w.Events:
_ = evt
s.QueueUpdateDraw(func() {
c.RefreshCustomViews()
})
case err := <-w.Errors:
log.Info().Err(err).Msg("CustomView watcher failed")
return
case <-ctx.Done():
log.Debug().Msgf("CustomViewWatcher Done `%s!!", config.K9sViewConfigFile)
if err := w.Close(); err != nil {
log.Error().Err(err).Msg("Closing CustomView watcher")
}
return
}
}
}()
log.Debug().Msgf("CustomView watching `%s", config.K9sViewConfigFile)
c.RefreshCustomViews()
return w.Add(config.K9sViewConfigFile)
}
// RefreshCustomView load view configuration changes.
func (c *Configurator) RefreshCustomViews() {
if c.CustomView == nil {
c.CustomView = config.NewCustomView()
} else {
c.CustomView.Reset()
}
if err := c.CustomView.Load(config.K9sViewConfigFile); err != nil {
log.Debug().Msgf("No view custom configuration file found -- %s", config.K9sViewConfigFile)
return
}
}
// StylesWatcher watches for skin file changes.
func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error {
if !c.HasSkin() {
return nil
}
@ -56,7 +104,7 @@ func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error
case <-ctx.Done():
log.Debug().Msgf("SkinWatcher Done `%s!!", c.skinFile)
if err := w.Close(); err != nil {
log.Error().Err(err).Msg("Closing watcher")
log.Error().Err(err).Msg("Closing Skin watcher")
}
return
}

View File

@ -13,12 +13,12 @@ import (
type MaxyPad []int
// ComputeMaxColumns figures out column max size and necessary padding.
func ComputeMaxColumns(pads MaxyPad, sortCol int, header render.HeaderRow, ee render.RowEvents) {
func ComputeMaxColumns(pads MaxyPad, sortColName string, header render.Header, ee render.RowEvents) {
const colPadding = 1
for index, h := range header {
pads[index] = len(h.Name)
if index == sortCol {
if h.Name == sortColName {
pads[index] = len(h.Name) + 2
}
}
@ -26,7 +26,7 @@ func ComputeMaxColumns(pads MaxyPad, sortCol int, header render.HeaderRow, ee re
var row int
for _, e := range ee {
for index, field := range e.Row.Fields {
if header.AgeCol(index) {
if header.IsAgeCol(index) {
field = toAgeHuman(field)
}
width := len(field) + colPadding
@ -53,11 +53,9 @@ func Pad(s string, width int) string {
if len(s) == width {
return s
}
if len(s) > width {
return render.Truncate(s, width)
}
return s + strings.Repeat(" ", width-len(s))
}

View File

@ -10,12 +10,12 @@ import (
func TestMaxColumn(t *testing.T) {
uu := map[string]struct {
t render.TableData
s int
s string
e MaxyPad
}{
"ascii col 0": {
render.TableData{
Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}},
Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
RowEvents: render.RowEvents{
render.RowEvent{
Row: render.Row{
@ -29,12 +29,12 @@ func TestMaxColumn(t *testing.T) {
},
},
},
0,
"A",
MaxyPad{6, 6},
},
"ascii col 1": {
render.TableData{
Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}},
Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
RowEvents: render.RowEvents{
render.RowEvent{
Row: render.Row{
@ -48,12 +48,12 @@ func TestMaxColumn(t *testing.T) {
},
},
},
1,
"B",
MaxyPad{6, 6},
},
"non_ascii": {
render.TableData{
Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}},
Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
RowEvents: render.RowEvents{
render.RowEvent{
Row: render.Row{
@ -67,15 +67,18 @@ func TestMaxColumn(t *testing.T) {
},
},
},
0,
"A",
MaxyPad{32, 6},
},
}
for _, u := range uu {
pads := make(MaxyPad, len(u.t.Header))
ComputeMaxColumns(pads, u.s, u.t.Header, u.t.RowEvents)
assert.Equal(t, u.e, pads)
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
pads := make(MaxyPad, len(u.t.Header))
ComputeMaxColumns(pads, u.s, u.t.Header, u.t.RowEvents)
assert.Equal(t, u.e, pads)
})
}
}
@ -114,7 +117,7 @@ func TestPad(t *testing.T) {
func BenchmarkMaxColumn(b *testing.B) {
table := render.TableData{
Header: render.HeaderRow{render.Header{Name: "A"}, render.Header{Name: "B"}},
Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
RowEvents: render.RowEvents{
render.RowEvent{
Row: render.Row{
@ -134,6 +137,6 @@ func BenchmarkMaxColumn(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
ComputeMaxColumns(pads, 0, table.Header, table.RowEvents)
ComputeMaxColumns(pads, "A", table.Header, table.RowEvents)
}
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
@ -30,20 +31,22 @@ type (
type Table struct {
*SelectTable
actions KeyActions
BaseTitle string
Path string
cmdBuff *CmdBuff
styles *config.Styles
sortCol SortColumn
colorerFn render.ColorerFunc
decorateFn DecorateFunc
wide bool
toast bool
actions KeyActions
gvr client.GVR
Path string
cmdBuff *CmdBuff
styles *config.Styles
viewSetting *config.ViewSetting
sortCol SortColumn
colorerFn render.ColorerFunc
decorateFn DecorateFunc
wide bool
toast bool
header render.Header
}
// NewTable returns a new table view.
func NewTable(gvr string) *Table {
func NewTable(gvr client.GVR) *Table {
return &Table{
SelectTable: &SelectTable{
Table: tview.NewTable(),
@ -51,10 +54,10 @@ func NewTable(gvr string) *Table {
selectedRow: 1,
marks: make(map[string]struct{}),
},
actions: make(KeyActions),
cmdBuff: NewCmdBuff('/', FilterBuff),
BaseTitle: gvr,
sortCol: SortColumn{index: -1, colCount: 0, asc: true},
gvr: gvr,
actions: make(KeyActions),
cmdBuff: NewCmdBuff('/', FilterBuff),
sortCol: SortColumn{asc: true},
}
}
@ -69,10 +72,22 @@ func (t *Table) Init(ctx context.Context) {
t.SetInputCapture(t.keyboard)
t.SetBackgroundColor(tcell.ColorDefault)
t.styles = mustExtractSyles(ctx)
if cfg, ok := ctx.Value(internal.KeyViewConfig).(*config.CustomView); ok && cfg != nil {
cfg.AddListener(t.GVR().String(), t)
}
t.styles = mustExtractStyles(ctx)
t.StylesChanged(t.styles)
}
// GVR returns a resource descriptor.
func (t *Table) GVR() client.GVR { return t.gvr }
// ViewSettingsChanged notifies listener the view configuration changed.
func (t *Table) ViewSettingsChanged(settings config.ViewSetting) {
t.viewSetting = &settings
t.Refresh()
}
// StylesChanged notifies the skin changed.
func (t *Table) StylesChanged(s *config.Styles) {
t.SetBackgroundColor(s.Table().BgColor.Color())
@ -178,12 +193,13 @@ func (t *Table) SetColorerFn(f render.ColorerFunc) {
}
// SetSortCol sets in sort column index and order.
func (t *Table) SetSortCol(index, count int, asc bool) {
t.sortCol.index, t.sortCol.colCount, t.sortCol.asc = index, count, asc
func (t *Table) SetSortCol(name string, asc bool) {
t.sortCol.name, t.sortCol.asc = name, asc
}
// Update table content.
func (t *Table) Update(data render.TableData) {
t.header = data.Header
if t.decorateFn != nil {
data = t.decorateFn(data)
}
@ -193,18 +209,36 @@ func (t *Table) Update(data render.TableData) {
func (t *Table) doUpdate(data render.TableData) {
if client.IsAllNamespaces(data.Namespace) {
t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd(-2, true), false)
t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false)
} else {
t.actions.Delete(KeyShiftP)
}
hasMX := t.model.HasMetrics()
var cols []string
if t.viewSetting != nil {
cols = t.viewSetting.Columns
}
if len(cols) == 0 {
cols = t.header.Columns(t.wide)
}
data = data.Customize(cols, t.wide)
if t.sortCol.name == "" || data.Header.IndexOf(t.sortCol.name, false) == -1 {
t.sortCol.name = data.Header[0].Name
}
t.Clear()
t.adjustSorter(data)
fg := t.styles.Table().Header.FgColor.Color()
bg := t.styles.Table().Header.BgColor.Color()
var col int
fmt.Printf("NS %q\n", t.GetModel().GetNamespace())
for _, h := range data.Header {
if h.Wide && !t.wide {
if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
continue
}
if h.MX && !hasMX {
continue
}
t.AddHeaderCell(col, h)
@ -213,35 +247,70 @@ func (t *Table) doUpdate(data render.TableData) {
c.SetTextColor(fg)
col++
}
data.RowEvents.Sort(data.Namespace, t.sortCol.index, t.sortCol.asc)
data.RowEvents.Sort(data.Namespace, data.Header.IndexOf(t.sortCol.name, false), t.sortCol.name == "AGE", t.sortCol.asc)
pads := make(MaxyPad, len(data.Header))
ComputeMaxColumns(pads, t.sortCol.index, data.Header, data.RowEvents)
for i, r := range data.RowEvents {
t.buildRow(data.Namespace, i+1, r, data.Header, pads)
ComputeMaxColumns(pads, t.sortCol.name, data.Header, data.RowEvents)
for row, re := range data.RowEvents {
t.buildRow(row+1, re, data.Header, pads, hasMX)
}
t.updateSelection(true)
}
// SortColCmd designates a sorted column.
func (t *Table) SortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
var index int
switch col {
case -2:
index = 0
case -1:
index = t.GetColumnCount() - 1
case -3:
index = t.GetColumnCount() - 2
default:
index = t.NameColIndex() + col
func (t *Table) buildRow(r int, re render.RowEvent, h render.Header, pads MaxyPad, hasMX bool) {
color := render.DefaultColorer
if t.colorerFn != nil {
color = t.colorerFn
}
marked := t.IsMarked(re.Row.ID)
var col int
for c, field := range re.Row.Fields {
if c >= len(h) {
log.Error().Msgf("field/header overflow detected for %d::%d. Check your mappings!", c, len(h))
continue
}
if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
continue
}
if h[c].MX && !hasMX {
continue
}
if !re.Deltas.IsBlank() && !h.IsAgeCol(c) {
field += Deltas(re.Deltas[c], field)
}
if h[c].Decorator != nil {
field = h[c].Decorator(field)
}
if h[c].Align == tview.AlignLeft {
field = formatCell(field, pads[c])
}
cell := tview.NewTableCell(field)
cell.SetExpansion(1)
cell.SetAlign(h[c].Align)
cell.SetTextColor(color(t.GetModel().GetNamespace(), h, re))
if marked {
cell.SetTextColor(t.styles.Table().MarkColor.Color())
}
if col == 0 {
cell.SetReference(re.Row.ID)
}
t.SetCell(r, col, cell)
col++
}
}
// SortColCmd designates a sorted column.
func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
t.sortCol.asc = !t.sortCol.asc
if t.sortCol.index != index {
if t.sortCol.name != name {
t.sortCol.asc = asc
}
t.sortCol.index = index
t.sortCol.name = name
t.Refresh()
return nil
}
@ -255,57 +324,6 @@ func (t *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
func (t *Table) adjustSorter(data render.TableData) {
// Going from namespace to non namespace or vice-versa?
switch {
case t.sortCol.colCount == 0:
case len(data.Header) > t.sortCol.colCount:
t.sortCol.index++
case len(data.Header) < t.sortCol.colCount:
t.sortCol.index--
}
t.sortCol.colCount = len(data.Header)
if t.sortCol.index < 0 {
t.sortCol.index = 0
}
}
func (t *Table) buildRow(ns string, r int, re render.RowEvent, header render.HeaderRow, pads MaxyPad) {
color := render.DefaultColorer
if t.colorerFn != nil {
color = t.colorerFn
}
marked := t.IsMarked(re.Row.ID)
var col int
for c, field := range re.Row.Fields {
if header[c].Wide && !t.wide {
continue
}
if !re.Deltas.IsBlank() && !header.AgeCol(c) {
field += Deltas(re.Deltas[c], field)
}
if header[c].Decorator != nil {
field = header[c].Decorator(field)
}
if header[c].Align == tview.AlignLeft {
field = formatCell(field, pads[c])
}
cell := tview.NewTableCell(field)
cell.SetExpansion(1)
cell.SetAlign(header[c].Align)
cell.SetTextColor(color(ns, re))
if marked {
cell.SetTextColor(t.styles.Table().MarkColor.Color())
}
if col == 0 {
cell.SetReference(re.Row.ID)
}
t.SetCell(r, col, cell)
col++
}
}
// ClearMarks clear out marked items.
func (t *Table) ClearMarks() {
t.SelectTable.ClearMarks()
@ -314,13 +332,16 @@ func (t *Table) ClearMarks() {
// Refresh update the table data.
func (t *Table) Refresh() {
data := t.model.Peek()
if len(data.Header) == 0 {
return
}
// BOZO!! Really want to tell model reload now. Refactor!
t.Update(t.model.Peek())
t.Update(data)
}
// GetSelectedRow returns the entire selected row.
func (t *Table) GetSelectedRow() render.Row {
log.Debug().Msgf("INDEX %d", t.GetSelectedRowIndex())
return t.model.Peek().RowEvents[t.GetSelectedRowIndex()-1].Row
}
@ -337,8 +358,9 @@ func (t *Table) NameColIndex() int {
}
// AddHeaderCell configures a table cell header.
func (t *Table) AddHeaderCell(col int, h render.Header) {
c := tview.NewTableCell(sortIndicator(t.sortCol, t.styles.Table(), col, h.Name))
func (t *Table) AddHeaderCell(col int, h render.HeaderColumn) {
sortCol := h.Name == t.sortCol.name
c := tview.NewTableCell(sortIndicator(sortCol, t.sortCol.asc, t.styles.Table(), h.Name))
c.SetExpansion(1)
c.SetAlign(h.Align)
t.SetCell(0, col, c)
@ -392,9 +414,9 @@ func (t *Table) styleTitle() string {
rc--
}
base := strings.Title(t.BaseTitle)
base := strings.Title(t.gvr.R())
ns := t.GetModel().GetNamespace()
if client.IsAllNamespaces(ns) {
if client.IsClusterWide(ns) || ns == client.NotNamespaced {
ns = client.NamespaceAll
}
path := t.Path

View File

@ -43,7 +43,7 @@ var (
fuzzyRx = regexp.MustCompile(`\A\-f`)
)
func mustExtractSyles(ctx context.Context) *config.Styles {
func mustExtractStyles(ctx context.Context) *config.Styles {
styles, ok := ctx.Value(internal.KeyStyles).(*config.Styles)
if !ok {
log.Fatal().Msg("Expecting valid styles")
@ -98,13 +98,13 @@ func SkinTitle(fmat string, style config.Frame) string {
return fmat
}
func sortIndicator(col SortColumn, style config.Table, index int, name string) string {
if col.index != index {
func sortIndicator(sort, asc bool, style config.Table, name string) string {
if !sort {
return name
}
order := descIndicator
if col.asc {
if asc {
order = ascIndicator
}
return fmt.Sprintf("%s[%s::b]%s[::]", name, style.Header.SorterColor, order)
@ -119,7 +119,7 @@ func formatCell(field string, padding int) string {
}
func filterToast(data render.TableData) render.TableData {
validX := data.Header.IndexOf("VALID")
validX := data.Header.IndexOf("VALID", true)
if validX == -1 {
return data
}

View File

@ -6,6 +6,7 @@ import (
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
@ -15,28 +16,26 @@ import (
)
func TestTableNew(t *testing.T) {
v := ui.NewTable("fred")
ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles())
v.Init(ctx)
v := ui.NewTable(client.NewGVR("fred"))
v.Init(makeContext())
assert.Equal(t, "fred", v.BaseTitle)
assert.Equal(t, "fred", v.GVR().String())
}
func TestTableUpdate(t *testing.T) {
v := ui.NewTable("fred")
ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles())
v.Init(ctx)
v := ui.NewTable(client.NewGVR("fred"))
v.Init(makeContext())
v.Update(makeTableData())
data := makeTableData()
v.Update(data)
assert.Equal(t, 3, v.GetRowCount())
assert.Equal(t, 3, v.GetColumnCount())
assert.Equal(t, len(data.RowEvents)+1, v.GetRowCount())
assert.Equal(t, len(data.Header), v.GetColumnCount())
}
func TestTableSelection(t *testing.T) {
v := ui.NewTable("fred")
ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles())
v.Init(ctx)
v := ui.NewTable(client.NewGVR("fred"))
v.Init(makeContext())
m := &testModel{}
v.SetModel(m)
v.Update(m.Peek())
@ -62,6 +61,7 @@ var _ ui.Tabular = &testModel{}
func (t *testModel) SetInstance(string) {}
func (t *testModel) Empty() bool { return false }
func (t *testModel) HasMetrics() bool { return true }
func (t *testModel) Peek() render.TableData { return makeTableData() }
func (t *testModel) ClusterWide() bool { return false }
func (t *testModel) GetNamespace() string { return "blee" }
@ -87,10 +87,10 @@ func (t *testModel) SetRefreshRate(time.Duration) {}
func makeTableData() render.TableData {
t := render.NewTableData()
t.Namespace = ""
t.Header = render.HeaderRow{
render.Header{Name: "a"},
render.Header{Name: "b"},
render.Header{Name: "c"},
t.Header = render.Header{
render.HeaderColumn{Name: "A"},
render.HeaderColumn{Name: "B"},
render.HeaderColumn{Name: "C"},
}
t.RowEvents = render.RowEvents{
render.RowEvent{
@ -109,3 +109,10 @@ func makeTableData() render.TableData {
return *t
}
func makeContext() context.Context {
ctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles())
ctx = context.WithValue(ctx, internal.KeyViewConfig, config.NewCustomView())
return ctx
}

View File

@ -15,9 +15,8 @@ type (
// SortColumn represents a sortable column.
SortColumn struct {
index int
colCount int
asc bool
name string
asc bool
}
)
@ -61,6 +60,9 @@ type Tabular interface {
// Peek returns current model data.
Peek() render.TableData
// HasMetrics returns true if metrics are available on cluster.
HasMetrics() bool
// Watch watches a given resource for changes.
Watch(context.Context)

View File

@ -49,9 +49,9 @@ func (a *Alias) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Add(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true),
ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.GetTable().SortColCmd(0, true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Command", a.GetTable().SortColCmd(1, true), false),
ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", a.GetTable().SortColCmd(2, true), false),
ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.GetTable().SortColCmd("RESOURCE", true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Command", a.GetTable().SortColCmd("COMMAND", true), false),
ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", a.GetTable().SortColCmd("APIGROUP", true), false),
})
}

View File

@ -23,7 +23,7 @@ func TestAliasNew(t *testing.T) {
assert.Nil(t, v.Init(makeContext()))
assert.Equal(t, "Aliases", v.Name())
assert.Equal(t, 7, len(v.Hints()))
assert.Equal(t, 6, len(v.Hints()))
}
func TestAliasSearch(t *testing.T) {
@ -101,6 +101,7 @@ var _ ui.Tabular = &testModel{}
func (t *testModel) SetInstance(string) {}
func (t *testModel) Empty() bool { return false }
func (t *testModel) HasMetrics() bool { return true }
func (t *testModel) Peek() render.TableData { return makeTableData() }
func (t *testModel) ClusterWide() bool { return false }
func (t *testModel) GetNamespace() string { return "blee" }
@ -127,10 +128,10 @@ func (t *testModel) SetRefreshRate(time.Duration) {}
func makeTableData() render.TableData {
return render.TableData{
Namespace: client.ClusterScope,
Header: render.HeaderRow{
render.Header{Name: "RESOURCE"},
render.Header{Name: "COMMAND"},
render.Header{Name: "APIGROUP"},
Header: render.Header{
render.HeaderColumn{Name: "RESOURCE"},
render.HeaderColumn{Name: "COMMAND"},
render.HeaderColumn{Name: "APIGROUP"},
},
RowEvents: render.RowEvents{
render.RowEvent{

View File

@ -178,9 +178,15 @@ func (a *App) Halt() {
func (a *App) Resume() {
var ctx context.Context
ctx, a.cancelFn = context.WithCancel(context.Background())
go a.clusterUpdater(ctx)
if err := a.StylesUpdater(ctx, a); err != nil {
log.Error().Err(err).Msgf("Styles update failed")
if err := a.StylesWatcher(ctx, a); err != nil {
log.Error().Err(err).Msgf("Styles watcher failed")
}
if err := a.CustomViewsWatcher(ctx, a); err != nil {
log.Error().Err(err).Msgf("CustomView watcher failed")
}
}
@ -261,7 +267,6 @@ func (a *App) switchCtx(name string, loadPods bool) error {
}
a.initFactory(ns)
client.ResetMetrics()
if err := a.command.Reset(true); err != nil {
return err
}

View File

@ -28,7 +28,7 @@ func NewBenchmark(gvr client.GVR) ResourceViewer {
b.GetTable().SetBorderFocusColor(tcell.ColorSeaGreen)
b.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorSeaGreen, tcell.AttrNone)
b.GetTable().SetColorerFn(render.Benchmark{}.ColorerFunc())
b.GetTable().SetSortCol(b.GetTable().NameColIndex()+7, 0, true)
b.GetTable().SetSortCol(ageCol, true)
b.SetContextFn(b.benchContext)
b.GetTable().SetEnterFn(b.viewBench)

View File

@ -32,6 +32,7 @@ type Browser struct {
// NewBrowser returns a new browser.
func NewBrowser(gvr client.GVR) ResourceViewer {
log.Debug().Msgf("BRO %q", gvr)
return &Browser{
Table: NewTable(gvr),
}
@ -40,18 +41,17 @@ func NewBrowser(gvr client.GVR) ResourceViewer {
// Init watches all running pods in given namespace
func (b *Browser) Init(ctx context.Context) error {
var err error
b.meta, err = dao.MetaAccess.MetaFor(b.gvr)
b.meta, err = dao.MetaAccess.MetaFor(b.GVR())
if err != nil {
return err
}
b.BaseTitle = b.meta.Kind
if err = b.Table.Init(ctx); err != nil {
return err
}
ns := client.CleanseNamespace(b.app.Config.ActiveNamespace())
if dao.IsK8sMeta(b.meta) && b.app.ConOK() {
if _, e := b.app.factory.CanForResource(ns, b.GVR(), client.MonitorAccess); e != nil {
if _, e := b.app.factory.CanForResource(ns, b.GVR().String(), client.MonitorAccess); e != nil {
return e
}
}
@ -61,7 +61,7 @@ func (b *Browser) Init(ctx context.Context) error {
if b.bindKeysFn != nil {
b.bindKeysFn(b.Actions())
}
b.accessor, err = dao.AccessorFor(b.app.factory, b.gvr)
b.accessor, err = dao.AccessorFor(b.app.factory, b.GVR())
if err != nil {
return err
}
@ -173,7 +173,7 @@ func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
ctx := b.defaultContext()
raw, err := b.GetModel().ToYAML(ctx, path)
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)
return nil
}
@ -231,7 +231,7 @@ func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
if b.enterFn != nil {
f = b.enterFn
}
f(b.app, b.GetModel(), b.gvr.String(), path)
f(b.app, b.GetModel(), b.GVR().String(), path)
return nil
}
@ -252,9 +252,9 @@ func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
b.Stop()
defer b.Start()
{
msg := fmt.Sprintf("Delete %s %s?", b.gvr.R(), selections[0])
msg := fmt.Sprintf("Delete %s %s?", b.GVR().R(), selections[0])
if len(selections) > 1 {
msg = fmt.Sprintf("Delete %d marked %s?", len(selections), b.gvr)
msg = fmt.Sprintf("Delete %d marked %s?", len(selections), b.GVR())
}
if !dao.IsK8sMeta(b.meta) {
b.simpleDelete(selections, msg)
@ -271,7 +271,7 @@ func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
if path == "" {
return evt
}
describeResource(b.app, b.GetModel(), b.gvr.String(), path)
describeResource(b.app, b.GetModel(), b.GVR().String(), path)
return nil
}
@ -284,7 +284,7 @@ func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey {
}
ns, n := client.Namespaced(path)
if ok, err := b.app.Conn().CanI(ns, b.GVR(), []string{"edit"}); !ok || err != nil {
if ok, err := b.app.Conn().CanI(ns, b.GVR().String(), []string{"edit"}); !ok || err != nil {
b.App().Flash().Err(fmt.Errorf("Current user can't edit resource %s", b.GVR()))
return nil
}
@ -312,7 +312,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
}
ns := b.namespaces[i]
auth, err := b.App().factory.Client().CanI(ns, b.GVR(), client.MonitorAccess)
auth, err := b.App().factory.Client().CanI(ns, b.GVR().String(), client.MonitorAccess)
if !auth {
if err == nil {
err = fmt.Errorf("current user can't access namespace %s", ns)
@ -355,7 +355,7 @@ func (b *Browser) setNamespace(ns string) {
func (b *Browser) defaultContext() context.Context {
ctx := context.Background()
ctx = context.WithValue(ctx, internal.KeyFactory, b.app.factory)
ctx = context.WithValue(ctx, internal.KeyGVR, b.gvr.String())
ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR().String())
ctx = context.WithValue(ctx, internal.KeyPath, b.Path)
ctx = context.WithValue(ctx, internal.KeyLabels, "")
@ -424,9 +424,9 @@ func (b *Browser) simpleDelete(selections []string, msg string) {
dialog.ShowConfirm(b.app.Content.Pages, "Confirm Delete", msg, func() {
b.ShowDeleted()
if len(selections) > 1 {
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.gvr)
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR())
} else {
b.app.Flash().Infof("Delete resource %s %s", b.gvr, selections[0])
b.app.Flash().Infof("Delete resource %s %s", b.GVR(), selections[0])
}
for _, sel := range selections {
nuker, ok := b.accessor.(dao.Nuker)
@ -448,9 +448,9 @@ func (b *Browser) resourceDelete(selections []string, msg string) {
dialog.ShowDelete(b.app.Content.Pages, msg, func(cascade, force bool) {
b.ShowDeleted()
if len(selections) > 1 {
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.gvr)
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR())
} else {
b.app.Flash().Infof("Delete resource %s %s", b.gvr, selections[0])
b.app.Flash().Infof("Delete resource %s %s", b.GVR(), selections[0])
}
for _, sel := range selections {
if err := b.GetModel().Delete(b.defaultContext(), sel, cascade, force); err != nil {

View File

@ -37,9 +37,9 @@ func (c *Chart) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Add(ui.KeyActions{
ui.KeyB: ui.NewKeyAction("Blee", c.bleeCmd, true),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", c.GetTable().SortColCmd(0, true), false),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(2, true), false),
ui.KeyShiftA: ui.NewKeyAction("Sort Age", c.GetTable().SortColCmd(-1, true), false),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", c.GetTable().SortColCmd(nameCol, true), false),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false),
ui.KeyShiftA: ui.NewKeyAction("Sort Age", c.GetTable().SortColCmd(ageCol, true), false),
})
}

View File

@ -44,8 +44,11 @@ func (c *ClusterInfo) StylesChanged(s *config.Styles) {
}
func (c *ClusterInfo) layout() {
for row, v := range []string{"Context", "Cluster", "User", "K9s Rev", "K8s Rev", "CPU", "MEM"} {
c.SetCell(row, 0, c.sectionCell(v))
for row, section := range []string{"Context", "Cluster", "User", "K9s Rev", "K8s Rev", "CPU", "MEM"} {
if (section == "CPU" || section == "MEM") && !c.app.Conn().HasMetrics() {
continue
}
c.SetCell(row, 0, c.sectionCell(section))
c.SetCell(row, 1, c.infoCell(render.NAValue))
}
}
@ -53,13 +56,6 @@ func (c *ClusterInfo) layout() {
func (c *ClusterInfo) sectionCell(t string) *tview.TableCell {
cell := tview.NewTableCell(t + ":")
cell.SetAlign(tview.AlignLeft)
// var style tcell.Style
// style.Bold(true).
// Background(tcell.ColorGreen).
// Foreground(config.AsColor(c.styles.K9s.Info.SectionColor))
// cell.SetStyle(style)
// cell.SetBackgroundColor(c.app.Styles.BgColor())
// cell.SetBackgroundColor(tcell.ColorDefault)
cell.SetBackgroundColor(tcell.ColorGreen)
return cell
@ -76,44 +72,28 @@ func (c *ClusterInfo) infoCell(t string) *tview.TableCell {
// ClusterInfoUpdated notifies the cluster meta was updated.
func (c *ClusterInfo) ClusterInfoUpdated(data model.ClusterMeta) {
c.app.QueueUpdateDraw(func() {
var row int
c.GetCell(row, 1).SetText(data.Context)
row++
c.GetCell(row, 1).SetText(data.Cluster)
row++
c.GetCell(row, 1).SetText(data.User)
row++
c.GetCell(row, 1).SetText(data.K9sVer)
row++
c.GetCell(row, 1).SetText(data.K8sVer)
row++
c.GetCell(row, 1).SetText(render.AsPerc(data.Cpu) + "%")
row++
c.GetCell(row, 1).SetText(render.AsPerc(data.Mem) + "%")
c.ClusterInfoChanged(data, data)
}
c.updateStyle()
})
func (c *ClusterInfo) setCell(row int, s string) int {
c.GetCell(row, 1).SetText(s)
return row + 1
}
// ClusterInfoChanged notifies the cluster meta was changed.
func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) {
c.app.QueueUpdateDraw(func() {
var row int
c.GetCell(row, 1).SetText(curr.Context)
row++
c.GetCell(row, 1).SetText(curr.Cluster)
row++
c.GetCell(row, 1).SetText(curr.User)
row++
c.GetCell(row, 1).SetText(curr.K9sVer)
row++
c.GetCell(row, 1).SetText(curr.K8sVer)
row++
c.GetCell(row, 1).SetText(ui.AsPercDelta(prev.Cpu, curr.Cpu))
row++
c.GetCell(row, 1).SetText(ui.AsPercDelta(prev.Mem, curr.Mem))
c.Clear()
c.layout()
row := c.setCell(0, curr.Context)
row = c.setCell(row, curr.Cluster)
row = c.setCell(row, curr.User)
row = c.setCell(row, curr.K9sVer)
row = c.setCell(row, curr.K8sVer)
if c.app.Conn().HasMetrics() {
row = c.setCell(row, ui.AsPercDelta(prev.Cpu, curr.Cpu))
_ = c.setCell(row, ui.AsPercDelta(prev.Mem, curr.Mem))
}
c.updateStyle()
})
}

View File

@ -10,12 +10,10 @@ import (
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
)
const (
containerTitle = "Containers"
portsCol = 13
)
const containerTitle = "Containers"
// Container represents a container view.
type Container struct {
@ -53,12 +51,12 @@ func (c *Container) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true),
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(6, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(7, false), false),
ui.KeyShiftX: ui.NewKeyAction("Sort %CPU (REQ)", c.GetTable().SortColCmd(8, false), false),
ui.KeyShiftZ: ui.NewKeyAction("Sort %MEM (REQ)", c.GetTable().SortColCmd(9, false), false),
tcell.KeyCtrlX: ui.NewKeyAction("Sort %CPU (LIM)", c.GetTable().SortColCmd(8, false), false),
tcell.KeyCtrlQ: ui.NewKeyAction("Sort %MEM (LIM)", c.GetTable().SortColCmd(9, false), false),
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(cpuCol, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(memCol, false), false),
ui.KeyShiftX: ui.NewKeyAction("Sort %CPU (REQ)", c.GetTable().SortColCmd("%CPU/R", false), false),
ui.KeyShiftZ: ui.NewKeyAction("Sort %MEM (REQ)", c.GetTable().SortColCmd("%MEM/R", false), false),
tcell.KeyCtrlX: ui.NewKeyAction("Sort %CPU (LIM)", c.GetTable().SortColCmd("%CPU/L", false), false),
tcell.KeyCtrlQ: ui.NewKeyAction("Sort %MEM (LIM)", c.GetTable().SortColCmd("%MEM/L", false), false),
})
}
@ -131,13 +129,41 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (c *Container) isForwardable(path string) ([]string, bool) {
state := c.GetTable().GetSelectedCell(3)
if state != "Running" {
po, err := fetchPod(c.App().factory, c.GetTable().Path)
if err != nil {
return nil, false
}
cc := po.Spec.Containers
var co *v1.Container
for i := range cc {
if cc[i].Name == path {
co = &cc[i]
}
}
if co == nil {
log.Error().Err(fmt.Errorf("unable to locate container named %q", path))
return nil, false
}
var cs *v1.ContainerStatus
ss := po.Status.ContainerStatuses
for i := range ss {
if ss[i].Name == path {
cs = &ss[i]
}
}
if cs == nil {
log.Error().Err(fmt.Errorf("unable to locate container status named %q", path))
return nil, false
}
if render.ToContainerState(cs.State) != "Running" {
c.App().Flash().Err(fmt.Errorf("Container %s is not running?", path))
return nil, false
}
portC := c.GetTable().GetSelectedCell(portsCol)
portC := render.ToContainerPorts(co.Ports)
ports := strings.Split(portC, ",")
if len(ports) == 0 {
c.App().Flash().Err(errors.New("Container exposes no ports"))

View File

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

View File

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

View File

@ -73,7 +73,7 @@ func (c *CronJob) trigger(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
res, err := dao.AccessorFor(c.App().factory, client.NewGVR(c.GVR()))
res, err := dao.AccessorFor(c.App().factory, c.GVR())
if err != nil {
return nil
}

View File

@ -38,15 +38,15 @@ func NewDeploy(gvr client.GVR) ResourceViewer {
func (d *Deploy) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(1, true), false),
ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(2, true), false),
ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(3, true), false),
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(readyCol, true), false),
ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(uptodateCol, true), false),
ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(availCol, true), false),
})
}
func (d *Deploy) showPods(app *App, model ui.Tabular, gvr, path string) {
var res dao.Deployment
res.Init(app.factory, client.NewGVR(d.GVR()))
res.Init(app.factory, d.GVR())
dp, err := res.GetInstance(path)
if err != nil {

View File

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

View File

@ -30,17 +30,17 @@ func NewDaemonSet(gvr client.GVR) ResourceViewer {
func (d *DaemonSet) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
ui.KeyShiftD: ui.NewKeyAction("Sort Desired", d.GetTable().SortColCmd(1, true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Current", d.GetTable().SortColCmd(2, true), false),
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(3, true), false),
ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(4, true), false),
ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(5, true), false),
ui.KeyShiftD: ui.NewKeyAction("Sort Desired", d.GetTable().SortColCmd("DESIRED", true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Current", d.GetTable().SortColCmd("CURRENT", true), false),
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(readyCol, true), false),
ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(uptodateCol, true), false),
ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(availCol, true), false),
})
}
func (d *DaemonSet) showPods(app *App, model ui.Tabular, _, path string) {
var res dao.DaemonSet
res.Init(app.factory, client.NewGVR(d.GVR()))
res.Init(app.factory, d.GVR())
ds, err := res.GetInstance(path)
if err != nil {

View File

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

View File

@ -19,7 +19,7 @@ func NewEvent(gvr client.GVR) ResourceViewer {
}
e.GetTable().SetColorerFn(render.Event{}.ColorerFunc())
e.SetBindKeysFn(e.bindKeys)
e.GetTable().SetSortCol(7, 0, true)
e.GetTable().SetSortCol(ageCol, true)
return &e
}
@ -27,9 +27,9 @@ func NewEvent(gvr client.GVR) ResourceViewer {
func (e *Event) bindKeys(aa ui.KeyActions) {
aa.Delete(tcell.KeyCtrlD, ui.KeyE)
aa.Add(ui.KeyActions{
ui.KeyShiftY: ui.NewKeyAction("Sort Type", e.GetTable().SortColCmd(1, true), false),
ui.KeyShiftR: ui.NewKeyAction("Sort Reason", e.GetTable().SortColCmd(2, true), false),
ui.KeyShiftE: ui.NewKeyAction("Sort Source", e.GetTable().SortColCmd(3, true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Count", e.GetTable().SortColCmd(4, true), false),
ui.KeyShiftY: ui.NewKeyAction("Sort Type", e.GetTable().SortColCmd("TYPE", true), false),
ui.KeyShiftR: ui.NewKeyAction("Sort Reason", e.GetTable().SortColCmd("REASON", true), false),
ui.KeyShiftE: ui.NewKeyAction("Sort Source", e.GetTable().SortColCmd("SOURCE", true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Count", e.GetTable().SortColCmd("COUNT", true), false),
})
}

View File

@ -29,7 +29,7 @@ func (g *Group) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Add(ui.KeyActions{
tcell.KeyEnter: ui.NewKeyAction("Rules", g.policyCmd, true),
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", g.GetTable().SortColCmd(1, true), false),
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", g.GetTable().SortColCmd("KIND", true), false),
})
}

View File

@ -21,7 +21,7 @@ func TestHelp(t *testing.T) {
v := view.NewHelp()
assert.Nil(t, v.Init(ctx))
assert.Equal(t, 23, v.GetRowCount())
assert.Equal(t, 22, v.GetRowCount())
assert.Equal(t, 8, v.GetColumnCount())
assert.Equal(t, "<a>", strings.TrimSpace(v.GetCell(1, 0).Text))
assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text))

View File

@ -111,7 +111,7 @@ func podCtx(app *App, path, labelSel, fieldSel string) ContextFunc {
mx := client.NewMetricsServer(app.factory.Client())
nmx, err := mx.FetchPodsMetrics(ns)
if err != nil {
log.Warn().Err(err).Msgf("No pods metrics")
log.Debug().Err(err).Msgf("No pods metrics")
}
ctx = context.WithValue(ctx, internal.KeyMetrics, nmx)

View File

@ -65,7 +65,7 @@ func (l *LogsExtender) showLogs(path string, prev bool) {
if l.containerFn != nil {
co = l.containerFn()
}
if err := l.App().inject(NewLog(client.NewGVR(l.GVR()), path, co, prev)); err != nil {
if err := l.App().inject(NewLog(l.GVR(), path, co, prev)); err != nil {
l.App().Flash().Err(err)
}
}

View File

@ -28,10 +28,10 @@ func (n *Node) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlD)
aa.Add(ui.KeyActions{
ui.KeyY: ui.NewKeyAction("YAML", n.viewCmd, true),
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", n.GetTable().SortColCmd(7, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", n.GetTable().SortColCmd(8, false), false),
ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", n.GetTable().SortColCmd(9, false), false),
ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", n.GetTable().SortColCmd(10, false), false),
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", n.GetTable().SortColCmd(cpuCol, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", n.GetTable().SortColCmd(memCol, false), false),
ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", n.GetTable().SortColCmd("%CPU", false), false),
ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", n.GetTable().SortColCmd("%MEM", false), false),
})
}
@ -46,7 +46,7 @@ func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
}
sel := n.GetTable().GetSelectedItem()
gvr := client.NewGVR(n.GVR()).GVR()
gvr := n.GVR().GVR()
o, err := n.App().factory.Client().DynDialOrDie().Resource(gvr).Get(sel, metav1.GetOptions{})
if err != nil {
n.App().Flash().Errf("Unable to get resource %q -- %s", n.GVR(), err)

View File

@ -78,7 +78,6 @@ func (n *Namespace) decorate(data render.TableData) render.TableData {
// checks if all ns is in the list if not add it.
if _, ok := data.RowEvents.FindIndex(client.NamespaceAll); !ok {
log.Debug().Msg("YO!!")
data.RowEvents = append(data.RowEvents,
render.RowEvent{
Kind: render.EventUnchanged,

Some files were not shown because too many files have changed in this diff Show More