checkpoint
parent
0a6caa2d54
commit
fec714f0ca
|
|
@ -4,11 +4,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -30,28 +28,8 @@ func newAliasView(app *appView) *aliasView {
|
|||
v.colorerFn = aliasColorer
|
||||
v.current = app.content.GetPrimitive("main").(igniter)
|
||||
v.currentNS = ""
|
||||
v.registerActions()
|
||||
}
|
||||
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)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
v.cancel = cancel
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug().Msg("Alias GR bailing out!")
|
||||
return
|
||||
case <-time.After(1 * time.Second):
|
||||
v.update(v.hydrate())
|
||||
v.app.Draw()
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +40,14 @@ func (v *aliasView) init(context.Context, string) {
|
|||
v.resetTitle()
|
||||
}
|
||||
|
||||
func (v *aliasView) registerActions() {
|
||||
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)
|
||||
}
|
||||
|
||||
func (v *aliasView) getTitle() string {
|
||||
return aliasTitle
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type (
|
|||
|
||||
igniter interface {
|
||||
tview.Primitive
|
||||
|
||||
getTitle() string
|
||||
init(ctx context.Context, ns string)
|
||||
}
|
||||
|
|
@ -30,6 +31,7 @@ type (
|
|||
|
||||
resourceViewer interface {
|
||||
igniter
|
||||
setEnterFn(enterFn)
|
||||
}
|
||||
|
||||
appView struct {
|
||||
|
|
@ -90,6 +92,8 @@ func NewApp(cfg *config.Config) *appView {
|
|||
v.actions[tcell.KeyDelete] = newKeyAction("Erase", v.eraseCmd, false)
|
||||
v.actions[tcell.KeyTab] = newKeyAction("Focus", v.focusCmd, false)
|
||||
|
||||
// v.actions[KeyO] = newKeyAction("RBAC", v.rbacCmd, false)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +160,11 @@ func (a *appView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return evt
|
||||
}
|
||||
|
||||
func (a *appView) rbacCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
a.inject(newRBACView(a, "", "aa_k9s", clusterRole))
|
||||
return evt
|
||||
}
|
||||
|
||||
func (a *appView) redrawCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
a.Draw()
|
||||
return evt
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ func aliasColorer(string, *resource.RowEvent) tcell.Color {
|
|||
return tcell.ColorFuchsia
|
||||
}
|
||||
|
||||
func rbacColorer(ns string, r *resource.RowEvent) tcell.Color {
|
||||
c := defaultColorer(ns, r)
|
||||
|
||||
// return tcell.ColorDarkOliveGreen
|
||||
return c
|
||||
}
|
||||
|
||||
func podColorer(ns string, r *resource.RowEvent) tcell.Color {
|
||||
c := defaultColorer(ns, r)
|
||||
|
||||
|
|
@ -62,6 +69,7 @@ func podColorer(ns string, r *resource.RowEvent) tcell.Color {
|
|||
c = errColor
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +82,7 @@ func ctxColorer(ns string, r *resource.RowEvent) tcell.Color {
|
|||
if strings.Contains(strings.TrimSpace(r.Fields[0]), "*") {
|
||||
c = highlightColor
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
|
@ -86,6 +95,7 @@ func pvColorer(ns string, r *resource.RowEvent) tcell.Color {
|
|||
if strings.TrimSpace(r.Fields[4]) != "Bound" {
|
||||
return errColor
|
||||
}
|
||||
|
||||
return stdColor
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +113,7 @@ func pvcColorer(ns string, r *resource.RowEvent) tcell.Color {
|
|||
if strings.TrimSpace(r.Fields[markCol]) != "Bound" {
|
||||
c = errColor
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +130,7 @@ func pdbColorer(ns string, r *resource.RowEvent) tcell.Color {
|
|||
if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) {
|
||||
return errColor
|
||||
}
|
||||
|
||||
return stdColor
|
||||
}
|
||||
|
||||
|
|
@ -135,6 +147,7 @@ func dpColorer(ns string, r *resource.RowEvent) tcell.Color {
|
|||
if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) {
|
||||
return errColor
|
||||
}
|
||||
|
||||
return stdColor
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +164,7 @@ func stsColorer(ns string, r *resource.RowEvent) tcell.Color {
|
|||
if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) {
|
||||
return errColor
|
||||
}
|
||||
|
||||
return stdColor
|
||||
}
|
||||
|
||||
|
|
@ -167,6 +181,7 @@ func rsColorer(ns string, r *resource.RowEvent) tcell.Color {
|
|||
if strings.TrimSpace(r.Fields[markCol]) != strings.TrimSpace(r.Fields[markCol+1]) {
|
||||
return errColor
|
||||
}
|
||||
|
||||
return stdColor
|
||||
}
|
||||
|
||||
|
|
@ -184,6 +199,7 @@ func evColorer(ns string, r *resource.RowEvent) tcell.Color {
|
|||
case "Killing":
|
||||
c = killColor
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ func (c *command) run(cmd string) bool {
|
|||
}
|
||||
}()
|
||||
|
||||
var v igniter
|
||||
var v resourceViewer
|
||||
switch cmd {
|
||||
case "q", "quit":
|
||||
c.app.Stop()
|
||||
|
|
@ -56,12 +56,19 @@ func (c *command) run(cmd string) bool {
|
|||
if res, ok := resourceViews()[cmd]; ok {
|
||||
var r resource.List
|
||||
if res.listMxFn != nil {
|
||||
r = res.listMxFn(c.app.conn(), k8s.NewMetricsServer(c.app.conn()), resource.DefaultNamespace)
|
||||
r = res.listMxFn(c.app.conn(),
|
||||
k8s.NewMetricsServer(c.app.conn()),
|
||||
resource.DefaultNamespace,
|
||||
)
|
||||
} else {
|
||||
r = res.listFn(c.app.conn(), resource.DefaultNamespace)
|
||||
}
|
||||
v = res.viewFn(res.title, c.app, r, res.colorerFn)
|
||||
c.app.flash(flashInfo, fmt.Sprintf("Viewing %s in namespace %s...", res.title, c.app.config.ActiveNamespace()))
|
||||
if res.enterFn != nil {
|
||||
v.setEnterFn(res.enterFn)
|
||||
}
|
||||
const fmat = "Viewing %s in namespace %s..."
|
||||
c.app.flash(flashInfo, fmt.Sprintf(fmat, res.title, c.app.config.ActiveNamespace()))
|
||||
log.Debug().Msgf("Running command %s", cmd)
|
||||
c.exec(cmd, v)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func (v *helpView) init(_ context.Context, _ string) {
|
|||
}
|
||||
fmt.Fprintf(v, "🏠 [aqua::b]%s\n", "General")
|
||||
for _, h := range general {
|
||||
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
|
||||
v.printHelp(h.key, h.description)
|
||||
}
|
||||
|
||||
navigation := []helpItem{
|
||||
|
|
@ -90,7 +90,7 @@ func (v *helpView) init(_ context.Context, _ string) {
|
|||
}
|
||||
fmt.Fprintf(v, "\n🤖 [aqua::b]%s\n", "View Navigation")
|
||||
for _, h := range navigation {
|
||||
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
|
||||
v.printHelp(h.key, h.description)
|
||||
}
|
||||
|
||||
views := []helpItem{
|
||||
|
|
@ -99,10 +99,13 @@ func (v *helpView) init(_ context.Context, _ string) {
|
|||
}
|
||||
fmt.Fprintf(v, "️️\n😱 [aqua::b]%s\n", "Help")
|
||||
for _, h := range views {
|
||||
fmt.Fprintf(v, "[pink::b]%9s [gray::]%s\n", h.key, h.description)
|
||||
v.printHelp(h.key, h.description)
|
||||
}
|
||||
v.app.setHints(v.hints())
|
||||
}
|
||||
|
||||
v.app.setHints(v.hints())
|
||||
func (v *helpView) printHelp(key, desc string) {
|
||||
fmt.Fprintf(v, "[pink::b]%9s [white::]%s\n", key, desc)
|
||||
}
|
||||
|
||||
func (v *helpView) hints() hints {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ func deltas(c, n string) string {
|
|||
if len(c) == 0 {
|
||||
return n
|
||||
}
|
||||
|
||||
switch strings.Compare(c, n) {
|
||||
case 1, -1:
|
||||
return delta(n)
|
||||
|
|
@ -76,6 +77,7 @@ func numerical(s string) (int, bool) {
|
|||
func delta(s string) string {
|
||||
return suffix(s, "𝜟")
|
||||
}
|
||||
|
||||
func plus(s string) string {
|
||||
return suffix(s, "+")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,363 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
clusterRole roleKind = iota
|
||||
role
|
||||
|
||||
all = "*"
|
||||
rbacTitle = "RBAC"
|
||||
rbacTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])"
|
||||
)
|
||||
|
||||
type (
|
||||
roleKind = int8
|
||||
|
||||
rbacView struct {
|
||||
*tableView
|
||||
|
||||
current igniter
|
||||
cancel context.CancelFunc
|
||||
roleType roleKind
|
||||
roleName string
|
||||
cache resource.RowEvents
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
rbacHeaderVerbs = resource.Row{
|
||||
"GET ",
|
||||
"LIST ",
|
||||
"DLIST ",
|
||||
"WATCH ",
|
||||
"CREATE",
|
||||
"PATCH ",
|
||||
"UPDATE",
|
||||
"DELETE",
|
||||
"EXTRAS",
|
||||
}
|
||||
rbacHeader = append(resource.Row{"NAME", "GROUP"}, rbacHeaderVerbs...)
|
||||
|
||||
k8sVerbs = []string{
|
||||
"get",
|
||||
"list",
|
||||
"deletecollection",
|
||||
"watch",
|
||||
"create",
|
||||
"patch",
|
||||
"update",
|
||||
"delete",
|
||||
}
|
||||
|
||||
httpVerbs = []string{
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"patch",
|
||||
"delete",
|
||||
"options",
|
||||
}
|
||||
|
||||
httpTok8sVerbs = map[string]string{
|
||||
"post": "create",
|
||||
"put": "update",
|
||||
}
|
||||
)
|
||||
|
||||
func newRBACView(app *appView, ns, name string, kind roleKind) *rbacView {
|
||||
v := 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.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)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
v.cancel = cancel
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug().Msg("RBAC Watch bailing out!")
|
||||
return
|
||||
case <-time.After(time.Duration(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) getTitle() string {
|
||||
title := "ClusterRole"
|
||||
if v.roleType == role {
|
||||
title = "Role"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(rbacTitleFmt, title, v.roleName)
|
||||
}
|
||||
|
||||
func (v *rbacView) refresh() {
|
||||
data, err := v.reconcile(v.currentNS, v.roleName, v.roleType)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Unable to reconcile for %s:%d", v.roleName, v.roleType)
|
||||
}
|
||||
v.update(data)
|
||||
}
|
||||
|
||||
func (v *rbacView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if !v.cmdBuff.empty() {
|
||||
v.cmdBuff.reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.backCmd(evt)
|
||||
}
|
||||
|
||||
func (v *rbacView) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
if v.cancel != nil {
|
||||
v.cancel()
|
||||
}
|
||||
|
||||
if v.cmdBuff.isActive() {
|
||||
v.cmdBuff.reset()
|
||||
} else {
|
||||
v.app.inject(v.current)
|
||||
}
|
||||
|
||||
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 {
|
||||
return resource.TableData{}, err
|
||||
}
|
||||
|
||||
data := resource.TableData{
|
||||
Header: rbacHeader,
|
||||
Rows: make(resource.RowEvents, len(evts)),
|
||||
Namespace: resource.NotNamespaced,
|
||||
}
|
||||
|
||||
noDeltas := make(resource.Row, len(rbacHeader))
|
||||
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 *rbacView) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) {
|
||||
var (
|
||||
evts resource.RowEvents
|
||||
err error
|
||||
)
|
||||
|
||||
switch kind {
|
||||
case clusterRole:
|
||||
evts, err = v.clusterPolicies(name)
|
||||
case role:
|
||||
evts, err = v.namespacedPolicies(name)
|
||||
default:
|
||||
return evts, fmt.Errorf("Expecting clusterrole/role but found %d", kind)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Unable to load CR")
|
||||
return evts, err
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.parseRules(cr.Rules), nil
|
||||
}
|
||||
|
||||
func (v *rbacView) namespacedPolicies(path string) (resource.RowEvents, error) {
|
||||
ns, na := namespaced(path)
|
||||
cr, err := v.app.conn().DialOrDie().Rbac().Roles(ns).Get(na, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.parseRules(cr.Rules), nil
|
||||
}
|
||||
|
||||
func (*rbacView) parseRules(rules []rbacv1.PolicyRule) resource.RowEvents {
|
||||
const (
|
||||
nameLen = 60
|
||||
groupLen = 30
|
||||
)
|
||||
|
||||
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[n] = &resource.RowEvent{
|
||||
Fields: makeRow(resource.Pad(n, nameLen), resource.Pad(toGroup(grp), groupLen), asVerbs(r.Verbs...)),
|
||||
}
|
||||
}
|
||||
m[k] = &resource.RowEvent{
|
||||
Fields: makeRow(resource.Pad(k, nameLen), resource.Pad(toGroup(grp), groupLen), asVerbs(r.Verbs...)),
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, nres := range r.NonResourceURLs {
|
||||
if nres[0] != '/' {
|
||||
nres = "/" + nres
|
||||
}
|
||||
m[nres] = &resource.RowEvent{
|
||||
Fields: makeRow(resource.Pad(nres, nameLen), resource.Pad(resource.NAValue, groupLen), asVerbs(r.Verbs...)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func makeRow(res, group string, verbs []string) resource.Row {
|
||||
r := make(resource.Row, 0, 12)
|
||||
r = append(r, res, group)
|
||||
|
||||
return append(r, verbs...)
|
||||
}
|
||||
|
||||
func asVerbs(verbs ...string) resource.Row {
|
||||
const (
|
||||
verbLen = 4
|
||||
unknownLen = 30
|
||||
)
|
||||
|
||||
r := make(resource.Row, 0, len(k8sVerbs)+1)
|
||||
for _, v := range k8sVerbs {
|
||||
r = append(r, resource.Pad(toVerbIcon(hasVerb(verbs, v)), 4))
|
||||
}
|
||||
|
||||
var unknowns []string
|
||||
for _, v := range verbs {
|
||||
if hv, ok := httpTok8sVerbs[v]; ok {
|
||||
v = hv
|
||||
}
|
||||
if !hasVerb(k8sVerbs, v) && v != all {
|
||||
unknowns = append(unknowns, v)
|
||||
}
|
||||
}
|
||||
|
||||
return append(r, resource.Truncate(strings.Join(unknowns, ","), unknownLen))
|
||||
}
|
||||
|
||||
func toVerbIcon(ok bool) string {
|
||||
if ok {
|
||||
return "[green::b] ✓ [::]"
|
||||
}
|
||||
return "[orangered::b] 𐄂 [::]"
|
||||
}
|
||||
|
||||
func hasVerb(verbs []string, verb string) bool {
|
||||
if len(verbs) == 1 && verbs[0] == all {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, v := range verbs {
|
||||
if hv, ok := httpTok8sVerbs[v]; ok {
|
||||
if hv == verb {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if v == verb {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
)
|
||||
|
||||
func TestHasVerb(t *testing.T) {
|
||||
uu := []struct {
|
||||
vv []string
|
||||
v string
|
||||
e bool
|
||||
}{
|
||||
{[]string{"*"}, "get", true},
|
||||
{[]string{"get", "list", "watch"}, "watch", true},
|
||||
{[]string{"get", "dope", "list"}, "watch", false},
|
||||
{[]string{"get"}, "get", true},
|
||||
{[]string{"post"}, "create", true},
|
||||
{[]string{"put"}, "update", true},
|
||||
{[]string{"list", "deletecollection"}, "deletecollection", true},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.e, hasVerb(u.vv, u.v))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsVerbs(t *testing.T) {
|
||||
ok, nok := toVerbIcon(true), toVerbIcon(false)
|
||||
|
||||
uu := []struct {
|
||||
vv []string
|
||||
e resource.Row
|
||||
}{
|
||||
{[]string{"*"}, resource.Row{ok, ok, ok, ok, ok, ok, ok, ok, ""}},
|
||||
{[]string{"get", "list", "patch"}, resource.Row{ok, ok, nok, nok, nok, ok, nok, nok, ""}},
|
||||
{[]string{"get", "list", "deletecollection", "post"}, resource.Row{ok, ok, ok, nok, ok, nok, nok, nok, ""}},
|
||||
{[]string{"get", "list", "blee"}, resource.Row{ok, ok, nok, nok, nok, nok, nok, nok, "blee"}},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.e, asVerbs(u.vv...))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRules(t *testing.T) {
|
||||
ok, nok := toVerbIcon(true), toVerbIcon(false)
|
||||
_ = nok
|
||||
|
||||
uu := []struct {
|
||||
pp []rbacv1.PolicyRule
|
||||
e map[string]resource.Row
|
||||
}{
|
||||
{
|
||||
[]rbacv1.PolicyRule{
|
||||
{APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}},
|
||||
},
|
||||
map[string]resource.Row{
|
||||
"*.*": resource.Row{resource.Pad("*.*", 60), resource.Pad("*", 30), ok, ok, ok, ok, ok, ok, ok, ok, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]rbacv1.PolicyRule{
|
||||
{APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"get"}},
|
||||
},
|
||||
map[string]resource.Row{
|
||||
"*.*": resource.Row{resource.Pad("*.*", 60), resource.Pad("*", 30), ok, nok, nok, nok, nok, nok, nok, nok, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]rbacv1.PolicyRule{
|
||||
{APIGroups: []string{""}, Resources: []string{"*"}, Verbs: []string{"list"}},
|
||||
},
|
||||
map[string]resource.Row{
|
||||
"*": resource.Row{resource.Pad("*", 60), resource.Pad("v1", 30), nok, ok, nok, nok, nok, nok, nok, nok, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]rbacv1.PolicyRule{
|
||||
{APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"list"}, ResourceNames: []string{"fred"}},
|
||||
},
|
||||
map[string]resource.Row{
|
||||
"pods": resource.Row{resource.Pad("pods", 60), resource.Pad("v1", 30), nok, ok, nok, nok, nok, nok, nok, nok, ""},
|
||||
"pods/fred": resource.Row{resource.Pad("pods/fred", 60), resource.Pad("v1", 30), nok, ok, nok, nok, nok, nok, nok, nok, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]rbacv1.PolicyRule{
|
||||
{APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"/fred"}},
|
||||
},
|
||||
map[string]resource.Row{
|
||||
"/fred": resource.Row{resource.Pad("/fred", 60), resource.Pad("<n/a>", 30), ok, nok, nok, nok, nok, nok, nok, nok, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]rbacv1.PolicyRule{
|
||||
{APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"fred"}},
|
||||
},
|
||||
map[string]resource.Row{
|
||||
"/fred": resource.Row{resource.Pad("/fred", 60), resource.Pad("<n/a>", 30), ok, nok, nok, nok, nok, nok, nok, nok, ""},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var v rbacView
|
||||
for _, u := range uu {
|
||||
evts := v.parseRules(u.pp)
|
||||
for k, v := range u.e {
|
||||
assert.Equal(t, v, evts[k].Fields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type (
|
||||
|
|
@ -11,6 +12,7 @@ type (
|
|||
listFn func(c resource.Connection, ns string) resource.List
|
||||
listMxFn func(c resource.Connection, mx resource.MetricsServer, ns string) resource.List
|
||||
colorerFn func(ns string, evt *resource.RowEvent) tcell.Color
|
||||
enterFn func(app *appView, ns, resource, selection string)
|
||||
|
||||
resCmd struct {
|
||||
title string
|
||||
|
|
@ -18,6 +20,7 @@ type (
|
|||
viewFn viewFn
|
||||
listFn listFn
|
||||
listMxFn listMxFn
|
||||
enterFn enterFn
|
||||
colorerFn colorerFn
|
||||
}
|
||||
)
|
||||
|
|
@ -72,6 +75,16 @@ func allCRDs(c k8s.Connection) map[string]k8s.APIGroup {
|
|||
return m
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
app.command.pushCmd("policies")
|
||||
app.inject(newRBACView(app, ns, selection, kind))
|
||||
}
|
||||
|
||||
func resourceViews() map[string]resCmd {
|
||||
return map[string]resCmd{
|
||||
"cm": {
|
||||
|
|
@ -86,6 +99,7 @@ func resourceViews() map[string]resCmd {
|
|||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewClusterRoleList,
|
||||
enterFn: showRBAC,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"crb": {
|
||||
|
|
@ -226,6 +240,7 @@ func resourceViews() map[string]resCmd {
|
|||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewRoleList,
|
||||
enterFn: showRBAC,
|
||||
colorerFn: defaultColorer,
|
||||
},
|
||||
"rs": {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ type (
|
|||
selectedNS string
|
||||
update sync.Mutex
|
||||
list resource.List
|
||||
enterFn enterFn
|
||||
extraActionsFn func(keyActions)
|
||||
selectedFn func() string
|
||||
decorateDataFn func(resource.TableData) resource.TableData
|
||||
|
|
@ -124,9 +125,21 @@ func (v *resourceView) hints() hints {
|
|||
return v.CurrentPage().Item.(hinter).hints()
|
||||
}
|
||||
|
||||
func (v *resourceView) setEnterFn(f enterFn) {
|
||||
v.enterFn = f
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Actions...
|
||||
|
||||
func (v *resourceView) enterCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
v.app.flash(flashInfo, "Enter pressed...")
|
||||
if v.enterFn != nil {
|
||||
v.enterFn(v.app, v.list.GetNamespace(), v.list.GetName(), v.selectedItem)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *resourceView) refreshCmd(*tcell.EventKey) *tcell.EventKey {
|
||||
v.app.flash(flashInfo, "Refreshing...")
|
||||
v.refresh()
|
||||
|
|
@ -359,6 +372,8 @@ func (v *resourceView) refreshActions() {
|
|||
}
|
||||
}
|
||||
|
||||
aa[tcell.KeyEnter] = newKeyAction("Enter", v.enterCmd, true)
|
||||
|
||||
aa[tcell.KeyCtrlR] = newKeyAction("Refresh", v.refreshCmd, false)
|
||||
aa[KeyHelp] = newKeyAction("Help", v.app.noopCmd, false)
|
||||
aa[KeyP] = newKeyAction("Previous", v.app.prevCmd, false)
|
||||
|
|
@ -370,7 +385,7 @@ func (v *resourceView) refreshActions() {
|
|||
aa[tcell.KeyCtrlD] = newKeyAction("Delete", v.deleteCmd, true)
|
||||
}
|
||||
if v.list.Access(resource.ViewAccess) {
|
||||
aa[KeyV] = newKeyAction("View", v.viewCmd, true)
|
||||
aa[KeyY] = newKeyAction("YAML", v.viewCmd, true)
|
||||
}
|
||||
if v.list.Access(resource.DescribeAccess) {
|
||||
aa[KeyD] = newKeyAction("Describe", v.describeCmd, true)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
titleFmt = " [aqua::b]%s[aqua::-]([fuchsia::b]%d[aqua::-]) "
|
||||
titleFmt = " [aqua::b]%s[aqua::-][[fuchsia::b]%d[aqua::-]] "
|
||||
searchFmt = "<[green::b]/%s[aqua::]> "
|
||||
nsTitleFmt = " [aqua::b]%s([fuchsia::b]%s[aqua::-])[aqua::-][[aqua::b]%d[aqua::-]][aqua::-] "
|
||||
)
|
||||
|
|
@ -63,24 +63,8 @@ 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.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", app.puntCmd, false)
|
||||
v.actions[KeyShiftG] = newKeyAction("Bottom", app.puntCmd, false)
|
||||
v.actions[KeyB] = newKeyAction("Down", v.pageDownCmd, false)
|
||||
v.actions[KeyF] = newKeyAction("Up", v.pageUpCmd, false)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +112,7 @@ func (v *tableView) pageDownCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.cmdBuff.setActive(false)
|
||||
v.refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -309,10 +294,11 @@ func (v *tableView) doUpdate(data resource.TableData) {
|
|||
row++
|
||||
|
||||
// for k := range data.Rows {
|
||||
// log.Debug().Msgf("Keys: %s", k)
|
||||
// log.Debug().Msgf("Keys: `%s`", k)
|
||||
// }
|
||||
|
||||
keys := v.sortRows(data)
|
||||
// log.Debug().Msgf("KEYS %#v", keys)
|
||||
groupKeys := map[string][]string{}
|
||||
for _, k := range keys {
|
||||
// log.Debug().Msgf("RKEY: %s", k)
|
||||
|
|
@ -447,6 +433,24 @@ 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) {
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue