added rbac and fu views

mine
derailed 2019-03-27 22:46:31 -06:00
parent fec714f0ca
commit 437e73ae6b
14 changed files with 566 additions and 135 deletions

View File

@ -3,10 +3,8 @@ before:
hooks:
- go mod download
- go generate ./...
release:
prerelease: true
builds:
- env:
- CGO_ENABLED=0
@ -24,14 +22,14 @@ builds:
- 7
ldflags:
- -s -w -X github.com/derailed/k9s/cmd.version={{.Version}} -X github.com/derailed/k9s/cmd.commit={{.Commit}} -X github.com/derailed/k9s/cmd.date={{.Date}}
archive:
replacements:
darwin: Darwin
linux: Linux
windows: Windows
arm: 32-bit
arm64: 64-bit
bit: Arm
bitv6: Arm6
bitv7: Arm7
386: i386
amd64: x86_64
checksum:
@ -63,29 +61,23 @@ brew:
# Snap
snapcraft:
name: k9s
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
replacements:
amd64: 64-bit
386: 32-bit
darwin: macOS
linux: Tux
publish: false
summary: K9s is a CLI to view and manage your Kubernetes clusters.
description: |
K9s is a CLI to view and manage your Kubernetes clusters.
By leveraging a terminal UI, you can easily traverse Kubernetes resources
and view the state of you clusters in a single powerful session.
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
replacements:
amd64: 64-bit
386: 32-bit
darwin: macOS
linux: Tux
publish: false
grade: devel
confinement: devmode
apps:
k9s:
plugs: ["home", "network", "home-dir"]
plugs:
home-dir:
read:

View File

