added rbac and fu views
parent
fec714f0ca
commit
437e73ae6b
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue