diff --git a/.goreleaser.yml b/.goreleaser.yml index 4f679b79..03150ea3 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -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: diff --git a/internal/resource/helpers.go b/internal/resource/helpers.go index a8857499..a57b87dd 100644 --- a/internal/resource/helpers.go +++ b/internal/resource/helpers.go @@ -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" diff --git a/internal/resource/helpers_test.go b/internal/resource/helpers_test.go index aa8a0cda..422e64da 100644 --- a/internal/resource/helpers_test.go +++ b/internal/resource/helpers_test.go @@ -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 diff --git a/internal/resource/job_int_test.go b/internal/resource/job_int_test.go index 62257df3..4b3110cd 100644 --- a/internal/resource/job_int_test.go +++ b/internal/resource/job_int_test.go @@ -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{ diff --git a/internal/resource/list.go b/internal/resource/list.go index 837173d8..c3de5a51 100644 --- a/internal/resource/list.go +++ b/internal/resource/list.go @@ -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 } diff --git a/internal/resource/pod.go b/internal/resource/pod.go index b042e796..7cd3efed 100644 --- a/internal/resource/pod.go +++ b/internal/resource/pod.go @@ -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 { diff --git a/internal/resource/pod_test.go b/internal/resource/pod_test.go index ccc0ddfb..80d01d33 100644 --- a/internal/resource/pod_test.go +++ b/internal/resource/pod_test.go @@ -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) { diff --git a/internal/views/app.go b/internal/views/app.go index c1aba4a7..54bfefb4 100644 --- a/internal/views/app.go +++ b/internal/views/app.go @@ -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) diff --git a/internal/views/command.go b/internal/views/command.go index 991de100..776f1cea 100644 --- a/internal/views/command.go +++ b/internal/views/command.go @@ -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 diff --git a/internal/views/fu.go b/internal/views/fu.go new file mode 100644 index 00000000..0dcb6901 --- /dev/null +++ b/internal/views/fu.go @@ -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" + } +} diff --git a/internal/views/rbac.go b/internal/views/rbac.go index 76cbc88e..9a658fc9 100644 --- a/internal/views/rbac.go +++ b/internal/views/rbac.go @@ -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 +} diff --git a/internal/views/registrar.go b/internal/views/registrar.go index 96579bd0..83f6f883 100644 --- a/internal/views/registrar.go +++ b/internal/views/registrar.go @@ -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 diff --git a/internal/views/table.go b/internal/views/table.go index beb4d373..1645b176 100644 --- a/internal/views/table.go +++ b/internal/views/table.go @@ -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 { diff --git a/internal/views/table_test.go b/internal/views/table_test.go new file mode 100644 index 00000000..e4e977d2 --- /dev/null +++ b/internal/views/table_test.go @@ -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) + } +}