@ -90,6 +90,14 @@ func toAge(timestamp metav1.Time) string {
return duration.HumanDuration(time.Since(timestamp.Time))
}
// FixCol set column width to specified size by either truncating or padding.
func FixCol(s string, size int) string {
if len(s) > size {
return Truncate(s, size)
}
return s + strings.Repeat(" ", size-len(s))
}
// Pad a string up to the given length.
func Pad(s string, l int) string {
fmat := "%-" + strconv.Itoa(l) + "s"

View File

@ -109,6 +109,22 @@ func TestTruncate(t *testing.T) {
}
}
func TestSizeCol(t *testing.T) {
uu := []struct {
s string
l int
e string
}{
{"fred", 3, "fr…"},
{"01234567890", 10, "012345678…"},
{"fred", 10, "fred "},
}
for _, u := range uu {
assert.Equal(t, u.e, FixCol(u.s, u.l))
}
}
func TestMapToStr(t *testing.T) {
uu := []struct {
i map[string]string

View File

@ -96,9 +96,9 @@ func TestJobToDuration(t *testing.T) {
},
{
batchv1.JobStatus{
StartTime: &t1,
StartTime: &metav1.Time{time.Now().Add(-10 * time.Second)},
},
"101d",
"10s",
},
{
batchv1.JobStatus{

View File

@ -145,6 +145,10 @@ func (l *list) GetNamespace() string {
// SetNamespace updates the namespace on the list. Default ns is "" for all
// namespaces.
func (l *list) SetNamespace(n string) {
if l.namespace == NotNamespaced {
return
}
if n == AllNamespace {
n = AllNamespaces
}

View File

@ -14,7 +14,6 @@ import (
const (
defaultTimeout = 1 * time.Second
podNameSize = 42
)
type (
@ -188,7 +187,7 @@ func (*Pod) Header(ns string) Row {
"NAME",
"READY",
"STATUS",
"RESTARTS",
"RS",
"CPU",
"MEM",
"IP",
@ -204,14 +203,14 @@ func (r *Pod) Fields(ns string) Row {
i := r.instance
if ns == AllNamespaces {
ff = append(ff, i.Namespace)
ff = append(ff, FixCol(i.Namespace, 13))
}
ss := i.Status.ContainerStatuses
cr, _, rc := r.statuses(ss)
return append(ff,
Pad(i.ObjectMeta.Name, podNameSize),
FixCol(i.ObjectMeta.Name, 50),
strconv.Itoa(cr)+"/"+strconv.Itoa(len(ss)),
r.phase(i.Status),
strconv.Itoa(rc),
@ -219,7 +218,7 @@ func (r *Pod) Fields(ns string) Row {
ToMi(r.metrics.CurrentMEM),
i.Status.PodIP,
i.Spec.NodeName,
string(i.Status.QOSClass),
r.mapQOS(i.Status.QOSClass),
toAge(i.ObjectMeta.CreationTimestamp),
)
}
@ -227,6 +226,16 @@ func (r *Pod) Fields(ns string) Row {
// ----------------------------------------------------------------------------
// Helpers...
func (*Pod) mapQOS(class v1.PodQOSClass) string {
switch class {
case v1.PodQOSGuaranteed:
return "Ga"
case v1.PodQOSBurstable:
return "Bu"
default:
return "BE"
}
}
func (r *Pod) statuses(ss []v1.ContainerStatus) (cr, ct, rc int) {
for _, c := range ss {
if c.State.Terminated != nil {

View File

@ -41,7 +41,7 @@ func TestPodListAccess(t *testing.T) {
func TestPodFields(t *testing.T) {
r := newPod().Fields("blee")
assert.Equal(t, resource.Pad("fred", 42), r[0])
assert.Equal(t, resource.FixCol("fred", 50), r[0])
}
func TestPodMarshal(t *testing.T) {

View File

@ -272,7 +272,7 @@ func (a *appView) inject(p igniter) {
var ctx context.Context
{
ctx, a.cancel = context.WithCancel(context.TODO())
ctx, a.cancel = context.WithCancel(context.Background())
p.init(ctx, a.config.ActiveNamespace())
}
a.content.AddPage("main", p, true, true)

View File

@ -2,6 +2,7 @@ package views
import (
"fmt"
"regexp"
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
@ -36,22 +37,27 @@ func (c *command) defaultCmd() {
// Helpers...
var fuMatcher = regexp.MustCompile(`\Afu\s([u|g|s]):([\w-:]+)\b`)
// Exec the command by showing associated display.
func (c *command) run(cmd string) bool {
defer func() {
if err := recover(); err != nil {
log.Debug().Msgf("Command failed %v", err)
}
}()
var v resourceViewer
switch cmd {
case "q", "quit":
switch {
case cmd == "q", cmd == "quit":
c.app.Stop()
return true
case "?", "help", "alias":
case cmd == "?", cmd == "help":
c.app.inject(newHelpView(c.app))
return true
case cmd == "alias":
c.app.inject(newAliasView(c.app))
return true
case fuMatcher.MatchString(cmd):
tokens := fuMatcher.FindAllStringSubmatch(cmd, -1)
if len(tokens) == 1 && len(tokens[0]) == 3 {
c.app.inject(newFuView(c.app, tokens[0][1], tokens[0][2]))
return true
}
default:
if res, ok := resourceViews()[cmd]; ok {
var r resource.List

326
internal/views/fu.go Normal file
View File

@ -0,0 +1,326 @@
package views
import (
"context"
"fmt"
"reflect"
"time"
"github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
)
var fuHeader = append(resource.Row{"NAMESPACE", "NAME", "GROUP", "BINDING"}, rbacHeaderVerbs...)
type fuView struct {
*tableView
current igniter
cancel context.CancelFunc
subjectKind string
subjectName string
cache resource.RowEvents
}
func newFuView(app *appView, subject, name string) *fuView {
v := fuView{}
{
v.subjectKind, v.subjectName = v.mapSubject(subject), name
v.tableView = newTableView(app, v.getTitle())
v.colorerFn = rbacColorer
v.current = app.content.GetPrimitive("main").(igniter)
v.bindKeys()
}
return &v
}
// Init the view.
func (v *fuView) init(_ context.Context, ns string) {
v.sortCol = sortColumn{1, len(rbacHeader), true}
ctx, cancel := context.WithCancel(context.Background())
v.cancel = cancel
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Debug().Msg("FU Watch bailing out!")
return
case <-time.After(time.Duration(v.app.config.K9s.RefreshRate) * time.Second):
v.refresh()
v.app.Draw()
}
}
}(ctx)
v.refresh()
v.app.SetFocus(v)
}
func (v *fuView) bindKeys() {
delete(v.actions, KeyShiftA)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false)
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
v.actions[KeyShiftS] = newKeyAction("Sort Namespace", v.sortColCmd(0), true)
v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(1), true)
v.actions[KeyShiftO] = newKeyAction("Sort Group", v.sortColCmd(2), true)
v.actions[KeyShiftB] = newKeyAction("Sort Binding", v.sortColCmd(3), true)
}
func (v *fuView) getTitle() string {
return fmt.Sprintf(rbacTitleFmt, "Fu", v.subjectKind+":"+v.subjectName)
}
func (v *fuView) refresh() {
data, err := v.reconcile()
if err != nil {
log.Error().Err(err).Msgf("Unable to reconcile for %s:%s", v.subjectKind, v.subjectName)
}
v.update(data)
}
func (v *fuView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !v.cmdBuff.empty() {
v.cmdBuff.reset()
return nil
}
return v.backCmd(evt)
}
func (v *fuView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cancel != nil {
v.cancel()
}
if v.cmdBuff.isActive() {
v.cmdBuff.reset()
} else {
v.app.prevCmd(evt)
}
return nil
}
func (v *fuView) hints() hints {
return v.actions.toHints()
}
func (v *fuView) reconcile() (resource.TableData, error) {
log.Debug().Msg("ClusterRoles...")
evts, errs := v.clusterPolicies()
if len(errs) > 0 {
for _, err := range errs {
log.Debug().Err(err).Msg("Unable to find cluster policies")
}
return resource.TableData{}, errs[0]
}
log.Debug().Msg("Roles...")
nevts, errs := v.namespacePolicies()
if len(errs) > 0 {
for _, err := range errs {
log.Debug().Err(err).Msg("Unable to find cluster policies")
}
return resource.TableData{}, errs[0]
}
for k, v := range nevts {
evts[k] = v
}
data := resource.TableData{
Header: fuHeader,
Rows: make(resource.RowEvents, len(evts)),
Namespace: "*",
}
noDeltas := make(resource.Row, len(fuHeader))
if len(v.cache) == 0 {
for k, ev := range evts {
ev.Action = resource.New
ev.Deltas = noDeltas
data.Rows[k] = ev
}
v.cache = evts
return data, nil
}
for k, ev := range evts {
data.Rows[k] = ev
newr := ev.Fields
if _, ok := v.cache[k]; !ok {
ev.Action, ev.Deltas = watch.Added, noDeltas
continue
}
oldr := v.cache[k].Fields
deltas := make(resource.Row, len(newr))
if !reflect.DeepEqual(oldr, newr) {
ev.Action = watch.Modified
for i, field := range oldr {
if field != newr[i] {
deltas[i] = field
}
}
ev.Deltas = deltas
} else {
ev.Action = resource.Unchanged
ev.Deltas = noDeltas
}
}
v.cache = evts
for k := range v.cache {
if _, ok := data.Rows[k]; !ok {
delete(v.cache, k)
}
}
return data, nil
}
func (v *fuView) clusterPolicies() (resource.RowEvents, []error) {
var errs []error
evts := make(resource.RowEvents)
crbs, err := v.app.conn().DialOrDie().Rbac().ClusterRoleBindings().List(metav1.ListOptions{})
if err != nil {
return evts, errs
}
var roles []string
for _, c := range crbs.Items {
for _, s := range c.Subjects {
if s.Kind == v.subjectKind && s.Name == v.subjectName {
roles = append(roles, c.RoleRef.Name)
}
}
}
for _, r := range roles {
cr, err := v.app.conn().DialOrDie().Rbac().ClusterRoles().Get(r, metav1.GetOptions{})
if err != nil {
errs = append(errs, err)
}
e := v.parseRules("*", r, cr.Rules)
for k, v := range e {
evts[k] = v
}
}
return evts, errs
}
func (v *fuView) namespacePolicies() (resource.RowEvents, []error) {
var errs []error
evts := make(resource.RowEvents)
rbs, err := v.app.conn().DialOrDie().Rbac().RoleBindings("").List(metav1.ListOptions{})
if err != nil {
return evts, errs
}
type nsRole struct {
ns, role string
}
var roles []nsRole
for _, rb := range rbs.Items {
for _, s := range rb.Subjects {
if s.Kind == v.subjectKind && s.Name == v.subjectName {
roles = append(roles, nsRole{rb.Namespace, rb.RoleRef.Name})
}
}
}
for _, r := range roles {
cr, err := v.app.conn().DialOrDie().Rbac().Roles(r.ns).Get(r.role, metav1.GetOptions{})
if err != nil {
errs = append(errs, err)
}
e := v.parseRules(r.ns, r.role, cr.Rules)
for k, v := range e {
evts[k] = v
}
}
return evts, errs
}
func (v *fuView) namespace(ns, n string) string {
return ns + "/" + n
}
func (v *fuView) parseRules(ns, binding string, rules []rbacv1.PolicyRule) resource.RowEvents {
m := make(resource.RowEvents, len(rules))
for _, r := range rules {
for _, grp := range r.APIGroups {
for _, res := range r.Resources {
k := res
if grp != "" {
k = res + "." + grp
}
for _, na := range r.ResourceNames {
n := k + "/" + na
m[v.namespace(ns, n)] = &resource.RowEvent{
Fields: v.prepRow(ns, n, grp, binding, r.Verbs),
}
}
m[v.namespace(ns, k)] = &resource.RowEvent{
Fields: v.prepRow(ns, k, grp, binding, r.Verbs),
}
}
}
for _, nres := range r.NonResourceURLs {
if nres[0] != '/' {
nres = "/" + nres
}
m[v.namespace(ns, nres)] = &resource.RowEvent{
Fields: v.prepRow(ns, nres, resource.NAValue, binding, r.Verbs),
}
}
}
return m
}
func (v *fuView) prepRow(ns, res, grp, binding string, verbs []string) resource.Row {
const (
nameLen = 60
groupLen = 30
nsLen = 30
)
if grp != resource.NAValue {
grp = toGroup(grp)
}
return v.makeRow(resource.Pad(ns, nsLen), resource.Pad(res, nameLen), resource.Pad(grp, groupLen), binding, asVerbs(verbs...))
}
func (*fuView) makeRow(ns, res, group, binding string, verbs []string) resource.Row {
r := make(resource.Row, 0, len(fuHeader))
r = append(r, ns, res, group, binding)
return append(r, verbs...)
}
func (v *fuView) mapSubject(subject string) string {
switch subject {
case "g":
return "Group"
case "s":
return "ServiceAccount"
default:
return "User"
}
}

View File

@ -84,13 +84,17 @@ func newRBACView(app *appView, ns, name string, kind roleKind) *rbacView {
v.roleName, v.roleType = name, kind
v.tableView = newTableView(app, v.getTitle())
v.currentNS = ns
// v.SetSelectedStyle(tcell.ColorWhite, tcell.ColorDarkSeaGreen, tcell.AttrNone)
v.colorerFn = rbacColorer
v.current = app.content.GetPrimitive("main").(igniter)
v.bindKeys()
}
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false)
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
v.actions[KeyShiftO] = newKeyAction("Sort Groups", v.sortColCmd(1), true)
return &v
}
// Init the view.
func (v *rbacView) init(_ context.Context, ns string) {
v.sortCol = sortColumn{1, len(rbacHeader), true}
ctx, cancel := context.WithCancel(context.Background())
v.cancel = cancel
@ -100,24 +104,27 @@ func newRBACView(app *appView, ns, name string, kind roleKind) *rbacView {
case <-ctx.Done():
log.Debug().Msg("RBAC Watch bailing out!")
return
case <-time.After(time.Duration(app.config.K9s.RefreshRate) * time.Second):
case <-time.After(time.Duration(v.app.config.K9s.RefreshRate) * time.Second):
v.refresh()
v.app.Draw()
}
}
}(ctx)
return &v
}
// Init the view.
func (v *rbacView) init(_ context.Context, ns string) {
// v.baseTitle = v.getTitle()
v.sortCol = sortColumn{1, len(rbacHeader), true}
v.refresh()
v.app.SetFocus(v)
}
func (v *rbacView) bindKeys() {
delete(v.actions, KeyShiftA)
v.actions[tcell.KeyEscape] = newKeyAction("Reset", v.resetCmd, false)
v.actions[KeySlash] = newKeyAction("Filter", v.activateCmd, false)
v.actions[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
v.actions[KeyShiftO] = newKeyAction("Sort Groups", v.sortColCmd(1), true)
}
func (v *rbacView) getTitle() string {
title := "ClusterRole"
if v.roleType == role {
@ -127,6 +134,10 @@ func (v *rbacView) getTitle() string {
return fmt.Sprintf(rbacTitleFmt, title, v.roleName)
}
func (v *rbacView) hints() hints {
return v.actions.toHints()
}
func (v *rbacView) refresh() {
data, err := v.reconcile(v.currentNS, v.roleName, v.roleType)
if err != nil {
@ -152,16 +163,12 @@ func (v *rbacView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cmdBuff.isActive() {
v.cmdBuff.reset()
} else {
v.app.inject(v.current)
v.app.prevCmd(evt)
}
return nil
}
func (v *rbacView) hints() hints {
return v.actions.toHints()
}
func (v *rbacView) reconcile(ns, name string, kind roleKind) (resource.TableData, error) {
evts, err := v.rowEvents(ns, name, kind)
if err != nil {
@ -216,6 +223,7 @@ func (v *rbacView) reconcile(ns, name string, kind roleKind) (resource.TableData
delete(v.cache, k)
}
}
return data, nil
}
@ -241,13 +249,6 @@ func (v *rbacView) rowEvents(ns, name string, kind roleKind) (resource.RowEvents
return evts, nil
}
func toGroup(g string) string {
if g == "" {
return "v1"
}
return g
}
func (v *rbacView) clusterPolicies(name string) (resource.RowEvents, error) {
cr, err := v.app.conn().DialOrDie().Rbac().ClusterRoles().Get(name, metav1.GetOptions{})
if err != nil {
@ -267,12 +268,7 @@ func (v *rbacView) namespacedPolicies(path string) (resource.RowEvents, error) {
return v.parseRules(cr.Rules), nil
}
func (*rbacView) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
const (
nameLen = 60
groupLen = 30
)
func (v *rbacView) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
m := make(resource.RowEvents, len(rules))
for _, r := range rules {
for _, grp := range r.APIGroups {
@ -284,11 +280,11 @@ func (*rbacView) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
for _, na := range r.ResourceNames {
n := k + "/" + na
m[n] = &resource.RowEvent{
Fields: makeRow(resource.Pad(n, nameLen), resource.Pad(toGroup(grp), groupLen), asVerbs(r.Verbs...)),
Fields: prepRow(n, grp, r.Verbs),
}
}
m[k] = &resource.RowEvent{
Fields: makeRow(resource.Pad(k, nameLen), resource.Pad(toGroup(grp), groupLen), asVerbs(r.Verbs...)),
Fields: prepRow(k, grp, r.Verbs),
}
}
}
@ -297,7 +293,7 @@ func (*rbacView) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
nres = "/" + nres
}
m[nres] = &resource.RowEvent{
Fields: makeRow(resource.Pad(nres, nameLen), resource.Pad(resource.NAValue, groupLen), asVerbs(r.Verbs...)),
Fields: prepRow(nres, resource.NAValue, r.Verbs),
}
}
}
@ -305,8 +301,21 @@ func (*rbacView) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
return m
}
func prepRow(res, grp string, verbs []string) resource.Row {
const (
nameLen = 60
groupLen = 30
)
if grp != resource.NAValue {
grp = toGroup(grp)
}
return makeRow(resource.Pad(res, nameLen), resource.Pad(grp, groupLen), asVerbs(verbs...))
}
func makeRow(res, group string, verbs []string) resource.Row {
r := make(resource.Row, 0, 12)
r := make(resource.Row, 0, len(rbacHeader))
r = append(r, res, group)
return append(r, verbs...)
@ -361,3 +370,10 @@ func hasVerb(verbs []string, verb string) bool {
return false
}
func toGroup(g string) string {
if g == "" {
return "v1"
}
return g
}

View File

@ -4,7 +4,6 @@ import (
"github.com/derailed/k9s/internal/k8s"
"github.com/derailed/k9s/internal/resource"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
type (
@ -76,7 +75,6 @@ func allCRDs(c k8s.Connection) map[string]k8s.APIGroup {
}
func showRBAC(app *appView, ns, resource, selection string) {
log.Debug().Msgf("Entered FN on `%s`--%s:%s", ns, resource, selection)
kind := clusterRole
if resource == "role" {
kind = role

View File

@ -54,6 +54,7 @@ func newTableView(app *appView, title string) *tableView {
v.baseTitle = title
v.actions = make(keyActions)
v.SetBorder(true)
v.SetFixed(1, 0)
v.SetBorderColor(tcell.ColorDodgerBlue)
v.SetBorderAttributes(tcell.AttrBold)
v.SetBorderPadding(0, 0, 1, 1)
@ -63,11 +64,30 @@ func newTableView(app *appView, title string) *tableView {
v.SetSelectable(true, false)
v.SetSelectedStyle(tcell.ColorBlack, tcell.ColorAqua, tcell.AttrBold)
v.SetInputCapture(v.keyboard)
v.registerHandlers()
v.bindKeys()
}
return &v
}
func (v *tableView) bindKeys() {
v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, true)
v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(0), true)
v.actions[KeyShiftA] = newKeyAction("Sort Age", v.sortColCmd(-1), true)
v.actions[KeySlash] = newKeyAction("Filter Mode", v.activateCmd, false)
v.actions[tcell.KeyEscape] = newKeyAction("Filter Reset", v.resetCmd, false)
v.actions[tcell.KeyEnter] = newKeyAction("Filter", v.filterCmd, false)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[KeyG] = newKeyAction("Top", v.app.puntCmd, false)
v.actions[KeyShiftG] = newKeyAction("Bottom", v.app.puntCmd, false)
v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false)
v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd, false)
}
func (v *tableView) clearSelection() {
v.Select(0, 0)
v.ScrollToBeginning()
@ -90,6 +110,7 @@ func (v *tableView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf(">> TableView handled %s", tcell.KeyNames[key])
return a.action(evt)
}
return evt
}
@ -101,11 +122,13 @@ func (v *tableView) setSelection() {
func (v *tableView) pageUpCmd(evt *tcell.EventKey) *tcell.EventKey {
v.PageUp()
return nil
}
func (v *tableView) pageDownCmd(evt *tcell.EventKey) *tcell.EventKey {
v.PageDown()
return nil
}
@ -120,6 +143,7 @@ func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
if v.cmdBuff.isActive() {
v.cmdBuff.del()
}
return nil
}
@ -129,6 +153,7 @@ func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
}
v.cmdBuff.reset()
v.refresh()
return nil
}
@ -156,12 +181,14 @@ func (v *tableView) sortColCmd(col int) func(evt *tcell.EventKey) *tcell.EventKe
func (v *tableView) sortNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
v.sortCol.index, v.sortCol.asc = 0, true
v.refresh()
return nil
}
func (v *tableView) sortInvertCmd(evt *tcell.EventKey) *tcell.EventKey {
v.sortCol.asc = !v.sortCol.asc
v.refresh()
return nil
}
@ -173,6 +200,7 @@ func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
v.app.flash(flashInfo, "Filtering...")
v.cmdBuff.reset()
v.cmdBuff.setActive(true)
return nil
}
@ -205,6 +233,7 @@ func (v *tableView) hints() hints {
if v.actions != nil {
return v.actions.toHints()
}
return nil
}
@ -250,6 +279,7 @@ func (v *tableView) filtered() resource.TableData {
filtered.Rows[k] = row
}
}
return filtered
}
@ -267,7 +297,7 @@ func (v *tableView) displayCol(index int, name string) string {
func (v *tableView) doUpdate(data resource.TableData) {
v.currentNS = data.Namespace
if v.currentNS == resource.AllNamespaces {
if v.currentNS == resource.AllNamespaces || v.currentNS == "*" {
v.actions[KeyShiftS] = newKeyAction("Sort Namespace", v.sortNamespaceCmd, true)
} else {
delete(v.actions, KeyShiftS)
@ -293,15 +323,15 @@ func (v *tableView) doUpdate(data resource.TableData) {
}
row++
// for k := range data.Rows {
// log.Debug().Msgf("Keys: `%s`", k)
// }
sortFn := v.defaultSort
if v.sortFn != nil {
sortFn = v.sortFn
}
keys := v.sortRows(data)
// log.Debug().Msgf("KEYS %#v", keys)
keys := make([]string, len(data.Rows))
v.sortRows(data.Rows, sortFn, v.sortCol, keys)
groupKeys := map[string][]string{}
for _, k := range keys {
// log.Debug().Msgf("RKEY: %s", k)
grp := data.Rows[k].Fields[v.sortCol.index]
if s, ok := groupKeys[grp]; ok {
s = append(s, k)
@ -358,48 +388,21 @@ func (v *tableView) addBodyCell(row, col int, field, delta string, color tcell.C
v.SetCell(row, col, c)
}
func (v *tableView) defaultSort(rows resource.Rows) {
t := rowSorter{rows: rows, index: v.sortCol.index, asc: v.sortCol.asc}
func (v *tableView) defaultSort(rows resource.Rows, sortCol sortColumn) {
t := rowSorter{rows: rows, index: sortCol.index, asc: sortCol.asc}
sort.Sort(t)
}
func (v *tableView) sortRows(data resource.TableData) []string {
rows := make(resource.Rows, 0, len(data.Rows))
for _, r := range data.Rows {
rows = append(rows, r.Fields)
func (*tableView) sortRows(evts resource.RowEvents, sortFn sortFn, sortCol sortColumn, keys []string) {
rows := make(resource.Rows, 0, len(evts))
for k, r := range evts {
rows = append(rows, append(r.Fields, k))
}
sortFn(rows, sortCol)
if v.sortFn != nil {
v.sortFn(rows, v.sortCol)
} else {
v.defaultSort(rows)
}
keys := make([]string, len(rows))
for i, r := range rows {
col, prefix := 0, v.currentNS
switch v.currentNS {
case resource.AllNamespaces:
col, prefix = 1, r[0]
case resource.NotNamespaced:
prefix = ""
}
key := r[col]
if v.cleanseFn != nil {
key = v.cleanseFn(key)
} else {
key = v.defaultColCleanse(key)
}
if len(prefix) == 0 {
keys[i] = key
} else {
keys[i] = prefix + "/" + key
}
keys[i] = r[len(r)-1]
}
return keys
}
func (*tableView) defaultColCleanse(s string) string {
@ -433,26 +436,7 @@ func (v *tableView) resetTitle() {
// ----------------------------------------------------------------------------
// Event listeners...
func (v *tableView) registerHandlers() {
v.actions[KeyShiftI] = newKeyAction("Invert", v.sortInvertCmd, true)
v.actions[KeyShiftN] = newKeyAction("Sort Name", v.sortColCmd(0), true)
v.actions[KeyShiftA] = newKeyAction("Sort Age", v.sortColCmd(-1), true)
v.actions[KeySlash] = newKeyAction("Filter Mode", v.activateCmd, false)
v.actions[tcell.KeyEscape] = newKeyAction("Filter Reset", v.resetCmd, false)
v.actions[tcell.KeyEnter] = newKeyAction("Filter", v.filterCmd, false)
v.actions[tcell.KeyBackspace2] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyBackspace] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false)
v.actions[KeyG] = newKeyAction("Top", v.app.puntCmd, false)
v.actions[KeyShiftG] = newKeyAction("Bottom", v.app.puntCmd, false)
v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false)
v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd, false)
}
func (v *tableView) changed(s string) {
}
func (v *tableView) changed(s string) {}
func (v *tableView) active(b bool) {
if b {

View File

@ -0,0 +1,72 @@
package views
import (
"testing"
"github.com/derailed/k9s/internal/resource"
"github.com/stretchr/testify/assert"
)
func TestTVSortRows(t *testing.T) {
uu := []struct {
rows resource.RowEvents
col int
asc bool
first resource.Row
e []string
}{
{
resource.RowEvents{
"row1": {Fields: resource.Row{"x", "y"}},
"row2": {Fields: resource.Row{"a", "b"}},
},
0,
true,
resource.Row{"a", "b"},
[]string{"row2", "row1"},
},
{
resource.RowEvents{
"row1": {Fields: resource.Row{"x", "y"}},
"row2": {Fields: resource.Row{"a", "b"}},
},
1,
true,
resource.Row{"a", "b"},
[]string{"row2", "row1"},
},
{
resource.RowEvents{
"row1": {Fields: resource.Row{"x", "y"}},
"row2": {Fields: resource.Row{"a", "b"}},
},
1,
false,
resource.Row{"x", "y"},
[]string{"row1", "row2"},
},
}
var v *tableView
for _, u := range uu {
keys := make([]string, len(u.rows))
v.sortRows(u.rows, v.defaultSort, sortColumn{u.col, len(u.rows), u.asc}, keys)
assert.Equal(t, u.e, keys)
assert.Equal(t, u.first, u.rows[u.e[0]].Fields)
}
}
func BenchmarkTVSortRows(b *testing.B) {
evts := resource.RowEvents{
"row1": {Fields: resource.Row{"x", "y"}},
"row2": {Fields: resource.Row{"a", "b"}},
}
sc := sortColumn{0, 2, true}
var v *tableView
keys := make([]string, len(evts))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
v.sortRows(evts, v.defaultSort, sc, keys)
}
}