misc bug fixes

mine
derailed 2019-06-18 17:54:44 -06:00
parent 921a39f897
commit c61365a9c9
19 changed files with 533 additions and 330 deletions

View File

@ -10,6 +10,7 @@ for changes and offers subsequent commands to interact with observed Kubernetes
---
[![Go Report Card](https://goreportcard.com/badge/github.com/derailed/k9s?)](https://goreportcard.com/report/github.com/derailed/k9s)
[![codebeat badge](https://codebeat.co/badges/89e5a80e-dfe8-4426-acf6-6be781e0a12e)](https://codebeat.co/projects/github-com-derailed-k9s-master)
[![Build Status](https://travis-ci.com/derailed/k9s.svg?branch=master)](https://travis-ci.com/derailed/k9s)
[![release](https://img.shields.io/github/release-pre/derailed/k9s.svg)](https://github.com/derailed/k9s/releases)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mum4k/termdash/blob/master/LICENSE)

3
go.mod
View File

@ -20,7 +20,6 @@ require (
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect
github.com/evanphx/json-patch v4.1.0+incompatible // indirect
github.com/fatih/camelcase v1.0.0 // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell v1.1.1
github.com/gogo/protobuf v1.2.1 // indirect
@ -34,7 +33,6 @@ require (
github.com/imdario/mergo v0.3.7 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/json-iterator/go v1.1.6 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-runewidth v0.0.4
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
@ -45,7 +43,6 @@ require (
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.3.0
github.com/wercker/stern v0.0.0-20181017112310-807830e57719
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 // indirect
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 // indirect
golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 // indirect

13
go.sum
View File

@ -33,10 +33,6 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/derailed/tview v0.1.8 h1:HjkCCTzgZWkkyUtJMGxhRJtvIvcRLuPCb9Uh11KRoC4=
github.com/derailed/tview v0.1.8/go.mod h1:g+ZyIsV5osK+lQ6LajiGQeLW10BQLJ6aMvy8Ldt2oa0=
github.com/derailed/tview v0.1.9 h1:CYyGBvhJ4VenoRlUE1NDstyv4kayjQVnidSDAwuemdk=
github.com/derailed/tview v0.1.9/go.mod h1:g+ZyIsV5osK+lQ6LajiGQeLW10BQLJ6aMvy8Ldt2oa0=
github.com/derailed/tview v0.1.10 h1:QWjK82ccTl3C7Tfyfmv765eRqEt/T3aXp40464cfnlw=
github.com/derailed/tview v0.1.10/go.mod h1:g+ZyIsV5osK+lQ6LajiGQeLW10BQLJ6aMvy8Ldt2oa0=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
@ -55,8 +51,6 @@ github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7Vpz
github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
@ -129,10 +123,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC15uMxFv5FY/J/8vzyaBiArCOkMdFT9Jsw78iY=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -193,8 +183,6 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/wercker/stern v0.0.0-20181017112310-807830e57719 h1:x9/CGytbUciiGoBHn04xsJJR/Lsg4b3qQlKJSZY5Gzw=
github.com/wercker/stern v0.0.0-20181017112310-807830e57719/go.mod h1:+72MfLYlS87s4tqq+eVDANQ9GdILz0lkpAFlX/1+WWY=
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek=
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@ -243,7 +231,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190426135247-a129542de9ae h1:mQLHiymj/JXKnnjc62tb7nD5pZLs940/sXJu+Xp3DBA=
golang.org/x/sys v0.0.0-20190426135247-a129542de9ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -22,19 +22,18 @@ type (
// Style tracks K9s styles.
Style struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
LogoColor string `yaml:"logoColor"`
Info *Info `yaml:"info"`
Border *Border `yaml:"border"`
Menu *Menu `yaml:"menu"`
Crumb *Crumb `yaml:"crumb"`
Table *Table `yaml:"table"`
Status *Status `yaml:"status"`
Title *Title `yaml:"title"`
Yaml *Yaml `yaml:"yaml"`
Log *Log `yaml:"logs"`
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
LogoColor string `yaml:"logoColor"`
Title *Title `yaml:"title"`
Border *Border `yaml:"border"`
Info *Info `yaml:"info"`
Menu *Menu `yaml:"menu"`
Crumb *Crumb `yaml:"crumb"`
Table *Table `yaml:"table"`
Status *Status `yaml:"status"`
Yaml *Yaml `yaml:"yaml"`
Log *Log `yaml:"logs"`
}
// Status tracks resource status styles.
@ -117,13 +116,13 @@ func newStyle() *Style {
FgColor: "cadetblue",
BgColor: "black",
LogoColor: "orange",
Info: newInfo(),
Border: newBorder(),
Title: newTitle(),
Info: newInfo(),
Menu: newMenu(),
Crumb: newCrumb(),
Table: newTable(),
Status: newStatus(),
Title: newTitle(),
Yaml: newYaml(),
Log: newLog(),
}

View File

@ -3,12 +3,14 @@ package resource
import (
"bytes"
"context"
"fmt"
"path"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/watch"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions/printers"
@ -127,14 +129,23 @@ func (b *Base) List(ns string) (Columnars, error) {
// Describe a given resource.
func (b *Base) Describe(kind, pa string) (string, error) {
ns, n := namespaced(pa)
mapping, err := k8s.RestMapping.Find(kind)
if err != nil {
log.Debug().Msgf("Unable to find mapper for %s %s", kind, pa)
return "", err
g, v, n := b.Resource.(*k8s.Resource).GetInfo()
mapper := k8s.RestMapper{b.Connection}
var e error
mapping, e = mapper.ResourceFor(fmt.Sprintf("%s.%s.%s", n, v, g))
if e != nil {
log.Debug().Err(err).Msgf("Unable to find mapper for %s %s", kind, pa)
return "", err
}
}
return b.doDescribe(pa, mapping)
}
func (b *Base) doDescribe(pa string, mapping *meta.RESTMapping) (string, error) {
ns, n := namespaced(pa)
d, err := versioned.Describer(b.Connection.Config().Flags(), mapping)
if err != nil {
log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping)

View File

@ -39,32 +39,23 @@ func (v *aliasView) init(context.Context, string) {
v.update(v.hydrate())
v.app.SetFocus(v)
v.resetTitle()
v.app.setHints(v.hints())
}
func (v *aliasView) registerActions() {
delete(v.actions, KeyShiftA)
v.actions[tcell.KeyEnter] = newKeyAction("Goto", v.gotoCmd, true)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false)
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
v.actions[KeyShiftR] = newKeyAction("Sort Resources", v.sortResourceCmd, true)
v.actions[KeyShiftO] = newKeyAction("Sort Groups", v.sortGroupCmd, true)
v.actions[KeyShiftR] = newKeyAction("Sort Resources", v.sortColCmd(1), true)
v.actions[KeyShiftO] = newKeyAction("Sort Groups", v.sortColCmd(2), true)
}
func (v *aliasView) getTitle() string {
return aliasTitle
}
func (v *aliasView) sortResourceCmd(evt *tcell.EventKey) *tcell.EventKey {
v.sortCol.index, v.sortCol.asc = 1, true
v.refresh()
return nil
}
func (v *aliasView) sortGroupCmd(evt *tcell.EventKey) *tcell.EventKey {
v.sortCol.index, v.sortCol.asc = 2, true
v.refresh()
return nil
}
func (v *aliasView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.empty() {
v.cmdBuff.reset()
@ -115,7 +106,8 @@ func (v *aliasView) hints() hints {
}
func (v *aliasView) hydrate() resource.TableData {
cmds := helpCmds(v.app.conn())
cmds := make(map[string]resCmd, 40)
aliasCmds(v.app.conn(), cmds)
data := resource.TableData{
Header: resource.Row{"NAME", "RESOURCE", "APIGROUP"},

View File

@ -99,8 +99,10 @@ func (a *appView) registerActions() {
}
func (a *appView) Init(version string, rate int) {
a.startInformer()
a.clusterInfo().init(version)
if a.conn() != nil {
a.startInformer()
a.clusterInfo().init(version)
}
a.cmdBuff.addListener(a.cmd())
header := tview.NewFlex()
@ -333,6 +335,7 @@ func (a *appView) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
func (a *appView) currentView() igniter {
return a.content.GetPrimitive("main").(igniter)
}
func (a *appView) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.inCmdMode() {
return evt

View File

@ -1,18 +1,16 @@
package views
// import (
// "testing"
import (
"testing"
// "github.com/derailed/k9s/internal/config"
// "github.com/stretchr/testify/assert"
// )
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert"
)
// func TestNewApp(t *testing.T) {
// mk := NewMockKubeSettings()
// cfg := config.NewConfig(mk)
// a := NewApp(cfg)
// a.Init("blee", 10)
func TestNewApp(t *testing.T) {
a := NewApp(config.NewConfig(ks{}))
a.Init("blee", 10)
// assert.Equal(t, 10, len(a.actions))
// assert.Equal(t, false, a.hasSkins)
// }
assert.Equal(t, 10, len(a.actions))
assert.Equal(t, false, a.hasSkins)
}

View File

@ -23,13 +23,11 @@ const (
// K9sBenchDir directory to store K9s benchmark files.
var K9sBenchDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-bench-%s", config.MustK9sUser()))
type (
benchmark struct {
canceled bool
config config.BenchConfig
worker *requester.Work
}
)
type benchmark struct {
canceled bool
config config.BenchConfig
worker *requester.Work
}
func newBenchmark(base string, cfg config.BenchConfig) (*benchmark, error) {
b := benchmark{config: cfg}

View File

@ -68,7 +68,9 @@ func (c *command) run(cmd string) bool {
return true
}
default:
if res, ok := resourceViews(c.app.conn())[cmd]; ok {
cmds := make(map[string]resCmd, 30)
resourceViews(c.app.conn(), cmds)
if res, ok := cmds[cmd]; ok {
var r resource.List
if res.listFn != nil {
r = res.listFn(c.app.conn(), resource.DefaultNamespace)
@ -91,20 +93,22 @@ func (c *command) run(cmd string) bool {
}
}
res, ok := allCRDs(c.app.conn())[cmd]
cmds := make(map[string]resCmd, 30)
allCRDs(c.app.conn(), cmds)
res, ok := cmds[cmd]
if !ok {
c.app.flash().warnf("Huh? `%s` command not found", cmd)
return false
}
name := res.Plural
if len(name) == 0 {
name = res.Singular
name := res.plural
if name == "" {
name = res.singular
}
v = newResourceView(
res.Kind,
res.title,
c.app,
resource.NewCustomList(c.app.conn(), "", res.Group, res.Version, name),
resource.NewCustomList(c.app.conn(), "", res.api, res.version, name),
)
v.setColorerFn(defaultColorer)
c.exec(cmd, v)

View File

@ -0,0 +1,19 @@
package views
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert"
)
func TestCommandPush(t *testing.T) {
c := newCommand(NewApp(config.NewConfig(ks{})))
c.pushCmd("fred")
c.pushCmd("blee")
p, top := c.previousCmd()
assert.Equal(t, "fred", p)
assert.True(t, top)
assert.True(t, c.lastCmd())
}

View File

@ -10,8 +10,8 @@ type contextView struct {
*resourceView
}
func newContextView(t string, app *appView, list resource.List) resourceViewer {
v := contextView{newResourceView(t, app, list).(*resourceView)}
func newContextView(title string, app *appView, list resource.List) resourceViewer {
v := contextView{newResourceView(title, app, list).(*resourceView)}
v.extraActionsFn = v.extraActions
v.enterFn = v.useCtx
v.getTV().cleanseFn = v.cleanser

View File

@ -0,0 +1,33 @@
package views
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource"
"github.com/stretchr/testify/assert"
)
func TestContextView(t *testing.T) {
l := resource.NewContextList(nil, "fred")
v := newContextView("blee", NewApp(config.NewConfig(ks{})), l)
assert.Equal(t, "blee", v.getTitle())
}
func TestCleaner(t *testing.T) {
uu := map[string]struct {
s, e string
}{
"normal": {"fred", "fred"},
"default": {"fred*", "fred"},
"delta": {"fred(𝜟)", "fred"},
}
v := contextView{}
for k, u := range uu {
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, v.cleanser(u.s))
})
}
}

View File

@ -12,8 +12,8 @@ type deployView struct {
*logResourceView
}
func newDeployView(ns string, app *appView, list resource.List) resourceViewer {
v := deployView{newLogResourceView(ns, app, list)}
func newDeployView(title string, app *appView, list resource.List) resourceViewer {
v := deployView{newLogResourceView(title, app, list)}
v.extraActionsFn = v.extraActions
v.enterFn = v.showPods

16
internal/views/dp_test.go Normal file
View File

@ -0,0 +1,16 @@
package views
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource"
"github.com/stretchr/testify/assert"
)
func TestDeployView(t *testing.T) {
l := resource.NewDeploymentList(nil, "fred")
v := newDeployView("blee", NewApp(config.NewConfig(ks{})), l)
assert.Equal(t, "blee", v.getTitle())
}

16
internal/views/ds_test.go Normal file
View File

@ -0,0 +1,16 @@
package views
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource"
"github.com/stretchr/testify/assert"
)
func TestDaemonSetView(t *testing.T) {
l := resource.NewDaemonSetList(nil, "fred")
v := newDaemonSetView("blee", NewApp(config.NewConfig(ks{})), l)
assert.Equal(t, "blee", v.getTitle())
}

View File

@ -18,6 +18,9 @@ type (
resCmd struct {
title string
api string
version string
plural string
singular string
viewFn viewFn
listFn listFn
enterFn enterFn
@ -26,25 +29,14 @@ type (
}
)
func helpCmds(c k8s.Connection) map[string]resCmd {
cmdMap := resourceViews(c)
cmds := make(map[string]resCmd, len(cmdMap))
for k, v := range cmdMap {
cmds[k] = v
func aliasCmds(c k8s.Connection, m map[string]resCmd) {
resourceViews(c, m)
if c != nil {
allCRDs(c, m)
}
for k, v := range allCRDs(c) {
cmds[k] = resCmd{title: v.Kind, api: v.Group}
}
return cmds
}
func allCRDs(c k8s.Connection) map[string]k8s.APIGroup {
m := map[string]k8s.APIGroup{}
if c == nil {
return m
}
func allCRDs(c k8s.Connection, m map[string]resCmd) {
crds, _ := resource.NewCustomResourceDefinitionList(c, resource.AllNamespaces).
Resource().
List(resource.AllNamespaces)
@ -58,24 +50,23 @@ func allCRDs(c k8s.Connection) map[string]k8s.APIGroup {
Version: ff["version"].(string),
}
res := resCmd{title: grp.Kind, api: grp.Group, version: grp.Version}
if p, ok := ff["plural"].(string); ok {
grp.Plural = p
m[p] = grp
res.plural = p
m[p] = res
}
if s, ok := ff["singular"].(string); ok {
grp.Singular = s
m[s] = grp
res.singular = s
m[s] = res
}
if aa, ok := ff["aliases"].([]interface{}); ok {
for _, a := range aa {
m[a.(string)] = grp
m[a.(string)] = res
}
}
}
return m
}
func showRBAC(app *appView, ns, resource, selection string) {
@ -110,220 +101,254 @@ func showSAPolicy(app *appView, _, _, selection string) {
app.inject(newPolicyView(app, mapFuSubject("ServiceAccount"), n))
}
func resourceViews(c k8s.Connection) map[string]resCmd {
cmds := map[string]resCmd{
"cm": {
title: "ConfigMaps",
api: "",
viewFn: newResourceView,
listFn: resource.NewConfigMapList,
},
"cr": {
title: "ClusterRoles",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleList,
enterFn: showRBAC,
},
"crb": {
title: "ClusterRoleBindings",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleBindingList,
enterFn: showClusterRole,
},
"crd": {
title: "CustomResourceDefinitions",
api: "apiextensions.k8s.io",
viewFn: newResourceView,
listFn: resource.NewCustomResourceDefinitionList,
},
"cj": {
title: "CronJobs",
api: "batch",
viewFn: newCronJobView,
listFn: resource.NewCronJobList,
},
"ctx": {
title: "Contexts",
api: "",
viewFn: newContextView,
listFn: resource.NewContextList,
colorerFn: ctxColorer,
},
"ds": {
title: "DaemonSets",
api: "",
viewFn: newDaemonSetView,
listFn: resource.NewDaemonSetList,
colorerFn: dpColorer,
},
"dp": {
title: "Deployments",
api: "apps",
viewFn: newDeployView,
listFn: resource.NewDeploymentList,
colorerFn: dpColorer,
},
"ep": {
title: "EndPoints",
api: "",
viewFn: newResourceView,
listFn: resource.NewEndpointsList,
},
"ev": {
title: "Events",
api: "",
viewFn: newResourceView,
listFn: resource.NewEventList,
colorerFn: evColorer,
},
"ing": {
title: "Ingress",
api: "extensions",
viewFn: newResourceView,
listFn: resource.NewIngressList,
},
"jo": {
title: "Jobs",
api: "batch",
viewFn: newJobView,
listFn: resource.NewJobList,
},
"no": {
title: "Nodes",
api: "",
viewFn: newNodeView,
listFn: resource.NewNodeList,
colorerFn: nsColorer,
},
"ns": {
title: "Namespaces",
api: "",
viewFn: newNamespaceView,
listFn: resource.NewNamespaceList,
colorerFn: nsColorer,
},
"pdb": {
title: "PodDisruptionBudgets",
api: "v1.beta1",
viewFn: newResourceView,
listFn: resource.NewPDBList,
colorerFn: pdbColorer,
},
"po": {
title: "Pods",
api: "",
viewFn: newPodView,
listFn: resource.NewPodList,
colorerFn: podColorer,
},
"pv": {
title: "PersistentVolumes",
api: "",
viewFn: newResourceView,
listFn: resource.NewPersistentVolumeList,
colorerFn: pvColorer,
},
"pvc": {
title: "PersistentVolumeClaims",
api: "",
viewFn: newResourceView,
listFn: resource.NewPersistentVolumeClaimList,
colorerFn: pvcColorer,
},
"rb": {
title: "RoleBindings",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewRoleBindingList,
enterFn: showRole,
},
"rc": {
title: "ReplicationControllers",
api: "",
viewFn: newResourceView,
listFn: resource.NewReplicationControllerList,
colorerFn: rsColorer,
},
"ro": {
title: "Roles",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewRoleList,
enterFn: showRBAC,
},
"rs": {
title: "ReplicaSets",
api: "apps",
viewFn: newReplicaSetView,
listFn: resource.NewReplicaSetList,
colorerFn: rsColorer,
},
"sa": {
title: "ServiceAccounts",
api: "",
viewFn: newResourceView,
listFn: resource.NewServiceAccountList,
enterFn: showSAPolicy,
},
"sec": {
title: "Secrets",
api: "",
viewFn: newSecretView,
listFn: resource.NewSecretList,
},
"sts": {
title: "StatefulSets",
api: "apps",
viewFn: newStatefulSetView,
listFn: resource.NewStatefulSetList,
colorerFn: stsColorer,
},
"svc": {
title: "Services",
api: "",
viewFn: newSvcView,
listFn: resource.NewServiceList,
},
"usr": {
title: "Users",
api: "",
viewFn: newSubjectView,
},
"grp": {
title: "Groups",
api: "",
viewFn: newSubjectView,
},
"pf": {
title: "PortForward",
api: "",
viewFn: newForwardView,
},
"be": {
title: "Benchmark",
api: "",
viewFn: newBenchView,
},
"sd": {
title: "ScreenDumps",
api: "",
viewFn: newDumpView,
},
func resourceViews(c k8s.Connection, m map[string]resCmd) {
coreRes(m)
rbacRes(m)
apiExtRes(m)
batchRes(m)
appsRes(m)
extRes(m)
v1beta1Res(m)
custRes(m)
if c != nil {
hpaRes(c, m)
}
}
func coreRes(m map[string]resCmd) {
m["cm"] = resCmd{
title: "ConfigMaps",
api: "",
viewFn: newResourceView,
listFn: resource.NewConfigMapList,
}
m["ctx"] = resCmd{
title: "Contexts",
api: "",
viewFn: newContextView,
listFn: resource.NewContextList,
colorerFn: ctxColorer,
}
m["ds"] = resCmd{
title: "DaemonSets",
api: "",
viewFn: newDaemonSetView,
listFn: resource.NewDaemonSetList,
colorerFn: dpColorer,
}
m["ep"] = resCmd{
title: "EndPoints",
api: "",
viewFn: newResourceView,
listFn: resource.NewEndpointsList,
}
m["ev"] = resCmd{
title: "Events",
api: "",
viewFn: newResourceView,
listFn: resource.NewEventList,
colorerFn: evColorer,
}
m["no"] = resCmd{
title: "Nodes",
api: "",
viewFn: newNodeView,
listFn: resource.NewNodeList,
colorerFn: nsColorer,
}
m["ns"] = resCmd{
title: "Namespaces",
api: "",
viewFn: newNamespaceView,
listFn: resource.NewNamespaceList,
colorerFn: nsColorer,
}
m["po"] = resCmd{
title: "Pods",
api: "",
viewFn: newPodView,
listFn: resource.NewPodList,
colorerFn: podColorer,
}
m["pv"] = resCmd{
title: "PersistentVolumes",
api: "",
viewFn: newResourceView,
listFn: resource.NewPersistentVolumeList,
colorerFn: pvColorer,
}
m["pvc"] = resCmd{
title: "PersistentVolumeClaims",
api: "",
viewFn: newResourceView,
listFn: resource.NewPersistentVolumeClaimList,
colorerFn: pvcColorer,
}
m["rc"] = resCmd{
title: "ReplicationControllers",
api: "",
viewFn: newResourceView,
listFn: resource.NewReplicationControllerList,
colorerFn: rsColorer,
}
m["sa"] = resCmd{
title: "ServiceAccounts",
api: "",
viewFn: newResourceView,
listFn: resource.NewServiceAccountList,
enterFn: showSAPolicy,
}
m["sec"] = resCmd{
title: "Secrets",
api: "",
viewFn: newSecretView,
listFn: resource.NewSecretList,
}
m["svc"] = resCmd{
title: "Services",
api: "",
viewFn: newSvcView,
listFn: resource.NewServiceList,
}
}
func custRes(m map[string]resCmd) {
m["usr"] = resCmd{
title: "Users",
api: "",
viewFn: newSubjectView,
}
m["grp"] = resCmd{
title: "Groups",
api: "",
viewFn: newSubjectView,
}
m["pf"] = resCmd{
title: "PortForward",
api: "",
viewFn: newForwardView,
}
m["be"] = resCmd{
title: "Benchmark",
api: "",
viewFn: newBenchView,
}
m["sd"] = resCmd{
title: "ScreenDumps",
api: "",
viewFn: newDumpView,
}
}
func rbacRes(m map[string]resCmd) {
m["cr"] = resCmd{
title: "ClusterRoles",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleList,
enterFn: showRBAC,
}
m["crb"] = resCmd{
title: "ClusterRoleBindings",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewClusterRoleBindingList,
enterFn: showClusterRole,
}
if c == nil {
return cmds
m["rb"] = resCmd{
title: "RoleBindings",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewRoleBindingList,
enterFn: showRole,
}
m["ro"] = resCmd{
title: "Roles",
api: "rbac.authorization.k8s.io",
viewFn: newResourceView,
listFn: resource.NewRoleList,
enterFn: showRBAC,
}
}
func apiExtRes(m map[string]resCmd) {
m["crd"] = resCmd{
title: "CustomResourceDefinitions",
api: "apiextensions.k8s.io",
viewFn: newResourceView,
listFn: resource.NewCustomResourceDefinitionList,
}
}
func batchRes(m map[string]resCmd) {
m["cj"] = resCmd{
title: "CronJobs",
api: "batch",
viewFn: newCronJobView,
listFn: resource.NewCronJobList,
}
m["jo"] = resCmd{
title: "Jobs",
api: "batch",
viewFn: newJobView,
listFn: resource.NewJobList,
}
}
func appsRes(m map[string]resCmd) {
m["dp"] = resCmd{
title: "Deployments",
api: "apps",
viewFn: newDeployView,
listFn: resource.NewDeploymentList,
colorerFn: dpColorer,
}
m["rs"] = resCmd{
title: "ReplicaSets",
api: "apps",
viewFn: newReplicaSetView,
listFn: resource.NewReplicaSetList,
colorerFn: rsColorer,
}
m["sts"] = resCmd{
title: "StatefulSets",
api: "apps",
viewFn: newStatefulSetView,
listFn: resource.NewStatefulSetList,
colorerFn: stsColorer,
}
}
func extRes(m map[string]resCmd) {
m["ing"] = resCmd{
title: "Ingress",
api: "extensions",
viewFn: newResourceView,
listFn: resource.NewIngressList,
}
}
func v1beta1Res(m map[string]resCmd) {
m["pdb"] = resCmd{
title: "PodDisruptionBudgets",
api: "v1.beta1",
viewFn: newResourceView,
listFn: resource.NewPDBList,
colorerFn: pdbColorer,
}
}
func hpaRes(c k8s.Connection, cmds map[string]resCmd) {
rev, ok, err := c.SupportsRes("autoscaling", []string{"v1", "v2beta1", "v2beta2"})
if err != nil {
log.Error().Err(err).Msg("Checking HPA")
return cmds
return
}
if !ok {
log.Error().Msg("HPA are not supported on this cluster")
return cmds
return
}
switch rev {
@ -351,6 +376,4 @@ func resourceViews(c k8s.Connection) map[string]resCmd {
default:
log.Panic().Msgf("K9s unsupported HPA version. Exiting!")
}
return cmds
}

View File

@ -49,14 +49,14 @@ type (
app *appView
baseTitle string
currentNS string
data resource.TableData
actions keyActions
cmdBuff *cmdBuff
colorerFn colorerFn
sortFn sortFn
cleanseFn cleanseFn
data resource.TableData
cmdBuff *cmdBuff
sortCol sortColumn
filterFn func(string)
sortCol sortColumn
}
)
@ -406,7 +406,15 @@ func (v *tableView) doUpdate(data resource.TableData) {
fgColor = v.colorerFn(data.Namespace, data.Rows[sk])
}
for col, field := range data.Rows[sk].Fields {
v.addBodyCell(data.NumCols, data.Header[col], row, col, field, data.Rows[sk].Deltas[col], fgColor, pads)
header := data.Header[col]
field, align := v.formatCell(data.NumCols[header], header, field, pads[col])
c := tview.NewTableCell(field + deltas(data.Rows[sk].Deltas[col], field))
{
c.SetExpansion(1)
c.SetAlign(align)
c.SetTextColor(fgColor)
}
v.SetCell(row, col, c)
}
row++
}
@ -447,7 +455,7 @@ func (v *tableView) addHeaderCell(numCols map[string]bool, col int, name string,
v.SetCell(0, col, c)
}
func (v *tableView) addBodyCell(numCols map[string]bool, header string, row, col int, field, delta string, color tcell.Color, pads maxyPad) {
func (v *tableView) formatCell(numerical bool, header, field string, padding int) (string, int) {
if header == "AGE" {
dur, err := time.ParseDuration(field)
if err == nil {
@ -455,21 +463,16 @@ func (v *tableView) addBodyCell(numCols map[string]bool, header string, row, col
}
}
field += deltas(delta, field)
align := tview.AlignLeft
if numCols[header] || cpuRX.MatchString(header) || memRX.MatchString(header) {
align = tview.AlignRight
} else if isASCII(field) {
field = pad(field, pads[col])
if numerical || cpuRX.MatchString(header) || memRX.MatchString(header) {
return field, tview.AlignRight
}
c := tview.NewTableCell(field)
{
c.SetExpansion(1)
c.SetAlign(align)
c.SetTextColor(color)
align := tview.AlignLeft
if isASCII(field) {
return pad(field, padding), align
}
v.SetCell(row, col, c)
return field, align
}
func (v *tableView) defaultSort(rows resource.Rows, sortCol sortColumn) {

View File

@ -2,13 +2,116 @@ package views
import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/resource"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/watch"
)
func TestTableViewSave(t *testing.T) {
v := newTableView(NewApp(config.NewConfig(ks{})), "test")
v.baseTitle = "k9s-test"
dir := filepath.Join(config.K9sDumpDir, v.app.config.K9s.CurrentCluster)
c1, _ := ioutil.ReadDir(dir)
v.saveCmd(nil)
c2, _ := ioutil.ReadDir(dir)
assert.Equal(t, len(c2), len(c1)+1)
}
func TestTableViewNew(t *testing.T) {
v := newTableView(NewApp(config.NewConfig(ks{})), "test")
data := resource.TableData{
Header: resource.Row{"NAMESPACE", "NAME", "FRED", "AGE"},
Rows: resource.RowEvents{
"ns1/a": &resource.RowEvent{
Action: watch.Added,
Fields: resource.Row{"ns1", "a", "10", "3m"},
Deltas: resource.Row{"", "", "", ""},
},
"ns1/b": &resource.RowEvent{
Action: watch.Added,
Fields: resource.Row{"ns1", "b", "15", "1m"},
Deltas: resource.Row{"", "", "20", ""},
},
},
NumCols: map[string]bool{
"FRED": true,
},
Namespace: "",
}
v.update(data)
assert.Equal(t, 3, v.GetRowCount())
}
func TestTableViewFilter(t *testing.T) {
v := newTableView(NewApp(config.NewConfig(ks{})), "test")
data := resource.TableData{
Header: resource.Row{"NAMESPACE", "NAME", "FRED", "AGE"},
Rows: resource.RowEvents{
"ns1/blee": &resource.RowEvent{
Action: watch.Added,
Fields: resource.Row{"ns1", "blee", "10", "3m"},
Deltas: resource.Row{"", "", "", ""},
},
"ns1/fred": &resource.RowEvent{
Action: watch.Added,
Fields: resource.Row{"ns1", "fred", "15", "1m"},
Deltas: resource.Row{"", "", "20", ""},
},
},
NumCols: map[string]bool{
"FRED": true,
},
Namespace: "",
}
v.update(data)
v.cmdBuff.setActive(true)
v.cmdBuff.buff = []rune("blee")
v.filterCmd(nil)
assert.Equal(t, 2, v.GetRowCount())
v.resetCmd(nil)
assert.Equal(t, 3, v.GetRowCount())
}
func TestTableViewSort(t *testing.T) {
v := newTableView(NewApp(config.NewConfig(ks{})), "test")
data := resource.TableData{
Header: resource.Row{"NAMESPACE", "NAME", "FRED", "AGE"},
Rows: resource.RowEvents{
"ns1/blee": &resource.RowEvent{
Action: watch.Added,
Fields: resource.Row{"ns1", "blee", "10", "3m"},
Deltas: resource.Row{"", "", "", ""},
},
"ns1/fred": &resource.RowEvent{
Action: watch.Added,
Fields: resource.Row{"ns1", "fred", "15", "1m"},
Deltas: resource.Row{"", "", "20", ""},
},
},
NumCols: map[string]bool{
"FRED": true,
},
Namespace: "",
}
v.update(data)
v.sortColCmd(1)(nil)
assert.Equal(t, 3, v.GetRowCount())
assert.Equal(t, "blee ", v.GetCell(1, 1).Text)
v.sortInvertCmd(nil)
assert.Equal(t, 3, v.GetRowCount())
assert.Equal(t, "fred ", v.GetCell(1, 1).Text)
}
func TestIsSelector(t *testing.T) {
uu := map[string]struct {
sel string