added RBAC support
commit
0a19c0f7e7
|
|
@ -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: arm32
|
||||
arm64: arm64
|
||||
bit: Arm
|
||||
bitv6: Arm6
|
||||
bitv7: Arm7
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
checksum:
|
||||
|
|
@ -63,6 +61,11 @@ brew:
|
|||
# Snapcraft
|
||||
snapcraft:
|
||||
name: k9s
|
||||
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 }}"
|
||||
publish: true
|
||||
replacements:
|
||||
|
|
@ -70,11 +73,9 @@ snapcraft:
|
|||
386: 32-bit
|
||||
darwin: macOS
|
||||
linux: Tux
|
||||
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.
|
||||
bit: Arm
|
||||
bitv6: Arm6
|
||||
bitv7: Arm7
|
||||
grade: stable
|
||||
confinement: strict
|
||||
apps:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ for changes and offers subsequent commands to interact with observed resources.
|
|||
[](https://goreportcard.com/report/github.com/derailed/k9s)
|
||||
[](https://travis-ci.com/derailed/k9s)
|
||||
[](https://github.com/derailed/k9s/releases)
|
||||
[](https://snapcraft.io/k9s)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
# Release v0.4.0
|
||||
|
||||
## Notes
|
||||
|
||||
Thank you to all that contributed with flushing out issues with K9s! I'll try
|
||||
to mark some of these issues as fixed. But if you don't mind grab the latest
|
||||
rev and see if we're happier with some of the fixes!
|
||||
|
||||
If you've filed an issue please help me verify and close.
|
||||
|
||||
Thank you so much for your support and awesome suggestions to make K9s better!!
|
||||
|
||||
---
|
||||
|
||||
## Change Logs
|
||||
|
||||
> NOTE! Lots of changes here, please report any disturbances in the force. Thank you!
|
||||
> NOTE!: This feature is very much an alpha feature right now.
|
||||
> I find it really powerful and useful, hopefully I am not the only to be hunanimous on that??
|
||||
|
||||
1. [Feature #82](https://github.com/derailed/k9s/issues/82)
|
||||
1. Added ability to view RBAC policies while in clusterrole or role view.
|
||||
2. The RBAC view will auto-refresh just like any K9s views hence showing live RBAC updates
|
||||
3. RBAC view supports standard K8s verbs ie get,list,deletecollection,watch,create,patch,update,delete.
|
||||
4. Any verbs not in this standard K8s verb list, will end up in the EXTRAS column.
|
||||
5. For non resource URLS, we map standard REST verbs to K8s verbs ie post=create patch=update, etc..
|
||||
6. Added initial sorts by name and group while in RBAC view.
|
||||
7. Usage: To activate, enter command mode via `:cr` or `:ro` for clusterole(cr)/role(ro), select a row and press `<enter>`
|
||||
8. To bail out of the view and return to previous use `p` or `<esc>`
|
||||
2. One feature that was mentioned in the comments for the RBAC feature above Tx [faheem-cliqz](https://github.com/faheem-cliqz)! was the ability to check RBAC rules for a given user. Namely reverse RBAC lookup
|
||||
1. Added a new view, code name *Fu* view to show all the clusterroles/roles associated with a given user.
|
||||
2. The view also supports for checking RBAC Fu for a user, a group or an app via a serviceaccount.
|
||||
3. To activate: Enter command mode via `:fu` followed by u|g|s:subject + `<enter>`.
|
||||
For example: To view user *fred* Fu enter `:fu u:fred` + `<enter>` will show all clusterroles/roles and verbs associated
|
||||
with the user *fred*
|
||||
4. For group Fu lookup, use the same command as above and substitute `u:fred` with `g:fred`
|
||||
5. For ServiceAccount *fred* Fu check: use `s:fred`
|
||||
3. Eliminated jitter while scrolling tables
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Resolved Bugs
|
||||
|
||||
+ None
|
||||
|
|
@ -72,7 +72,7 @@ func (r *ClusterRole) Fields(ns string) Row {
|
|||
i := r.instance
|
||||
|
||||
return append(ff,
|
||||
Pad(i.Name, 70),
|
||||
i.Name,
|
||||
toAge(i.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package resource
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/rbac/v1"
|
||||
|
|
@ -63,15 +65,54 @@ func (r *ClusterRoleBinding) Marshal(path string) (string, error) {
|
|||
|
||||
// Header return resource header.
|
||||
func (*ClusterRoleBinding) Header(_ string) Row {
|
||||
return append(Row{}, "NAME", "AGE")
|
||||
return append(Row{}, "NAME", "ROLE", "KIND", "SUBJECTS", "AGE")
|
||||
}
|
||||
|
||||
// Fields retrieves displayable fields.
|
||||
func (r *ClusterRoleBinding) Fields(ns string) Row {
|
||||
ff := make(Row, 0, len(r.Header(ns)))
|
||||
|
||||
i := r.instance
|
||||
kind, ss := renderSubjects(i.Subjects)
|
||||
|
||||
return append(ff,
|
||||
r.instance.Name,
|
||||
toAge(r.instance.ObjectMeta.CreationTimestamp),
|
||||
i.Name,
|
||||
i.RoleRef.Name,
|
||||
kind,
|
||||
ss,
|
||||
toAge(i.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func renderSubjects(ss []v1.Subject) (kind string, subjects string) {
|
||||
if len(ss) == 0 {
|
||||
return NAValue, ""
|
||||
}
|
||||
|
||||
var tt []string
|
||||
for _, s := range ss {
|
||||
kind = toSubjectAlias(s.Kind)
|
||||
tt = append(tt, s.Name)
|
||||
}
|
||||
return kind, strings.Join(tt, ",")
|
||||
}
|
||||
|
||||
func toSubjectAlias(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
switch s {
|
||||
case v1.UserKind:
|
||||
return "USR"
|
||||
case v1.GroupKind:
|
||||
return "GRP"
|
||||
case v1.ServiceAccountKind:
|
||||
return "SA"
|
||||
default:
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func TestCRBListData(t *testing.T) {
|
|||
assert.Equal(t, 1, len(td.Rows))
|
||||
assert.Equal(t, resource.NotNamespaced, l.GetNamespace())
|
||||
row := td.Rows["fred"]
|
||||
assert.Equal(t, 2, len(row.Deltas))
|
||||
assert.Equal(t, 5, len(row.Deltas))
|
||||
for _, d := range row.Deltas {
|
||||
assert.Equal(t, "", d)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package resource_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -42,12 +41,12 @@ func TestCRListAccess(t *testing.T) {
|
|||
|
||||
func TestCRFields(t *testing.T) {
|
||||
r := newClusterRole().Fields("blee")
|
||||
assert.Equal(t, "fred"+strings.Repeat(" ", 66), r[0])
|
||||
assert.Equal(t, "fred", r[0])
|
||||
}
|
||||
|
||||
func TestCRFieldsAllNS(t *testing.T) {
|
||||
r := newClusterRole().Fields(resource.AllNamespaces)
|
||||
assert.Equal(t, "fred"+strings.Repeat(" ", 66), r[0])
|
||||
assert.Equal(t, "fred", r[0])
|
||||
}
|
||||
|
||||
func TestCRMarshal(t *testing.T) {
|
||||
|
|
@ -85,7 +84,7 @@ func TestCRListData(t *testing.T) {
|
|||
for _, d := range row.Deltas {
|
||||
assert.Equal(t, "", d)
|
||||
}
|
||||
assert.Equal(t, resource.Row{"fred" + strings.Repeat(" ", 66)}, row.Fields[:1])
|
||||
assert.Equal(t, resource.Row{"fred"}, row.Fields[:1])
|
||||
}
|
||||
|
||||
// Helpers...
|
||||
|
|
|
|||
|
|
@ -90,13 +90,6 @@ func toAge(timestamp metav1.Time) string {
|
|||
return duration.HumanDuration(time.Since(timestamp.Time))
|
||||
}
|
||||
|
||||
// Pad a string up to the given length.
|
||||
func Pad(s string, l int) string {
|
||||
fmat := "%-" + strconv.Itoa(l) + "s"
|
||||
|
||||
return fmt.Sprintf(fmat, s)
|
||||
}
|
||||
|
||||
// Truncate a string to the given l and suffix ellipsis if needed.
|
||||
func Truncate(str string, width int) string {
|
||||
return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))
|
||||
|
|
|
|||
|
|
@ -77,22 +77,6 @@ func TestNa(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPad(t *testing.T) {
|
||||
uu := []struct {
|
||||
s string
|
||||
l int
|
||||
e string
|
||||
}{
|
||||
{"fred", 10, "fred "},
|
||||
{"fred", 6, "fred "},
|
||||
{"fred", 4, "fred"},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.e, Pad(u.s, u.l))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncate(t *testing.T) {
|
||||
uu := []struct {
|
||||
s string
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -211,7 +210,7 @@ func (r *Pod) Fields(ns string) Row {
|
|||
cr, _, rc := r.statuses(ss)
|
||||
|
||||
return append(ff,
|
||||
Pad(i.ObjectMeta.Name, podNameSize),
|
||||
i.ObjectMeta.Name,
|
||||
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,17 @@ 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, "fred", r[0])
|
||||
}
|
||||
|
||||
func TestPodMarshal(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package resource
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/rbac/v1"
|
||||
|
|
@ -70,49 +68,25 @@ func (*RoleBinding) Header(ns string) Row {
|
|||
hh = append(hh, "NAMESPACE")
|
||||
}
|
||||
|
||||
return append(hh, "NAME", "ROLE", "SUBJECTS", "AGE")
|
||||
return append(hh, "NAME", "ROLE", "KIND", "SUBJECTS", "AGE")
|
||||
}
|
||||
|
||||
// Fields retrieves displayable fields.
|
||||
func (r *RoleBinding) Fields(ns string) Row {
|
||||
ff := make(Row, 0, len(r.Header(ns)))
|
||||
i := r.instance
|
||||
|
||||
ff := make(Row, 0, len(r.Header(ns)))
|
||||
if ns == AllNamespaces {
|
||||
ff = append(ff, i.Namespace)
|
||||
}
|
||||
|
||||
kind, ss := renderSubjects(i.Subjects)
|
||||
|
||||
return append(ff,
|
||||
i.Name,
|
||||
i.RoleRef.Name,
|
||||
r.toSubjects(i.Subjects),
|
||||
kind,
|
||||
ss,
|
||||
toAge(i.ObjectMeta.CreationTimestamp),
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helpers...
|
||||
|
||||
func (r *RoleBinding) toSubjects(ss []v1.Subject) string {
|
||||
var acc string
|
||||
for i, s := range ss {
|
||||
acc += s.Name + "/" + r.toSubjectAlias(s.Kind)
|
||||
if i < len(ss)-1 {
|
||||
acc += ","
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
func (r *RoleBinding) toSubjectAlias(s string) string {
|
||||
switch s {
|
||||
case v1.UserKind:
|
||||
return "USR"
|
||||
case v1.GroupKind:
|
||||
return "GRP"
|
||||
case v1.ServiceAccountKind:
|
||||
return "SA"
|
||||
default:
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import (
|
|||
)
|
||||
|
||||
func TestToSubjectAlias(t *testing.T) {
|
||||
r := RoleBinding{}
|
||||
|
||||
uu := []struct {
|
||||
i string
|
||||
e string
|
||||
|
|
@ -20,38 +18,44 @@ func TestToSubjectAlias(t *testing.T) {
|
|||
{"fred", "FRED"},
|
||||
}
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.e, r.toSubjectAlias(u.i))
|
||||
assert.Equal(t, u.e, toSubjectAlias(u.i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestToSubjects(t *testing.T) {
|
||||
r := RoleBinding{}
|
||||
|
||||
func TestRenderSubjects(t *testing.T) {
|
||||
uu := []struct {
|
||||
i []rbacv1.Subject
|
||||
e string
|
||||
ss []rbacv1.Subject
|
||||
ek string
|
||||
e string
|
||||
}{
|
||||
{
|
||||
[]rbacv1.Subject{
|
||||
{Name: "blee", Kind: rbacv1.UserKind},
|
||||
},
|
||||
"blee/USR",
|
||||
"USR",
|
||||
"blee",
|
||||
},
|
||||
{
|
||||
[]rbacv1.Subject{},
|
||||
"<n/a>",
|
||||
"",
|
||||
},
|
||||
}
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.e, r.toSubjects(u.i))
|
||||
kind, ss := renderSubjects(u.ss)
|
||||
assert.Equal(t, u.e, ss)
|
||||
assert.Equal(t, u.ek, kind)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToSubjects(b *testing.B) {
|
||||
var r RoleBinding
|
||||
ss := []rbacv1.Subject{
|
||||
{Name: "blee", Kind: rbacv1.UserKind},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
r.toSubjects(ss)
|
||||
renderSubjects(ss)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func TestRBListData(t *testing.T) {
|
|||
assert.Equal(t, 1, len(td.Rows))
|
||||
assert.Equal(t, "blee", l.GetNamespace())
|
||||
row := td.Rows["blee/fred"]
|
||||
assert.Equal(t, 4, len(row.Deltas))
|
||||
assert.Equal(t, 5, len(row.Deltas))
|
||||
for _, d := range row.Deltas {
|
||||
assert.Equal(t, "", d)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,13 +75,13 @@ func (*ReplicaSet) Header(ns string) Row {
|
|||
|
||||
// Fields retrieves displayable fields.
|
||||
func (r *ReplicaSet) Fields(ns string) Row {
|
||||
i := r.instance
|
||||
|
||||
ff := make(Row, 0, len(r.Header(ns)))
|
||||
if ns == AllNamespaces {
|
||||
ff = append(ff, r.instance.Namespace)
|
||||
ff = append(ff, i.Namespace)
|
||||
}
|
||||
|
||||
i := r.instance
|
||||
|
||||
return append(ff,
|
||||
i.Name,
|
||||
strconv.Itoa(int(*i.Spec.Replicas)),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -138,9 +124,9 @@ func (v *aliasView) hydrate() resource.TableData {
|
|||
|
||||
for k := range cmds {
|
||||
fields := resource.Row{
|
||||
resource.Pad(k, 30),
|
||||
resource.Pad(cmds[k].title, 30),
|
||||
resource.Pad(cmds[k].api, 30),
|
||||
pad(k, 30),
|
||||
pad(cmds[k].title, 30),
|
||||
pad(cmds[k].api, 30),
|
||||
}
|
||||
data.Rows[k] = &resource.RowEvent{
|
||||
Action: resource.New,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type (
|
|||
|
||||
igniter interface {
|
||||
tview.Primitive
|
||||
|
||||
getTitle() string
|
||||
init(ctx context.Context, ns string)
|
||||
}
|
||||
|
|
@ -30,6 +31,10 @@ type (
|
|||
|
||||
resourceViewer interface {
|
||||
igniter
|
||||
|
||||
setEnterFn(enterFn)
|
||||
setColorerFn(colorerFn)
|
||||
setDecorateFn(decorateFn)
|
||||
}
|
||||
|
||||
appView struct {
|
||||
|
|
@ -90,6 +95,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 +163,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
|
||||
|
|
@ -263,7 +275,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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package views
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/derailed/k9s/internal/k8s"
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
|
|
@ -36,32 +37,50 @@ 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 igniter
|
||||
switch cmd {
|
||||
case "q", "quit":
|
||||
var v resourceViewer
|
||||
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
|
||||
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()))
|
||||
v = res.viewFn(res.title, c.app, r)
|
||||
if res.colorerFn != nil {
|
||||
v.setColorerFn(res.colorerFn)
|
||||
}
|
||||
if res.enterFn != nil {
|
||||
v.setEnterFn(res.enterFn)
|
||||
}
|
||||
if res.decorateFn != nil {
|
||||
v.setDecorateFn(res.decorateFn)
|
||||
}
|
||||
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
|
||||
|
|
@ -74,16 +93,16 @@ func (c *command) run(cmd string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
n := res.Plural
|
||||
if len(n) == 0 {
|
||||
n = res.Singular
|
||||
name := res.Plural
|
||||
if len(name) == 0 {
|
||||
name = res.Singular
|
||||
}
|
||||
v = newResourceView(
|
||||
res.Kind,
|
||||
c.app,
|
||||
resource.NewCustomList(c.app.conn(), "", res.Group, res.Version, n),
|
||||
defaultColorer,
|
||||
resource.NewCustomList(c.app.conn(), "", res.Group, res.Version, name),
|
||||
)
|
||||
v.setColorerFn(defaultColorer)
|
||||
c.exec(cmd, v)
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ type contextView struct {
|
|||
*resourceView
|
||||
}
|
||||
|
||||
func newContextView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
|
||||
v := contextView{newResourceView(t, app, list, c).(*resourceView)}
|
||||
func newContextView(t string, app *appView, list resource.List) resourceViewer {
|
||||
v := contextView{newResourceView(t, app, list).(*resourceView)}
|
||||
{
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.getTV().cleanseFn = v.cleanser
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ type cronJobView struct {
|
|||
*resourceView
|
||||
}
|
||||
|
||||
func newCronJobView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
|
||||
func newCronJobView(t string, app *appView, list resource.List) resourceViewer {
|
||||
v := cronJobView{
|
||||
resourceView: newResourceView(t, app, list, c).(*resourceView),
|
||||
resourceView: newResourceView(t, app, list).(*resourceView),
|
||||
}
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.switchPage("cronjob")
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ func (v *cronJobView) trigger(evt *tcell.EventKey) *tcell.EventKey {
|
|||
v.app.flash(flashErr, "Boom!", err.Error())
|
||||
return evt
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,324 @@
|
|||
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) {
|
||||
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]
|
||||
}
|
||||
|
||||
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(ns, res, grp, 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,12 +99,15 @@ 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())
|
||||
}
|
||||
|
||||
func (v *helpView) printHelp(key, desc string) {
|
||||
fmt.Fprintf(v, "[pink::b]%9s [white::]%s\n", key, desc)
|
||||
}
|
||||
|
||||
func (v *helpView) hints() hints {
|
||||
return v.actions.toHints()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ func toPerc(f float64) string {
|
|||
}
|
||||
|
||||
func deltas(c, n string) string {
|
||||
c, n = strings.TrimSpace(c), strings.TrimSpace(n)
|
||||
|
||||
// log.Debug().Msgf("`%s` vs `%s`", c, n)
|
||||
|
||||
if c == "n/a" {
|
||||
return n
|
||||
}
|
||||
|
|
@ -46,6 +50,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 +81,7 @@ func numerical(s string) (int, bool) {
|
|||
func delta(s string) string {
|
||||
return suffix(s, "𝜟")
|
||||
}
|
||||
|
||||
func plus(s string) string {
|
||||
return suffix(s, "+")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ type jobView struct {
|
|||
*resourceView
|
||||
}
|
||||
|
||||
func newJobView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
|
||||
v := jobView{newResourceView(t, app, list, c).(*resourceView)}
|
||||
func newJobView(t string, app *appView, list resource.List) resourceViewer {
|
||||
v := jobView{newResourceView(t, app, list).(*resourceView)}
|
||||
{
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.AddPage("logs", newLogsView(&v), true, false)
|
||||
v.switchPage("job")
|
||||
}
|
||||
v.AddPage("logs", newLogsView(&v), true, false)
|
||||
v.switchPage("job")
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
|
|
@ -60,6 +61,7 @@ func (v *jobView) logs(evt *tcell.EventKey) *tcell.EventKey {
|
|||
|
||||
v.switchPage("logs")
|
||||
l.init()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,16 @@ type namespaceView struct {
|
|||
*resourceView
|
||||
}
|
||||
|
||||
func newNamespaceView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
|
||||
v := namespaceView{newResourceView(t, app, list, c).(*resourceView)}
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.selectedFn = v.getSelectedItem
|
||||
v.decorateDataFn = v.decorate
|
||||
v.getTV().cleanseFn = v.cleanser
|
||||
v.switchPage("ns")
|
||||
func newNamespaceView(t string, app *appView, list resource.List) resourceViewer {
|
||||
v := namespaceView{newResourceView(t, app, list).(*resourceView)}
|
||||
{
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.selectedFn = v.getSelectedItem
|
||||
v.decorateFn = v.decorate
|
||||
v.getTV().cleanseFn = v.cleanser
|
||||
v.switchPage("ns")
|
||||
}
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
|
|
@ -44,6 +47,7 @@ func (v *namespaceView) switchNsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
v.useNamespace(v.getSelectedItem())
|
||||
v.app.gotoResource("po", true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +56,7 @@ func (v *namespaceView) useNsCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
return evt
|
||||
}
|
||||
v.useNamespace(v.getSelectedItem())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -92,5 +97,6 @@ func (v *namespaceView) decorate(data resource.TableData) resource.TableData {
|
|||
r.Action = resource.Unchanged
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ type nodeView struct {
|
|||
*resourceView
|
||||
}
|
||||
|
||||
func newNodeView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
|
||||
v := nodeView{newResourceView(t, app, list, c).(*resourceView)}
|
||||
func newNodeView(t string, app *appView, list resource.List) resourceViewer {
|
||||
v := nodeView{newResourceView(t, app, list).(*resourceView)}
|
||||
{
|
||||
v.extraActionsFn = v.extraActions
|
||||
v.switchPage("no")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
)
|
||||
|
||||
type maxyPad []int
|
||||
|
||||
func computeMaxColumns(pads maxyPad, sortCol int, table resource.TableData) {
|
||||
for index, h := range table.Header {
|
||||
pads[index] = len(h)
|
||||
if index == sortCol {
|
||||
pads[index] = len(h) + 2
|
||||
}
|
||||
}
|
||||
|
||||
var row int
|
||||
for _, rev := range table.Rows {
|
||||
for index, field := range rev.Fields {
|
||||
if len(field) > pads[index] && isASCII(field) {
|
||||
pads[index] = len([]rune(field))
|
||||
}
|
||||
}
|
||||
row++
|
||||
}
|
||||
}
|
||||
|
||||
func isASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Pad a string up to the given length or truncates if greater than length.
|
||||
func pad(s string, width int) string {
|
||||
if len(s) == width {
|
||||
return s
|
||||
}
|
||||
|
||||
if len(s) > width {
|
||||
return resource.Truncate(s, width)
|
||||
}
|
||||
|
||||
return s + strings.Repeat(" ", width-len(s))
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMaxColumn(t *testing.T) {
|
||||
uu := []struct {
|
||||
t resource.TableData
|
||||
s int
|
||||
e maxyPad
|
||||
}{
|
||||
{
|
||||
resource.TableData{
|
||||
Header: resource.Row{"A", "B"},
|
||||
Rows: resource.RowEvents{
|
||||
"r1": &resource.RowEvent{Fields: resource.Row{"hello", "world"}},
|
||||
"r2": &resource.RowEvent{Fields: resource.Row{"yo", "mama"}},
|
||||
},
|
||||
},
|
||||
0,
|
||||
maxyPad{5, 5},
|
||||
},
|
||||
{
|
||||
resource.TableData{
|
||||
Header: resource.Row{"A", "B"},
|
||||
Rows: resource.RowEvents{
|
||||
"r1": &resource.RowEvent{Fields: resource.Row{"hello", "world"}},
|
||||
"r2": &resource.RowEvent{Fields: resource.Row{"yo", "mama"}},
|
||||
},
|
||||
},
|
||||
1,
|
||||
maxyPad{5, 5},
|
||||
},
|
||||
{
|
||||
resource.TableData{
|
||||
Header: resource.Row{"A", "B"},
|
||||
Rows: resource.RowEvents{
|
||||
"r1": &resource.RowEvent{Fields: resource.Row{"Hello World lord of ipsums 😅", "world"}},
|
||||
"r2": &resource.RowEvent{Fields: resource.Row{"o", "mama"}},
|
||||
},
|
||||
},
|
||||
0,
|
||||
maxyPad{3, 5},
|
||||
},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
pads := make(maxyPad, len(u.t.Header))
|
||||
computeMaxColumns(pads, u.s, u.t)
|
||||
assert.Equal(t, u.e, pads)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsASCII(t *testing.T) {
|
||||
uu := []struct {
|
||||
s string
|
||||
e bool
|
||||
}{
|
||||
{"hello", true},
|
||||
{"Yo! 😄", false},
|
||||
{"😄", false},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.e, isASCII(u.s))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPad(t *testing.T) {
|
||||
uu := []struct {
|
||||
s string
|
||||
l int
|
||||
e string
|
||||
}{
|
||||
{"fred", 3, "fr…"},
|
||||
{"01234567890", 10, "012345678…"},
|
||||
{"fred", 10, "fred "},
|
||||
{"fred", 6, "fred "},
|
||||
{"fred", 4, "fred"},
|
||||
}
|
||||
|
||||
for _, u := range uu {
|
||||
assert.Equal(t, u.e, pad(u.s, u.l))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMaxColumn(b *testing.B) {
|
||||
table := resource.TableData{
|
||||
Header: resource.Row{"A", "B"},
|
||||
Rows: resource.RowEvents{
|
||||
"r1": &resource.RowEvent{Fields: resource.Row{"hello", "world"}},
|
||||
"r2": &resource.RowEvent{Fields: resource.Row{"yo", "mama"}},
|
||||
},
|
||||
}
|
||||
|
||||
pads := make(maxyPad, len(table.Header))
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
computeMaxColumns(pads, 0, table)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,8 @@ type loggable interface {
|
|||
switchPage(n string)
|
||||
}
|
||||
|
||||
func newPodView(t string, app *appView, list resource.List, c colorerFn) resourceViewer {
|
||||
v := podView{newResourceView(t, app, list, c).(*resourceView)}
|
||||
func newPodView(t string, app *appView, list resource.List) resourceViewer {
|
||||
v := podView{newResourceView(t, app, list).(*resourceView)}
|
||||
{
|
||||
v.extraActionsFn = v.extraActions
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,379 @@
|
|||
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.colorerFn = rbacColorer
|
||||
v.current = app.content.GetPrimitive("main").(igniter)
|
||||
v.bindKeys()
|
||||
}
|
||||
|
||||
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
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug().Msg("RBAC 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 *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 {
|
||||
title = "Role"
|
||||
}
|
||||
|
||||
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 {
|
||||
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.prevCmd(evt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 (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 (v *rbacView) 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 := 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
|
||||
}
|
||||
|
|
@ -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{"*.*", "*", ok, ok, ok, ok, ok, ok, ok, ok, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]rbacv1.PolicyRule{
|
||||
{APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"get"}},
|
||||
},
|
||||
map[string]resource.Row{
|
||||
"*.*": resource.Row{"*.*", "*", ok, nok, nok, nok, nok, nok, nok, nok, ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]rbacv1.PolicyRule{
|
||||
{APIGroups: []string{""}, Resources: []string{"*"}, Verbs: []string{"list"}},
|
||||
},
|
||||
map[string]resource.Row{
|
||||
"*": resource.Row{"*", "v1", 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{"pods", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""},
|
||||
"pods/fred": resource.Row{"pods/fred", "v1", 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{"/fred", "<n/a>", 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{"/fred", "<n/a>", 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,18 +7,22 @@ import (
|
|||
)
|
||||
|
||||
type (
|
||||
viewFn func(ns string, app *appView, list resource.List, colorer colorerFn) resourceViewer
|
||||
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
|
||||
viewFn func(ns string, app *appView, list resource.List) resourceViewer
|
||||
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)
|
||||
decorateFn func(resource.TableData) resource.TableData
|
||||
|
||||
resCmd struct {
|
||||
title string
|
||||
api string
|
||||
viewFn viewFn
|
||||
listFn listFn
|
||||
listMxFn listMxFn
|
||||
colorerFn colorerFn
|
||||
title string
|
||||
api string
|
||||
viewFn viewFn
|
||||
listFn listFn
|
||||
listMxFn listMxFn
|
||||
enterFn enterFn
|
||||
colorerFn colorerFn
|
||||
decorateFn decorateFn
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -72,42 +76,48 @@ func allCRDs(c k8s.Connection) map[string]k8s.APIGroup {
|
|||
return m
|
||||
}
|
||||
|
||||
func showRBAC(app *appView, ns, resource, selection string) {
|
||||
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": {
|
||||
title: "ConfigMaps",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewConfigMapList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "ConfigMaps",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewConfigMapList,
|
||||
},
|
||||
"cr": {
|
||||
title: "ClusterRoles",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewClusterRoleList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "ClusterRoles",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewClusterRoleList,
|
||||
enterFn: showRBAC,
|
||||
},
|
||||
"crb": {
|
||||
title: "ClusterRoleBindings",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewClusterRoleBindingList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "ClusterRoleBindings",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewClusterRoleBindingList,
|
||||
// decorateFn: crbDecorator,
|
||||
},
|
||||
"crd": {
|
||||
title: "CustomResourceDefinitions",
|
||||
api: "apiextensions.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewCRDList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "CustomResourceDefinitions",
|
||||
api: "apiextensions.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewCRDList,
|
||||
},
|
||||
"cj": {
|
||||
title: "CronJobs",
|
||||
api: "batch",
|
||||
viewFn: newCronJobView,
|
||||
listFn: resource.NewCronJobList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "CronJobs",
|
||||
api: "batch",
|
||||
viewFn: newCronJobView,
|
||||
listFn: resource.NewCronJobList,
|
||||
},
|
||||
"ctx": {
|
||||
title: "Contexts",
|
||||
|
|
@ -131,11 +141,10 @@ func resourceViews() map[string]resCmd {
|
|||
colorerFn: dpColorer,
|
||||
},
|
||||
"ep": {
|
||||
title: "EndPoints",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewEndpointsList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "EndPoints",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewEndpointsList,
|
||||
},
|
||||
"ev": {
|
||||
title: "Events",
|
||||
|
|
@ -145,25 +154,22 @@ func resourceViews() map[string]resCmd {
|
|||
colorerFn: evColorer,
|
||||
},
|
||||
"hpa": {
|
||||
title: "HorizontalPodAutoscalers",
|
||||
api: "autoscaling",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewHPAList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "HorizontalPodAutoscalers",
|
||||
api: "autoscaling",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewHPAList,
|
||||
},
|
||||
"ing": {
|
||||
title: "Ingress",
|
||||
api: "extensions",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewIngressList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "Ingress",
|
||||
api: "extensions",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewIngressList,
|
||||
},
|
||||
"jo": {
|
||||
title: "Jobs",
|
||||
api: "batch",
|
||||
viewFn: newJobView,
|
||||
listFn: resource.NewJobList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "Jobs",
|
||||
api: "batch",
|
||||
viewFn: newJobView,
|
||||
listFn: resource.NewJobList,
|
||||
},
|
||||
"no": {
|
||||
title: "Nodes",
|
||||
|
|
@ -208,11 +214,10 @@ func resourceViews() map[string]resCmd {
|
|||
colorerFn: pvcColorer,
|
||||
},
|
||||
"rb": {
|
||||
title: "RoleBindings",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewRoleBindingList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "RoleBindings",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewRoleBindingList,
|
||||
},
|
||||
"rc": {
|
||||
title: "ReplicationControllers",
|
||||
|
|
@ -222,11 +227,11 @@ func resourceViews() map[string]resCmd {
|
|||
colorerFn: rsColorer,
|
||||
},
|
||||
"ro": {
|
||||
title: "Roles",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewRoleList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "Roles",
|
||||
api: "rbac.authorization.k8s.io",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewRoleList,
|
||||
enterFn: showRBAC,
|
||||
},
|
||||
"rs": {
|
||||
title: "ReplicaSets",
|
||||
|
|
@ -236,18 +241,16 @@ func resourceViews() map[string]resCmd {
|
|||
colorerFn: rsColorer,
|
||||
},
|
||||
"sa": {
|
||||
title: "ServiceAccounts",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewServiceAccountList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "ServiceAccounts",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewServiceAccountList,
|
||||
},
|
||||
"sec": {
|
||||
title: "Secrets",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewSecretList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "Secrets",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewSecretList,
|
||||
},
|
||||
"sts": {
|
||||
title: "StatefulSets",
|
||||
|
|
@ -257,11 +260,11 @@ func resourceViews() map[string]resCmd {
|
|||
colorerFn: stsColorer,
|
||||
},
|
||||
"svc": {
|
||||
title: "Services",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewServiceList,
|
||||
colorerFn: defaultColorer,
|
||||
title: "Services",
|
||||
api: "",
|
||||
viewFn: newResourceView,
|
||||
listFn: resource.NewServiceList,
|
||||
// decorateFn: svcDecorator,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,13 +42,15 @@ type (
|
|||
selectedNS string
|
||||
update sync.Mutex
|
||||
list resource.List
|
||||
enterFn enterFn
|
||||
extraActionsFn func(keyActions)
|
||||
selectedFn func() string
|
||||
decorateDataFn func(resource.TableData) resource.TableData
|
||||
decorateFn decorateFn
|
||||
colorerFn colorerFn
|
||||
}
|
||||
)
|
||||
|
||||
func newResourceView(title string, app *appView, list resource.List, c colorerFn) resourceViewer {
|
||||
func newResourceView(title string, app *appView, list resource.List) resourceViewer {
|
||||
v := resourceView{
|
||||
app: app,
|
||||
title: title,
|
||||
|
|
@ -59,7 +61,6 @@ func newResourceView(title string, app *appView, list resource.List, c colorerFn
|
|||
|
||||
tv := newTableView(app, v.title)
|
||||
{
|
||||
tv.SetColorer(c)
|
||||
tv.SetSelectionChangedFunc(v.selChanged)
|
||||
}
|
||||
v.AddPage(v.list.GetName(), tv, true, true)
|
||||
|
|
@ -79,6 +80,12 @@ func newResourceView(title string, app *appView, list resource.List, c colorerFn
|
|||
func (v *resourceView) init(ctx context.Context, ns string) {
|
||||
v.selectedItem, v.selectedNS = noSelection, ns
|
||||
|
||||
colorer := defaultColorer
|
||||
if v.colorerFn != nil {
|
||||
colorer = v.colorerFn
|
||||
}
|
||||
v.getTV().setColorer(colorer)
|
||||
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
|
|
@ -106,10 +113,6 @@ func (v *resourceView) selChanged(r, c int) {
|
|||
v.getTV().cmdBuff.setActive(false)
|
||||
}
|
||||
|
||||
func (v *resourceView) colorFn(f colorerFn) {
|
||||
v.getTV().SetColorer(f)
|
||||
}
|
||||
|
||||
func (v *resourceView) getSelectedItem() string {
|
||||
if v.selectedFn != nil {
|
||||
return v.selectedFn()
|
||||
|
|
@ -124,9 +127,30 @@ func (v *resourceView) hints() hints {
|
|||
return v.CurrentPage().Item.(hinter).hints()
|
||||
}
|
||||
|
||||
func (v *resourceView) setColorerFn(f colorerFn) {
|
||||
v.colorerFn = f
|
||||
v.getTV().setColorer(f)
|
||||
}
|
||||
|
||||
func (v *resourceView) setEnterFn(f enterFn) {
|
||||
v.enterFn = f
|
||||
}
|
||||
|
||||
func (v *resourceView) setDecorateFn(f decorateFn) {
|
||||
v.decorateFn = 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()
|
||||
|
|
@ -262,12 +286,12 @@ func (v *resourceView) refresh() {
|
|||
v.list.SetNamespace(v.selectedNS)
|
||||
}
|
||||
if err := v.list.Reconcile(); err != nil {
|
||||
log.Warn().Msgf("Reconcile %v", err)
|
||||
log.Error().Err(err).Msg("Reconciliation failed")
|
||||
v.app.flash(flashErr, err.Error())
|
||||
}
|
||||
data := v.list.Data()
|
||||
if v.decorateDataFn != nil {
|
||||
data = v.decorateDataFn(data)
|
||||
if v.decorateFn != nil {
|
||||
data = v.decorateFn(data)
|
||||
}
|
||||
v.getTV().update(data)
|
||||
v.selectItem(v.selectedRow, 0)
|
||||
|
|
@ -359,6 +383,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 +396,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::-] "
|
||||
)
|
||||
|
|
@ -55,6 +55,7 @@ func newTableView(app *appView, title string) *tableView {
|
|||
v.actions = make(keyActions)
|
||||
v.SetFixed(1, 0)
|
||||
v.SetBorder(true)
|
||||
v.SetFixed(1, 0)
|
||||
v.SetBorderColor(tcell.ColorDodgerBlue)
|
||||
v.SetBorderAttributes(tcell.AttrBold)
|
||||
v.SetBorderPadding(0, 0, 1, 1)
|
||||
|
|
@ -64,8 +65,13 @@ func newTableView(app *appView, title string) *tableView {
|
|||
v.SetSelectable(true, false)
|
||||
v.SetSelectedStyle(tcell.ColorBlack, tcell.ColorAqua, tcell.AttrBold)
|
||||
v.SetInputCapture(v.keyboard)
|
||||
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)
|
||||
|
|
@ -77,12 +83,10 @@ func newTableView(app *appView, title string) *tableView {
|
|||
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[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)
|
||||
|
||||
return &v
|
||||
}
|
||||
|
||||
func (v *tableView) clearSelection() {
|
||||
|
|
@ -107,6 +111,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
|
||||
}
|
||||
|
||||
|
|
@ -118,17 +123,20 @@ 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
|
||||
}
|
||||
|
||||
func (v *tableView) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||
v.cmdBuff.setActive(false)
|
||||
v.refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +144,7 @@ func (v *tableView) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
if v.cmdBuff.isActive() {
|
||||
v.cmdBuff.del()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -145,6 +154,7 @@ func (v *tableView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
}
|
||||
v.cmdBuff.reset()
|
||||
v.refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -172,12 +182,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
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +201,7 @@ func (v *tableView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|||
v.app.flash(flashInfo, "Filtering...")
|
||||
v.cmdBuff.reset()
|
||||
v.cmdBuff.setActive(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +214,7 @@ func (v *tableView) setDeleted() {
|
|||
}
|
||||
|
||||
// SetColorer sets up table row color management.
|
||||
func (v *tableView) SetColorer(f colorerFn) {
|
||||
func (v *tableView) setColorer(f colorerFn) {
|
||||
v.colorerFn = f
|
||||
}
|
||||
|
||||
|
|
@ -221,6 +234,7 @@ func (v *tableView) hints() hints {
|
|||
if v.actions != nil {
|
||||
return v.actions.toHints()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -266,10 +280,11 @@ func (v *tableView) filtered() resource.TableData {
|
|||
filtered.Rows[k] = row
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (v *tableView) displayCol(index int, name string) string {
|
||||
func (v *tableView) sortIndicator(index int, name string) string {
|
||||
if v.sortCol.index != index {
|
||||
return name
|
||||
}
|
||||
|
|
@ -278,13 +293,13 @@ func (v *tableView) displayCol(index int, name string) string {
|
|||
if v.sortCol.asc {
|
||||
order = "↑"
|
||||
}
|
||||
return fmt.Sprintf("%s[green::]%s[::]", name, order)
|
||||
return fmt.Sprintf("%s [green::]%s[::]", name, order)
|
||||
}
|
||||
|
||||
func (v *tableView) doUpdate(data resource.TableData) {
|
||||
v.currentNS = data.Namespace
|
||||
if v.currentNS == resource.AllNamespaces {
|
||||
v.actions[KeyShiftS] = newKeyAction("Sort Namespace", v.sortNamespaceCmd, true)
|
||||
if v.currentNS == resource.AllNamespaces || v.currentNS == "*" {
|
||||
v.actions[KeyShiftP] = newKeyAction("Sort Namespace", v.sortNamespaceCmd, true)
|
||||
} else {
|
||||
delete(v.actions, KeyShiftS)
|
||||
}
|
||||
|
|
@ -303,118 +318,94 @@ func (v *tableView) doUpdate(data resource.TableData) {
|
|||
v.sortCol.index = 0
|
||||
}
|
||||
|
||||
pads := make(maxyPad, len(data.Header))
|
||||
computeMaxColumns(pads, v.sortCol.index, data)
|
||||
var row int
|
||||
for col, h := range data.Header {
|
||||
v.addHeaderCell(col, h)
|
||||
v.addHeaderCell(col, h, pads)
|
||||
}
|
||||
row++
|
||||
|
||||
// for k := range data.Rows {
|
||||
// log.Debug().Msgf("Keys: %s", k)
|
||||
// }
|
||||
|
||||
keys := v.sortRows(data)
|
||||
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)
|
||||
groupKeys[grp] = s
|
||||
} else {
|
||||
groupKeys[grp] = []string{k}
|
||||
}
|
||||
sortFn := v.defaultSort
|
||||
if v.sortFn != nil {
|
||||
sortFn = v.sortFn
|
||||
}
|
||||
|
||||
// Performs secondary to sort by name for each groups.
|
||||
gKeys := make([]string, len(keys))
|
||||
for k, v := range groupKeys {
|
||||
sort.Strings(v)
|
||||
gKeys = append(gKeys, k)
|
||||
}
|
||||
rs := groupSorter{gKeys, v.sortCol.asc}
|
||||
sort.Sort(rs)
|
||||
|
||||
for _, gk := range gKeys {
|
||||
for _, k := range groupKeys[gk] {
|
||||
prim, sec := v.sortAllRows(data.Rows, sortFn)
|
||||
for _, pk := range prim {
|
||||
for _, sk := range sec[pk] {
|
||||
fgColor := tcell.ColorGray
|
||||
if v.colorerFn != nil {
|
||||
fgColor = v.colorerFn(data.Namespace, data.Rows[k])
|
||||
fgColor = v.colorerFn(data.Namespace, data.Rows[sk])
|
||||
}
|
||||
for col, field := range data.Rows[k].Fields {
|
||||
v.addBodyCell(row, col, field, data.Rows[k].Deltas[col], fgColor)
|
||||
for col, field := range data.Rows[sk].Fields {
|
||||
v.addBodyCell(row, col, field, data.Rows[sk].Deltas[col], fgColor, pads)
|
||||
}
|
||||
row++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *tableView) addHeaderCell(col int, name string) {
|
||||
c := tview.NewTableCell(v.displayCol(col, name))
|
||||
func (v *tableView) sortAllRows(rows resource.RowEvents, sortFn sortFn) (resource.Row, map[string]resource.Row) {
|
||||
keys := make([]string, len(rows))
|
||||
v.sortRows(rows, sortFn, v.sortCol, keys)
|
||||
|
||||
sec := make(map[string]resource.Row, len(rows))
|
||||
for _, k := range keys {
|
||||
grp := rows[k].Fields[v.sortCol.index]
|
||||
sec[grp] = append(sec[grp], k)
|
||||
}
|
||||
|
||||
// Performs secondary to sort by name for each groups.
|
||||
prim := make(resource.Row, 0, len(sec))
|
||||
for k, v := range sec {
|
||||
sort.Strings(v)
|
||||
prim = append(prim, k)
|
||||
}
|
||||
sort.Sort(groupSorter{prim, v.sortCol.asc})
|
||||
|
||||
return prim, sec
|
||||
}
|
||||
|
||||
func (v *tableView) addHeaderCell(col int, name string, pads maxyPad) {
|
||||
c := tview.NewTableCell(v.sortIndicator(col, name))
|
||||
{
|
||||
c.SetExpansion(3)
|
||||
if len(name) == 0 {
|
||||
c.SetExpansion(1)
|
||||
}
|
||||
c.SetExpansion(1)
|
||||
c.SetTextColor(tcell.ColorAntiqueWhite)
|
||||
}
|
||||
v.SetCell(0, col, c)
|
||||
}
|
||||
|
||||
func (v *tableView) addBodyCell(row, col int, field, delta string, color tcell.Color) {
|
||||
c := tview.NewTableCell(deltas(delta, field))
|
||||
func (v *tableView) addBodyCell(row, col int, field, delta string, color tcell.Color, pads maxyPad) {
|
||||
var pField string
|
||||
if isASCII(field) {
|
||||
pField = pad(deltas(delta, field), pads[col])
|
||||
} else {
|
||||
pField = deltas(delta, field)
|
||||
}
|
||||
|
||||
c := tview.NewTableCell(pField)
|
||||
{
|
||||
c.SetExpansion(3)
|
||||
if len(v.GetCell(0, col).Text) == 0 {
|
||||
c.SetExpansion(1)
|
||||
}
|
||||
c.SetExpansion(1)
|
||||
c.SetTextColor(color)
|
||||
}
|
||||
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 {
|
||||
|
|
@ -448,8 +439,7 @@ func (v *tableView) resetTitle() {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Event listeners...
|
||||
|
||||
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