356 lines
6.6 KiB
Go
356 lines
6.6 KiB
Go
package view
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/derailed/k9s/internal/resource"
|
|
"github.com/derailed/k9s/internal/ui"
|
|
"github.com/gdamore/tcell"
|
|
"github.com/rs/zerolog/log"
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
)
|
|
|
|
const (
|
|
clusterRole roleKind = iota
|
|
role
|
|
|
|
all = "*"
|
|
rbacTitle = "RBAC"
|
|
rbacTitleFmt = " [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])"
|
|
)
|
|
|
|
var (
|
|
rbacHeaderVerbs = resource.Row{
|
|
"GET ",
|
|
"LIST ",
|
|
"DLIST ",
|
|
"WATCH ",
|
|
"CREATE",
|
|
"PATCH ",
|
|
"UPDATE",
|
|
"DELETE",
|
|
"EXTRAS",
|
|
}
|
|
|
|
rbacHeader = append(resource.Row{"NAME", "API 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",
|
|
}
|
|
)
|
|
|
|
type roleKind = int8
|
|
|
|
// RBAC presents an RBAC policy viewer.
|
|
type RBAC struct {
|
|
*Table
|
|
|
|
app *App
|
|
cancelFn context.CancelFunc
|
|
roleType roleKind
|
|
roleName string
|
|
cache resource.RowEvents
|
|
}
|
|
|
|
// NewRBAC returns a new viewer.
|
|
func NewRBAC(app *App, ns, name string, kind roleKind) *RBAC {
|
|
r := RBAC{
|
|
app: app,
|
|
roleName: name,
|
|
roleType: kind,
|
|
}
|
|
r.Table = NewTable(r.getTitle())
|
|
r.SetActiveNS(ns)
|
|
r.SetColorerFn(rbacColorer)
|
|
r.bindKeys()
|
|
|
|
return &r
|
|
}
|
|
|
|
// Init initializes the view.
|
|
func (r *RBAC) Init(ctx context.Context) {
|
|
r.Table.Init(ctx)
|
|
|
|
r.Start()
|
|
r.SetSortCol(1, len(rbacHeader), true)
|
|
r.refresh()
|
|
}
|
|
|
|
// Start watches for viewer updates
|
|
func (r *RBAC) Start() {
|
|
r.Stop()
|
|
|
|
var ctx context.Context
|
|
ctx, r.cancelFn = context.WithCancel(context.Background())
|
|
|
|
go func(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(time.Duration(r.app.Config.K9s.GetRefreshRate()) * time.Second):
|
|
r.app.QueueUpdateDraw(func() {
|
|
r.refresh()
|
|
})
|
|
}
|
|
}
|
|
}(ctx)
|
|
}
|
|
|
|
// Stop terminates the viewer updater.
|
|
func (r *RBAC) Stop() {
|
|
if r.cancelFn != nil {
|
|
r.cancelFn()
|
|
}
|
|
}
|
|
|
|
// Name returns the component name.
|
|
func (r *RBAC) Name() string {
|
|
return rbacTitle
|
|
}
|
|
|
|
func (r *RBAC) bindKeys() {
|
|
r.RmAction(ui.KeyShiftA)
|
|
|
|
r.AddActions(ui.KeyActions{
|
|
tcell.KeyEscape: ui.NewKeyAction("Reset", r.resetCmd, false),
|
|
ui.KeySlash: ui.NewKeyAction("Filter", r.activateCmd, false),
|
|
ui.KeyP: ui.NewKeyAction("Previous", r.app.PrevCmd, false),
|
|
ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", r.SortColCmd(1), false),
|
|
})
|
|
}
|
|
|
|
func (r *RBAC) getTitle() string {
|
|
return skinTitle(fmt.Sprintf(rbacTitleFmt, rbacTitle, r.roleName), r.app.Styles.Frame())
|
|
}
|
|
|
|
func (r *RBAC) refresh() {
|
|
data, err := r.reconcile(r.ActiveNS(), r.roleName, r.roleType)
|
|
if err != nil {
|
|
log.Error().Err(err).Msgf("Refresh for %s:%d", r.roleName, r.roleType)
|
|
r.app.Flash().Err(err)
|
|
}
|
|
r.Update(data)
|
|
}
|
|
|
|
func (r *RBAC) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
if !r.SearchBuff().Empty() {
|
|
r.SearchBuff().Reset()
|
|
return nil
|
|
}
|
|
|
|
return r.backCmd(evt)
|
|
}
|
|
|
|
func (r *RBAC) backCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
if r.cancelFn != nil {
|
|
r.cancelFn()
|
|
}
|
|
|
|
if r.SearchBuff().IsActive() {
|
|
r.SearchBuff().Reset()
|
|
return nil
|
|
}
|
|
|
|
return r.app.PrevCmd(evt)
|
|
}
|
|
|
|
func (r *RBAC) reconcile(ns, name string, kind roleKind) (resource.TableData, error) {
|
|
var table resource.TableData
|
|
|
|
evts, err := r.rowEvents(ns, name, kind)
|
|
if err != nil {
|
|
return table, err
|
|
}
|
|
|
|
return buildTable(r, evts), nil
|
|
}
|
|
|
|
func (r *RBAC) header() resource.Row {
|
|
return rbacHeader
|
|
}
|
|
|
|
func (r *RBAC) getCache() resource.RowEvents {
|
|
return r.cache
|
|
}
|
|
|
|
func (r *RBAC) setCache(evts resource.RowEvents) {
|
|
r.cache = evts
|
|
}
|
|
|
|
func (r *RBAC) rowEvents(ns, name string, kind roleKind) (resource.RowEvents, error) {
|
|
var (
|
|
evts resource.RowEvents
|
|
err error
|
|
)
|
|
|
|
switch kind {
|
|
case clusterRole:
|
|
evts, err = r.clusterPolicies(name)
|
|
case role:
|
|
evts, err = r.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 (r *RBAC) clusterPolicies(name string) (resource.RowEvents, error) {
|
|
cr, err := r.app.Conn().DialOrDie().RbacV1().ClusterRoles().Get(name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return r.parseRules(cr.Rules), nil
|
|
}
|
|
|
|
func (r *RBAC) namespacedPolicies(path string) (resource.RowEvents, error) {
|
|
ns, na := namespaced(path)
|
|
cr, err := r.app.Conn().DialOrDie().RbacV1().Roles(ns).Get(na, metav1.GetOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return r.parseRules(cr.Rules), nil
|
|
}
|
|
|
|
func (r *RBAC) parseRules(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 := fqn(k, na)
|
|
m[n] = &resource.RowEvent{
|
|
Fields: prepRow(n, grp, r.Verbs),
|
|
}
|
|
}
|
|
m[k] = &resource.RowEvent{
|
|
Fields: prepRow(k, grp, r.Verbs),
|
|
}
|
|
}
|
|
}
|
|
for _, nres := range r.NonResourceURLs {
|
|
if nres[0] != '/' {
|
|
nres = "/" + nres
|
|
}
|
|
m[nres] = &resource.RowEvent{
|
|
Fields: prepRow(nres, resource.NAValue, r.Verbs),
|
|
}
|
|
}
|
|
}
|
|
|
|
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(res, grp, asVerbs(verbs...))
|
|
}
|
|
|
|
func makeRow(res, group string, verbs []string) resource.Row {
|
|
r := make(resource.Row, 0, len(rbacHeader))
|
|
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, toVerbIcon(hasVerb(verbs, v)))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func toGroup(g string) string {
|
|
if g == "" {
|
|
return "v1"
|
|
}
|
|
return g
|
|
}
|