diff --git a/internal/client/cluster.go b/internal/client/cluster.go deleted file mode 100644 index 5a383778..00000000 --- a/internal/client/cluster.go +++ /dev/null @@ -1,44 +0,0 @@ -package client - -import ( - v1 "k8s.io/api/core/v1" -) - -// Cluster represents a Kubernetes cluster. -type Cluster struct { - Connection -} - -// NewCluster instantiates a new cluster. -func NewCluster(c Connection) *Cluster { - return &Cluster{Connection: c} -} - -// Version returns the current cluster git version. -func (c *Cluster) Version() (string, error) { - rev, err := c.ServerVersion() - if err != nil { - return "", err - } - return rev.GitVersion, nil -} - -// ContextName returns the currently active context. -func (c *Cluster) ContextName() (string, error) { - return c.Config().CurrentContextName() -} - -// ClusterName return the currently active cluster name. -func (c *Cluster) ClusterName() (string, error) { - return c.Config().CurrentClusterName() -} - -// UserName returns the currently active user. -func (c *Cluster) UserName() (string, error) { - return c.Config().CurrentUserName() -} - -// GetNodes get all available nodes in the cluster. -func (c *Cluster) GetNodes() (*v1.NodeList, error) { - return c.FetchNodes() -} diff --git a/internal/client/gvr.go b/internal/client/gvr.go index 815ea120..7fba2c41 100644 --- a/internal/client/gvr.go +++ b/internal/client/gvr.go @@ -60,6 +60,18 @@ func (g GVR) ToV() string { return tokens[len(tokens)-2] } +func (g GVR) ToRAndG() (string, string) { + tokens := strings.Split(string(g), "/") + switch len(tokens) { + case 3: + return tokens[0], tokens[2] + case 2: + return "", tokens[1] + default: + return "", tokens[0] + } +} + // ToR returns the resource name. func (g GVR) ToR() string { tokens := strings.Split(string(g), "/") @@ -77,6 +89,7 @@ func (g GVR) ToG() string { } } +// type GVRs []GVR func (g GVRs) Len() int { diff --git a/internal/client/gvr_test.go b/internal/client/gvr_test.go index 47531112..471f3181 100644 --- a/internal/client/gvr_test.go +++ b/internal/client/gvr_test.go @@ -1,6 +1,7 @@ package client_test import ( + "sort" "testing" "github.com/derailed/k9s/internal/client" @@ -8,6 +9,52 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +func TestGVRSort(t *testing.T) { + gg := client.GVRs{"v1/pods", "v1/services", "apps/v1/deployments"} + sort.Sort(gg) + assert.Equal(t, client.GVRs{"v1/pods", "v1/services", "apps/v1/deployments"}, gg) +} + +func TestGVRCan(t *testing.T) { + uu := map[string]struct { + vv []string + v string + e bool + }{ + "describe": {[]string{"get"}, "describe", true}, + "view": {[]string{"get", "list", "watch"}, "view", true}, + "delete": {[]string{"delete", "list", "watch"}, "delete", true}, + "no_delete": {[]string{"get", "list", "watch"}, "delete", false}, + "edit": {[]string{"path", "update", "watch"}, "edit", true}, + "no_edit": {[]string{"get", "list", "watch"}, "edit", false}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.Can(u.vv, u.v)) + }) + } +} + +func TestAsGVR(t *testing.T) { + uu := map[string]struct { + gvr string + e schema.GroupVersionResource + }{ + "full": {"apps/v1/deployments", schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}}, + "core": {"v1/pods", schema.GroupVersionResource{Version: "v1", Resource: "pods"}}, + "bork": {"users", schema.GroupVersionResource{Resource: "users"}}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.GVR(u.gvr).AsGVR()) + }) + } +} + func TestAsGV(t *testing.T) { uu := map[string]struct { gvr string @@ -119,7 +166,7 @@ func TestToV(t *testing.T) { } } -func TestToStringer(t *testing.T) { +func TestToString(t *testing.T) { uu := map[string]struct { gvr string }{ diff --git a/internal/client/helper_test.go b/internal/client/helper_test.go new file mode 100644 index 00000000..4a4c5091 --- /dev/null +++ b/internal/client/helper_test.go @@ -0,0 +1,37 @@ +package client_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/stretchr/testify/assert" +) + +func TestNamespaced(t *testing.T) { + uu := []struct { + p, ns, n string + }{ + {"fred/blee", "fred", "blee"}, + {"blee", "", "blee"}, + } + + for _, u := range uu { + ns, n := client.Namespaced(u.p) + assert.Equal(t, u.ns, ns) + assert.Equal(t, u.n, n) + } +} + +func TestFQN(t *testing.T) { + uu := []struct { + ns, n string + e string + }{ + {"fred", "blee", "fred/blee"}, + {"", "blee", "blee"}, + } + + for _, u := range uu { + assert.Equal(t, u.e, client.FQN(u.ns, u.n)) + } +} diff --git a/internal/client/helpers.go b/internal/client/helpers.go index 8b529ffd..c78c6aec 100644 --- a/internal/client/helpers.go +++ b/internal/client/helpers.go @@ -12,10 +12,10 @@ import ( var toFileName = regexp.MustCompile(`[^(\w/\.)]`) // Namespaced converts a resource path to namespace and resource name. -func Namespaced(n string) (string, string) { - ns, po := path.Split(n) +func Namespaced(p string) (string, string) { + ns, n := path.Split(p) - return strings.Trim(ns, "/"), po + return strings.Trim(ns, "/"), n } // FQN returns a fully qualified resource name. diff --git a/internal/config/config.go b/internal/config/config.go index 67b3a90d..b52cd1d6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,8 +1,5 @@ package config -// BOZO!! Once yaml is stable implement validation -// go get gopkg.in/validator.v2 - import ( "errors" "fmt" diff --git a/internal/dao/generic.go b/internal/dao/generic.go index 1871a643..13841429 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -2,6 +2,7 @@ package dao import ( "github.com/derailed/k9s/internal/client" + "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/dynamic" ) @@ -24,9 +25,12 @@ func (g *Generic) Delete(path string, cascade, force bool) error { } ns, n := client.Namespaced(path) - return g.dynClient().Namespace(ns).Delete(n, &metav1.DeleteOptions{ - PropagationPolicy: &p, - }) + log.Debug().Msgf("DELETING %q:%q -- %q", ns, n, path) + opts := metav1.DeleteOptions{PropagationPolicy: &p} + if ns != "-" { + return g.dynClient().Namespace(ns).Delete(n, &opts) + } + return g.dynClient().Delete(n, &opts) } func (g *Generic) dynClient() dynamic.NamespaceableResourceInterface { diff --git a/internal/dao/reconcile.go b/internal/dao/reconcile.go index 4b173f32..79caa03c 100644 --- a/internal/dao/reconcile.go +++ b/internal/dao/reconcile.go @@ -15,14 +15,14 @@ import ( // Reconcile previous vs current state and emits delta events. func Reconcile(ctx context.Context, table render.TableData, gvr client.GVR) (render.TableData, error) { defer func(t time.Time) { - log.Debug().Msgf("Reconcile elapsed: %v", time.Since(t)) + log.Debug().Msgf("RECONCILE elapsed: %v", time.Since(t)) }(time.Now()) path, ok := ctx.Value(internal.KeyPath).(string) if !ok { return table, fmt.Errorf("no path specified for %s", gvr) } - log.Debug().Msgf(" Reconcile %q in ns %q with path %q", gvr, table.Namespace, path) + log.Debug().Msgf("Reconcile %q in ns %q with path %q", gvr, table.Namespace, path) factory, ok := ctx.Value(internal.KeyFactory).(Factory) if !ok { return table, fmt.Errorf("no factory found for %s", gvr) diff --git a/internal/dao/registry.go b/internal/dao/registry.go index 52f791e9..66496a2e 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -45,7 +45,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) { r, ok := m[gvr] if !ok { r = &Generic{} - log.Warn().Msgf("No DAO registry entry for %q. Going generic!", gvr) + log.Warn().Msgf("No DAO registry entry for %q. Using factory!", gvr) } r.Init(f, gvr) @@ -57,7 +57,7 @@ func RegisterMeta(gvr string, res metav1.APIResource) { resMetas[client.GVR(gvr)] = res } -func AllGVRs() []client.GVR { +func AllGVRs() client.GVRs { kk := make(client.GVRs, 0, len(resMetas)) for k := range resMetas { kk = append(kk, k) @@ -137,7 +137,13 @@ func loadNonResource(m ResourceMetas) error { } m["rbac"] = metav1.APIResource{ Name: "Rbac", - Kind: "RBAC", + Kind: "Rules", + Categories: []string{"k9s"}, + } + m["policy"] = metav1.APIResource{ + Name: "Policy", + Kind: "Rules", + Namespaced: true, Categories: []string{"k9s"}, } m["containers"] = metav1.APIResource{ diff --git a/internal/dao/types.go b/internal/dao/types.go index b06d5079..553d8857 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -8,7 +8,6 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/informers" restclient "k8s.io/client-go/rest" ) @@ -27,7 +26,7 @@ type Factory interface { ForResource(ns, gvr string) informers.GenericInformer // WaitForCacheSync synchronize the cache. - WaitForCacheSync() map[schema.GroupVersionResource]bool + WaitForCacheSync() // DeleteForwarder deletes a pod forwarder. DeleteForwarder(path string) diff --git a/internal/keys.go b/internal/keys.go index 935389de..2304a624 100644 --- a/internal/keys.go +++ b/internal/keys.go @@ -5,17 +5,19 @@ type ContextKey string // A collection of context keys. const ( - KeyFactory ContextKey = "factory" - KeyLabels ContextKey = "labels" - KeyFields ContextKey = "fields" - KeyTable ContextKey = "table" - KeyDir ContextKey = "dir" - KeyPath ContextKey = "path" - KeySubject ContextKey = "subject" - KeyGVR ContextKey = "gvr" - KeyForwards ContextKey = "forwards" - KeyContainers ContextKey = "containers" - KeyBenchCfg ContextKey = "benchcfg" - KeyAliases ContextKey = "aliases" - KeyUID ContextKey = "uid" + KeyFactory ContextKey = "factory" + KeyLabels ContextKey = "labels" + KeyFields ContextKey = "fields" + KeyTable ContextKey = "table" + KeyDir ContextKey = "dir" + KeyPath ContextKey = "path" + KeySubject ContextKey = "subject" + KeyGVR ContextKey = "gvr" + KeyForwards ContextKey = "forwards" + KeyContainers ContextKey = "containers" + KeyBenchCfg ContextKey = "benchcfg" + KeyAliases ContextKey = "aliases" + KeyUID ContextKey = "uid" + KeySubjectKind ContextKey = "subjectKind" + KeySubjectName ContextKey = "subjectName" ) diff --git a/internal/model/cluster.go b/internal/model/cluster.go index 3a10648e..625b45ef 100644 --- a/internal/model/cluster.go +++ b/internal/model/cluster.go @@ -7,17 +7,6 @@ import ( ) type ( - // ClusterMeta represents metadata about a Kubernetes cluster. - ClusterMeta interface { - client.Connection - - Version() (string, error) - ContextName() (string, error) - ClusterName() (string, error) - UserName() (string, error) - GetNodes() (*v1.NodeList, error) - } - // MetricsServer gather metrics information from pods and nodes. MetricsServer interface { MetricsService @@ -36,34 +25,34 @@ type ( // Cluster represents a kubernetes resource. Cluster struct { - api ClusterMeta - mx MetricsServer + client client.Connection + mx MetricsServer } ) // NewCluster returns a new cluster info resource. func NewCluster(c client.Connection, mx MetricsServer) *Cluster { - return NewClusterWithArgs(client.NewCluster(c), mx) + return NewClusterWithArgs(c, mx) } // NewClusterWithArgs for tests only! -func NewClusterWithArgs(ci ClusterMeta, mx MetricsServer) *Cluster { - return &Cluster{api: ci, mx: mx} +func NewClusterWithArgs(c client.Connection, mx MetricsServer) *Cluster { + return &Cluster{client: c, mx: mx} } // Version returns the current K8s cluster version. func (c *Cluster) Version() string { - info, err := c.api.Version() + info, err := c.client.ServerVersion() if err != nil { return "n/a" } - return info + return info.GitVersion } // ContextName returns the context name. func (c *Cluster) ContextName() string { - n, err := c.api.ContextName() + n, err := c.client.Config().CurrentContextName() if err != nil { return "n/a" } @@ -72,7 +61,7 @@ func (c *Cluster) ContextName() string { // ClusterName returns the cluster name. func (c *Cluster) ClusterName() string { - n, err := c.api.ClusterName() + n, err := c.client.Config().CurrentClusterName() if err != nil { return "n/a" } @@ -81,7 +70,7 @@ func (c *Cluster) ClusterName() string { // UserName returns the user name. func (c *Cluster) UserName() string { - n, err := c.api.UserName() + n, err := c.client.Config().CurrentUserName() if err != nil { return "n/a" } diff --git a/internal/model/cluster_test.go b/internal/model/cluster_test.go deleted file mode 100644 index 4b839ba7..00000000 --- a/internal/model/cluster_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package model_test - -import ( - "fmt" - "testing" - - "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/model" - m "github.com/petergtz/pegomock" - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" -) - -func init() { - zerolog.SetGlobalLevel(zerolog.Disabled) -} - -func TestClusterVersion(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mm.Version()).ThenReturn("1.2.3", nil) - - ci := model.NewClusterWithArgs(mm, mx) - assert.Equal(t, "1.2.3", ci.Version()) -} - -func TestClusterNoVersion(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mm.Version()).ThenReturn("bad", fmt.Errorf("No data")) - - ci := model.NewClusterWithArgs(mm, mx) - assert.Equal(t, "n/a", ci.Version()) -} - -func TestClusterName(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mm.ClusterName()).ThenReturn("fred", nil) - - ci := model.NewClusterWithArgs(mm, mx) - assert.Equal(t, "fred", ci.ClusterName()) -} - -func TestContextName(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mm.ContextName()).ThenReturn("fred", nil) - - ci := model.NewClusterWithArgs(mm, mx) - assert.Equal(t, "fred", ci.ContextName()) -} - -func TestUserName(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - m.When(mm.UserName()).ThenReturn("fred", nil) - - ci := model.NewClusterWithArgs(mm, mx) - assert.Equal(t, "fred", ci.UserName()) -} - -func TestClusterMetrics(t *testing.T) { - mm, mx := NewMockClusterMeta(), NewMockMetricsServer() - - mxx := clusterMetric() - - c := model.NewClusterWithArgs(mm, mx) - c.Metrics(nil, nil, &mxx) - assert.Equal(t, clusterMetric(), mxx) -} - -// Helpers... - -func TestUsingMocks(t *testing.T) { - m.RegisterMockTestingT(t) - m.RegisterMockFailHandler(func(m string, i ...int) { - fmt.Println("Boom!", m, i) - }) -} - -func clusterMetric() client.ClusterMetrics { - return client.ClusterMetrics{ - PercCPU: 100, - PercMEM: 1000, - } -} diff --git a/internal/model/generic.go b/internal/model/generic.go index ea6d9ae7..e4bd360c 100644 --- a/internal/model/generic.go +++ b/internal/model/generic.go @@ -3,6 +3,7 @@ package model import ( "context" "fmt" + "time" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" @@ -26,6 +27,10 @@ type Generic struct { // List returns a collection of node resources. func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { + defer func(t time.Time) { + log.Debug().Msgf("LIST elapsed: %v", time.Since(t)) + }(time.Now()) + // Ensures the factory is tracking this resource _ = g.factory.ForResource(g.namespace, g.gvr) @@ -47,7 +52,7 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { table, ok := o.(*metav1beta1.Table) if !ok { - return nil, fmt.Errorf("invalid table found on generic %s -- %T", g.gvr, o) + return nil, fmt.Errorf("expecting table but got %T", o) } g.table = table res := make([]runtime.Object, len(g.table.Rows)) @@ -61,6 +66,10 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { // Hydrate returns nodes as rows. func (g *Generic) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { + defer func(t time.Time) { + log.Debug().Msgf("HYDRATE elapsed: %v", time.Since(t)) + }(time.Now()) + gr, ok := re.(*render.Generic) if !ok { return fmt.Errorf("expecting generic renderer for %s but got %T", g.gvr, re) diff --git a/internal/model/policy.go b/internal/model/policy.go new file mode 100644 index 00000000..8cc167be --- /dev/null +++ b/internal/model/policy.go @@ -0,0 +1,237 @@ +package model + +import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +type Policy struct { + Resource +} + +func (p *Policy) List(ctx context.Context) ([]runtime.Object, error) { + gvr, ok := ctx.Value(internal.KeyGVR).(string) + if !ok { + return nil, fmt.Errorf("expecting a context gvr") + } + kind, ok := ctx.Value(internal.KeySubjectKind).(string) + if !ok { + return nil, fmt.Errorf("expecting a context subject kind") + } + name, ok := ctx.Value(internal.KeySubjectName).(string) + if !ok { + return nil, fmt.Errorf("expecting a context subject name") + } + + p.gvr = gvr + crps, err := p.loadClusterRoleBinding(kind, name) + if err != nil { + return nil, err + } + rps, err := p.loadRoleBinding(kind, name) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, 0, len(crps)+len(rps)) + for _, p := range crps { + oo = append(oo, p) + } + for _, p := range rps { + oo = append(oo, p) + } + + return oo, nil +} + +// BOZO!! refactor! +func (p *Policy) loadClusterRoleBinding(kind, name string) (render.Policies, error) { + crbs, err := fetchClusterRoleBindings(p.factory) + if err != nil { + return nil, err + } + + var nn []string + for _, crb := range crbs { + for _, s := range crb.Subjects { + if s.Kind == kind && s.Name == name { + nn = append(nn, crb.RoleRef.Name) + } + } + } + crs, err := p.fetchClusterRoles() + if err != nil { + return nil, err + } + + rows := make(render.Policies, 0, len(nn)) + for _, cr := range crs { + if !in(nn, cr.Name) { + continue + } + rows = append(rows, parseRules("*", "CR:"+cr.Name, cr.Rules)...) + } + + return rows, nil +} + +func (p *Policy) loadRoleBinding(kind, name string) (render.Policies, error) { + ss, err := p.fetchRoleBindingSubjects(kind, name) + if err != nil { + return nil, err + } + + crs, err := p.fetchClusterRoles() + if err != nil { + return nil, err + } + rows := make(render.Policies, 0, len(crs)) + for _, cr := range crs { + if !in(ss, "ClusterRole:"+cr.Name) { + continue + } + rows = append(rows, parseRules("*", "CR:"+cr.Name, cr.Rules)...) + } + + ros, err := p.fetchRoles() + if err != nil { + return nil, err + } + for _, ro := range ros { + if !in(ss, "Role:"+ro.Name) { + continue + } + log.Debug().Msgf("Loading rules for role %q:%q", ro.Namespace, ro.Name) + rows = append(rows, parseRules(ro.Namespace, "RO:"+ro.Name, ro.Rules)...) + } + + return rows, nil +} + +func fetchClusterRoleBindings(f Factory) ([]rbacv1.ClusterRoleBinding, error) { + oo, err := f.List("rbac.authorization.k8s.io/v1/clusterrolebindings", render.ClusterScope, labels.Everything()) + if err != nil { + return nil, err + } + + crbs := make([]rbacv1.ClusterRoleBinding, len(oo)) + for i, o := range oo { + var crb rbacv1.ClusterRoleBinding + if e := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb); e != nil { + return nil, e + } + crbs[i] = crb + } + + return crbs, nil +} + +func fetchRoleBindings(f Factory) ([]rbacv1.RoleBinding, error) { + oo, err := f.List("rbac.authorization.k8s.io/v1/rolebindings", render.ClusterScope, labels.Everything()) + if err != nil { + return nil, err + } + + rbs := make([]rbacv1.RoleBinding, 0, len(oo)) + for _, o := range oo { + var rb rbacv1.RoleBinding + if e := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb); e != nil { + return nil, e + } + rbs = append(rbs, rb) + } + + return rbs, nil +} + +func (p *Policy) fetchRoleBindingSubjects(kind, name string) ([]string, error) { + rbs, err := fetchRoleBindings(p.factory) + if err != nil { + return nil, err + } + ss := make([]string, 0, len(rbs)) + for _, rb := range rbs { + for _, s := range rb.Subjects { + if s.Kind == kind && s.Name == name { + ss = append(ss, rb.RoleRef.Kind+":"+rb.Name) + } + } + } + + return ss, nil +} + +func (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) { + oo, err := p.factory.List("rbac.authorization.k8s.io/v1/clusterroles", render.ClusterScope, labels.Everything()) + if err != nil { + return nil, err + } + + crs := make([]rbacv1.ClusterRole, len(oo)) + for i, o := range oo { + var cr rbacv1.ClusterRole + if e := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr); e != nil { + return nil, err + } + crs[i] = cr + } + + return crs, nil +} + +func (p *Policy) fetchRoles() ([]rbacv1.Role, error) { + oo, err := p.factory.List("rbac.authorization.k8s.io/v1/roles", render.AllNamespaces, labels.Everything()) + if err != nil { + return nil, err + } + + rr := make([]rbacv1.Role, len(oo)) + for i, o := range oo { + var ro rbacv1.Role + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro); err != nil { + return nil, err + } + rr[i] = ro + } + + return rr, nil +} + +func in(nn []string, match string) bool { + for _, n := range nn { + if n == match { + return true + } + } + return false +} + +func parseRules(ns, binding string, rules []rbacv1.PolicyRule) render.Policies { + pp := make(render.Policies, 0, len(rules)) + for _, rule := range rules { + for _, grp := range rule.APIGroups { + for _, res := range rule.Resources { + for _, na := range rule.ResourceNames { + pp = pp.Upsert(render.NewPolicyRes(ns, binding, FQN(res, na), grp, rule.Verbs)) + } + pp = pp.Upsert(render.NewPolicyRes(ns, binding, FQN(grp, res), grp, rule.Verbs)) + } + } + for _, nres := range rule.NonResourceURLs { + if nres[0] != '/' { + nres = "/" + nres + } + pp = pp.Upsert(render.NewPolicyRes(ns, binding, nres, "n/a", rule.Verbs)) + } + } + + return pp +} diff --git a/internal/model/rbac.go b/internal/model/rbac.go index eff90697..3f09da8b 100644 --- a/internal/model/rbac.go +++ b/internal/model/rbac.go @@ -7,17 +7,25 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" - "github.com/rs/zerolog/log" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) +const ( + crbGVR = "rbac.authorization.k8s.io/v1/clusterrolebindings" + crGVR = "rbac.authorization.k8s.io/v1/clusterroles" + rbGVR = "rbac.authorization.k8s.io/v1/rolebindings" + rGVR = "rbac.authorization.k8s.io/v1/roles" +) + +// Rbac represents a model for listing rbac resources. type Rbac struct { Resource } +// List lists out rbac resources. func (r *Rbac) List(ctx context.Context) ([]runtime.Object, error) { gvr, ok := ctx.Value(internal.KeyGVR).(string) if !ok { @@ -25,7 +33,6 @@ func (r *Rbac) List(ctx context.Context) ([]runtime.Object, error) { } r.gvr = gvr path, ok := ctx.Value(internal.KeyPath).(string) - log.Debug().Msgf("LISTING RBACK %q--%q", r.gvr, path) if !ok || path == "" { return r.Resource.List(ctx) } @@ -44,8 +51,9 @@ func (r *Rbac) List(ctx context.Context) ([]runtime.Object, error) { } } +// BOZO!!Refact gvr as const func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) { - o, err := r.factory.Get("rbac.authorization.k8s.io/v1/clusterrolebindings", path, labels.Everything()) + o, err := r.factory.Get(crbGVR, path, labels.Everything()) if err != nil { return nil, err } @@ -56,8 +64,7 @@ func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) { return nil, err } - kind := "rbac.authorization.k8s.io/v1/clusterroles" - crbo, err := r.factory.Get(kind, client.FQN("-", crb.RoleRef.Name), labels.Everything()) + crbo, err := r.factory.Get(crGVR, client.FQN("-", crb.RoleRef.Name), labels.Everything()) if err != nil { return nil, err } @@ -66,11 +73,12 @@ func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) { if err != nil { return nil, err } - return r.parseRules(cr.Rules), nil + + return asRuntimeObjects(parseRules(render.ClusterScope, "-", cr.Rules)), nil } func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { - o, err := r.factory.Get("rbac.authorization.k8s.io/v1/rolebindings", path, labels.Everything()) + o, err := r.factory.Get(rbGVR, path, labels.Everything()) if err != nil { return nil, err } @@ -81,8 +89,7 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { } if rb.RoleRef.Kind == "ClusterRole" { - kind := "rbac.authorization.k8s.io/v1/clusterroles" - o, e := r.factory.Get(kind, client.FQN("-", rb.RoleRef.Name), labels.Everything()) + o, e := r.factory.Get(crGVR, client.FQN("-", rb.RoleRef.Name), labels.Everything()) if e != nil { return nil, e } @@ -91,11 +98,10 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { if err != nil { return nil, err } - return r.parseRules(cr.Rules), nil + return asRuntimeObjects(parseRules(render.ClusterScope, "-", cr.Rules)), nil } - kind := "rbac.authorization.k8s.io/v1/roles" - ro, err := r.factory.Get(kind, client.FQN(rb.Namespace, rb.RoleRef.Name), labels.Everything()) + ro, err := r.factory.Get(rGVR, client.FQN(rb.Namespace, rb.RoleRef.Name), labels.Everything()) if err != nil { return nil, err } @@ -105,11 +111,11 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { return nil, err } - return r.parseRules(role.Rules), nil + return asRuntimeObjects(parseRules(render.ClusterScope, "-", role.Rules)), nil } func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) { - o, err := r.factory.Get("rbac.authorization.k8s.io/v1/clusterroles", path, labels.Everything()) + o, err := r.factory.Get(crGVR, path, labels.Everything()) if err != nil { return nil, err } @@ -120,11 +126,11 @@ func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) { return nil, err } - return r.parseRules(cr.Rules), nil + return asRuntimeObjects(parseRules(render.ClusterScope, "-", cr.Rules)), nil } func (r *Rbac) loadRole(path string) ([]runtime.Object, error) { - o, err := r.factory.Get("rbac.authorization.k8s.io/v1/roles", path, labels.Everything()) + o, err := r.factory.Get(rGVR, path, labels.Everything()) if err != nil { return nil, err } @@ -135,65 +141,14 @@ func (r *Rbac) loadRole(path string) ([]runtime.Object, error) { return nil, err } - return r.parseRules(ro.Rules), nil + return asRuntimeObjects(parseRules(render.ClusterScope, "-", ro.Rules)), nil } -func makeRes(res, grp string, vv []string) *render.PolicyRes { - return &render.PolicyRes{ - Resource: res, - Group: grp, - Verbs: vv, - } -} - -func (r *Rbac) parseRules(rules []rbacv1.PolicyRule) []runtime.Object { - m := make([]runtime.Object, 0, len(rules)) - for _, rule := range rules { - for _, grp := range rule.APIGroups { - for _, res := range rule.Resources { - k := res - if grp != "" { - k = res + "." + grp - } - for _, na := range rule.ResourceNames { - m = upsert(m, makeRes(FQN(k, na), grp, rule.Verbs)) - } - m = upsert(m, makeRes(k, grp, rule.Verbs)) - } - } - for _, nres := range rule.NonResourceURLs { - if nres[0] != '/' { - nres = "/" + nres - } - m = upsert(m, makeRes(nres, "", rule.Verbs)) - } - } - - return m -} - -func upsert(rr []runtime.Object, p *render.PolicyRes) []runtime.Object { - idx, ok := find(rr, p.Resource) - if !ok { - return append(rr, p) - } - rr[idx] = p - - return rr -} - -// Find locates a row by id. Retturns false is not found. -func find(rr []runtime.Object, res string) (int, bool) { +func asRuntimeObjects(rr render.Policies) []runtime.Object { + oo := make([]runtime.Object, len(rr)) for i, r := range rr { - p, ok := r.(*render.PolicyRes) - if !ok { - log.Error().Err(fmt.Errorf("expecting policyres but got `%T", r)) - return 0, false - } - if p.Resource == res { - return i, true - } + oo[i] = r } - return 0, false + return oo } diff --git a/internal/model/rbac_int_test.go b/internal/model/rbac_int_test.go deleted file mode 100644 index 59c5139b..00000000 --- a/internal/model/rbac_int_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package model - -// import( -// "testing" -// ) - -// BOZO!! -// func TestParseRules(t *testing.T) { -// ok, nok := toVerbIcon(true), toVerbIcon(false) -// _ = nok - -// uu := []struct { -// pp []rbacv1.PolicyRule -// e render.Rows -// }{ -// { -// []rbacv1.PolicyRule{ -// {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"*"}}, -// }, -// render.Rows{ -// render.Row{Fields: render.Fields{"*.*", "*", ok, ok, ok, ok, ok, ok, ok, ok, ""}}, -// }, -// }, -// { -// []rbacv1.PolicyRule{ -// {APIGroups: []string{"*"}, Resources: []string{"*"}, Verbs: []string{"get"}}, -// }, -// render.Rows{ -// render.Row{Fields: render.Fields{"*.*", "*", ok, nok, nok, nok, nok, nok, nok, nok, ""}}, -// }, -// }, -// { -// []rbacv1.PolicyRule{ -// {APIGroups: []string{""}, Resources: []string{"*"}, Verbs: []string{"list"}}, -// }, -// render.Rows{ -// render.Row{Fields: render.Fields{"*", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}}, -// }, -// }, -// { -// []rbacv1.PolicyRule{ -// {APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"list"}, ResourceNames: []string{"fred"}}, -// }, -// render.Rows{ -// render.Row{Fields: render.Fields{"pods", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}}, -// render.Row{Fields: render.Fields{"pods/fred", "v1", nok, ok, nok, nok, nok, nok, nok, nok, ""}}, -// }, -// }, -// { -// []rbacv1.PolicyRule{ -// {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"/fred"}}, -// }, -// render.Rows{ -// render.Row{Fields: render.Fields{"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""}}, -// }, -// }, -// { -// []rbacv1.PolicyRule{ -// {APIGroups: []string{}, Resources: []string{}, Verbs: []string{"get"}, NonResourceURLs: []string{"fred"}}, -// }, -// render.Rows{ -// render.Row{Fields: render.Fields{"/fred", resource.NAValue, ok, nok, nok, nok, nok, nok, nok, nok, ""}}, -// }, -// }, -// } - -// var v Rbac -// for _, u := range uu { -// evts := v.parseRules(u.pp) -// for k, v := range u.e { -// assert.Equal(t, v, evts[k].Fields) -// } -// } -// } diff --git a/internal/model/registry.go b/internal/model/registry.go index ea9716e7..4d8c076f 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -23,6 +23,18 @@ var Registry = map[string]ResourceMeta{ Model: &Rbac{}, Renderer: &render.Rbac{}, }, + "policy": ResourceMeta{ + Model: &Policy{}, + Renderer: &render.Policy{}, + }, + "users": ResourceMeta{ + Model: &Subject{}, + Renderer: &render.Subject{}, + }, + "groups": ResourceMeta{ + Model: &Subject{}, + Renderer: &render.Subject{}, + }, "portforwards": ResourceMeta{ Model: &PortForward{}, Renderer: &render.PortForward{}, diff --git a/internal/model/resource.go b/internal/model/resource.go index 26aa056e..37423f84 100644 --- a/internal/model/resource.go +++ b/internal/model/resource.go @@ -2,6 +2,7 @@ package model import ( "context" + "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/render" @@ -22,21 +23,23 @@ func (r *Resource) Init(ns, gvr string, f Factory) { // List returns a collection of nodes. func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) { + defer func(t time.Time) { + log.Debug().Msgf("LIST elapsed: %v", time.Since(t)) + }(time.Now()) + strLabel, ok := ctx.Value(internal.KeyLabels).(string) lsel := labels.Everything() if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil { lsel = sel.AsSelector() } - log.Debug().Msgf("^^^^^Listing with selector %q:%q--%#v", r.namespace, r.gvr, lsel) - oo, err := r.factory.List(r.gvr, r.namespace, lsel) - r.factory.WaitForCacheSync() - - return oo, err + return r.factory.List(r.gvr, r.namespace, lsel) } // Render returns a node as a row. func (r *Resource) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { - log.Debug().Msgf("^^^^^^ HYDRATING (%q) %d", r.namespace, len(oo)) + defer func(t time.Time) { + log.Debug().Msgf("HYDRATE elapsed: %v", time.Since(t)) + }(time.Now()) var index int for _, o := range oo { diff --git a/internal/model/subject.go b/internal/model/subject.go index 0b5620eb..b488f3a9 100644 --- a/internal/model/subject.go +++ b/internal/model/subject.go @@ -7,44 +7,63 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/render" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" ) // Subject represents a subject model. type Subject struct { Resource - - subjectKind string } // List returns a collection of subjects. func (s *Subject) List(ctx context.Context) ([]runtime.Object, error) { - var ok bool - s.subjectKind, ok = ctx.Value(internal.KeySubject).(string) + kind, ok := ctx.Value(internal.KeySubjectKind).(string) if !ok { - return nil, errors.New("expecting a subject") + return nil, errors.New("expecting a SubjectKind") } - crbs, err := s.factory.List(render.ClusterScope, "rbac.authorization.k8s.io/v1/clusterrolebindings", labels.Everything()) + crbs, err := fetchClusterRoleBindings(s.factory) if err != nil { return nil, err } + oo := make([]runtime.Object, 0, len(crbs)) + for _, crb := range crbs { + for _, su := range crb.Subjects { + if su.Kind != kind || inSubjectRes(oo, su.Name) { + continue + } + oo = append(oo, render.SubjectRef{ + Name: su.Name, + Kind: "ClusterRoleBinding", + FirstLocation: crb.Name, + }) + } + } - rbs, err := s.factory.List(render.ClusterScope, "rbac.authorization.k8s.io/v1/rolebindings", labels.Everything()) + rbs, err := fetchRoleBindings(s.factory) if err != nil { return nil, err } + for _, rb := range rbs { + for _, su := range rb.Subjects { + if su.Kind != kind || inSubjectRes(oo, su.Name) { + continue + } + oo = append(oo, render.SubjectRef{ + Name: su.Name, + Kind: "RoleBinding", + FirstLocation: rb.Name, + }) + } + } - return append(crbs, rbs...), nil + return oo, nil } // Hydrate returns a pod as container rows. func (s *Subject) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { for i, o := range oo { - res, ok := o.(*unstructured.Unstructured) + res, ok := o.(render.SubjectRef) if !ok { return fmt.Errorf("expecting unstructured but got %T", o) } @@ -57,77 +76,15 @@ func (s *Subject) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) erro return nil } -// BOZO!! -// func (s *Subject) fetchClusterRoleBindings() ([]runtime.Object, error) { -// oo, err := s.factory.List(render.ClusterScope, "rbac.authorization.k8s.io/v1/clusterrolebindings", labels.Everything()) -// if err != nil { -// return nil, err -// } - -// rows := make([]runtime.Object, 0, len(oo)) -// for _, o := range oo { -// var crb rbacv1.ClusterRoleBinding -// err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb) -// if err != nil { -// return nil, err -// } -// for _, subject := range crb.Subjects { -// if subject.Kind != s.subjectKind { -// continue -// } -// rows = append(rows, SubjectRes{ -// id: subject.Name, -// fields: render.Fields{subject.Name, "ClusterRoleBinding", crb.Name}, -// }) -// } -// } - -// return rows, nil -// } - -// func (s *Subject) fetchRoleBindings() ([]runtime.Object, error) { -// oo, err := s.factory.List(render.ClusterScope, "rbac.authorization.k8s.io/v1/rolebindings", labels.Everything()) -// if err != nil { -// return nil, err -// } - -// rows := make([]runtime.Object, 0, len(oo)) -// for _, o := range oo { -// var rb rbacv1.RoleBinding -// err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb) -// if err != nil { -// return nil, err -// } -// for _, subject := range rb.Subjects { -// if subject.Kind == s.subjectKind { -// rows = append(rows, SubjectRes{ -// id: subject.Name, -// fields: render.Fields{subject.Name, "RoleBinding", rb.Name}, -// }) -// } -// } -// } - -// return rows, nil -// } - -// ---------------------------------------------------------------------------- - -// SubjectRes represents a subject resource. -type SubjectRes struct { - id string - fields render.Fields -} - -func (s SubjectRes) GetID() string { return s.id } -func (s SubjectRes) GetFields() render.Fields { return s.fields } - -// GetObjectKind returns a schema object. -func (s SubjectRes) GetObjectKind() schema.ObjectKind { - return nil -} - -// DeepCopyObject returns a container copy. -func (s SubjectRes) DeepCopyObject() runtime.Object { - return s +func inSubjectRes(oo []runtime.Object, match string) bool { + for _, o := range oo { + res, ok := o.(render.SubjectRef) + if !ok { + continue + } + if res.Name == match { + return true + } + } + return false } diff --git a/internal/model/types.go b/internal/model/types.go index ecc92068..0387685a 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -9,7 +9,6 @@ import ( "github.com/derailed/tview" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/informers" ) @@ -84,7 +83,7 @@ type Factory interface { ForResource(ns, gvr string) informers.GenericInformer // WaitForCacheSync synchronize the cache. - WaitForCacheSync() map[schema.GroupVersionResource]bool + WaitForCacheSync() // Forwards returns all portforwards. Forwarders() watch.Forwarders diff --git a/internal/render/alias.go b/internal/render/alias.go index f4ee301b..428dfa30 100644 --- a/internal/render/alias.go +++ b/internal/render/alias.go @@ -26,25 +26,26 @@ func (Alias) Header(ns string) HeaderRow { Header{Name: "RESOURCE"}, Header{Name: "COMMAND"}, Header{Name: "APIGROUP"}, - // Header{Name: "AGE", Decorator: AgeDecorator}, } } // Render renders a K8s resource to screen. +// BOZO!! Pass in a row with pre-alloc fields?? func (Alias) Render(o interface{}, gvr string, r *Row) error { a, ok := o.(AliasRes) if !ok { - return fmt.Errorf("expected aliasres, but got %T", o) + return fmt.Errorf("expected AliasRes, but got %T", o) } + _ = a - g := client.GVR(a.GVR) - r.ID = string(g) - r.Fields = Fields{ - g.ToR(), + r.ID = gvr + gvr1 := client.GVR(a.GVR) + grp, res := gvr1.ToRAndG() + r.Fields = append(r.Fields, + res, strings.Join(a.Aliases, ","), - g.ToG(), - // time.Now().String(), - } + grp, + ) return nil } diff --git a/internal/render/alias_test.go b/internal/render/alias_test.go new file mode 100644 index 00000000..6caa6763 --- /dev/null +++ b/internal/render/alias_test.go @@ -0,0 +1,81 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/gdamore/tcell" + "github.com/stretchr/testify/assert" +) + +func TestAliasColorer(t *testing.T) { + var a render.Alias + + r := render.Row{ID: "g/v/r", Fields: render.Fields{"r", "blee", "g"}} + uu := map[string]struct { + ns string + re render.RowEvent + e tcell.Color + }{ + "addAll": { + ns: render.AllNamespaces, + re: render.RowEvent{Kind: render.EventAdd, Row: r}, + e: tcell.ColorMediumSpringGreen}, + "deleteAll": { + ns: render.AllNamespaces, + re: render.RowEvent{Kind: render.EventDelete, Row: r}, + e: tcell.ColorMediumSpringGreen}, + "updateAll": { + ns: render.AllNamespaces, + re: render.RowEvent{Kind: render.EventUpdate, Row: r}, + e: tcell.ColorMediumSpringGreen, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, a.ColorerFunc()(u.ns, u.re)) + }) + } +} + +func TestAliasHeader(t *testing.T) { + h := render.HeaderRow{ + render.Header{Name: "RESOURCE"}, + render.Header{Name: "COMMAND"}, + render.Header{Name: "APIGROUP"}, + } + + var a render.Alias + assert.Equal(t, h, a.Header("fred")) + assert.Equal(t, h, a.Header(render.AllNamespaces)) +} + +func TestAliasRender(t *testing.T) { + a := render.Alias{} + + o := render.AliasRes{ + GVR: "fred/v1/blee", + Aliases: []string{"a", "b", "c"}, + } + + var r render.Row + assert.Nil(t, a.Render(o, "fred/v1/blee", &r)) + assert.Equal(t, render.Row{ID: "fred/v1/blee", Fields: render.Fields{"blee", "a,b,c", "fred"}}, r) +} + +func BenchmarkAlias(b *testing.B) { + o := render.AliasRes{ + GVR: "fred/v1/blee", + Aliases: []string{"a", "b", "c"}, + } + var a render.Alias + + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + var r render.Row + a.Render(o, "aliases", &r) + } +} diff --git a/internal/render/assets/sc.json b/internal/render/assets/sc.json new file mode 100644 index 00000000..afd1d892 --- /dev/null +++ b/internal/render/assets/sc.json @@ -0,0 +1,24 @@ +{ + "apiVersion": "storage.k8s.io/v1", + "kind": "StorageClass", + "metadata": { + "annotations": { + "storageclass.beta.kubernetes.io/is-default-class": "true" + }, + "creationTimestamp": "2019-02-05T22:04:14Z", + "labels": { + "addonmanager.kubernetes.io/mode": "EnsureExists", + "kubernetes.io/cluster-service": "true" + }, + "name": "standard", + "resourceVersion": "277", + "selfLink": "/apis/storage.k8s.io/v1/storageclasses/standard", + "uid": "f9d4c94a-2991-11e9-81cd-42010a80005b" + }, + "parameters": { + "type": "pd-standard" + }, + "provisioner": "kubernetes.io/gce-pd", + "reclaimPolicy": "Delete", + "volumeBindingMode": "Immediate" +} \ No newline at end of file diff --git a/internal/render/assets/sts.json b/internal/render/assets/sts.json new file mode 100644 index 00000000..35516896 --- /dev/null +++ b/internal/render/assets/sts.json @@ -0,0 +1,110 @@ +{ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"StatefulSet\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"nginx-sts\"},\"name\":\"nginx-sts\",\"namespace\":\"default\"},\"spec\":{\"replicas\":2,\"selector\":{\"matchLabels\":{\"app\":\"nginx-sts\"}},\"serviceName\":\"nginx-sts\",\"template\":{\"metadata\":{\"labels\":{\"app\":\"nginx-sts\"}},\"spec\":{\"containers\":[{\"image\":\"k8s.gcr.io/nginx-slim:0.8\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80,\"name\":\"web\"}],\"volumeMounts\":[{\"mountPath\":\"/usr/share/nginx/html\",\"name\":\"www\"}]}]}},\"volumeClaimTemplates\":[{\"metadata\":{\"name\":\"www\"},\"spec\":{\"accessModes\":[\"ReadWriteOnce\"],\"resources\":{\"requests\":{\"storage\":\"1Mi\"}}}}]}}\n" + }, + "creationTimestamp": "2019-11-30T15:41:42Z", + "generation": 5, + "labels": { + "app": "nginx-sts" + }, + "name": "nginx-sts", + "namespace": "default", + "resourceVersion": "82973198", + "selfLink": "/apis/apps/v1/namespaces/default/statefulsets/nginx-sts", + "uid": "e87310a8-1387-11ea-aa02-42010a800053" + }, + "spec": { + "podManagementPolicy": "OrderedReady", + "replicas": 4, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app": "nginx-sts" + } + }, + "serviceName": "nginx-sts", + "template": { + "metadata": { + "annotations": { + "kubectl.kubernetes.io/restartedAt": "2019-12-01T13:50:44-07:00" + }, + "creationTimestamp": null, + "labels": { + "app": "nginx-sts" + } + }, + "spec": { + "containers": [ + { + "image": "k8s.gcr.io/nginx-slim:0.8", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "name": "web", + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/usr/share/nginx/html", + "name": "www" + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + }, + "updateStrategy": { + "rollingUpdate": { + "partition": 0 + }, + "type": "RollingUpdate" + }, + "volumeClaimTemplates": [ + { + "metadata": { + "creationTimestamp": null, + "name": "www" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "dataSource": null, + "resources": { + "requests": { + "storage": "1Mi" + } + }, + "volumeMode": "Filesystem" + }, + "status": { + "phase": "Pending" + } + } + ] + }, + "status": { + "collisionCount": 0, + "currentReplicas": 4, + "currentRevision": "nginx-sts-5b89ffb894", + "observedGeneration": 5, + "readyReplicas": 4, + "replicas": 4, + "updateRevision": "nginx-sts-5b89ffb894", + "updatedReplicas": 4 + } +} \ No newline at end of file diff --git a/internal/render/colorer_test.go b/internal/render/colorer_test.go deleted file mode 100644 index 62d93dee..00000000 --- a/internal/render/colorer_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package render - -// BOZO!! -// type ( -// colorerUC struct { -// ns string -// r RowEvent -// e tcell.Color -// } -// colorerUCs []colorerUC -// ) - -// func TestDefaultColorer(t *testing.T) { -// uu := map[string]struct { -// re render.RowEvent -// e tcell.Color -// }{ -// "default": {render.RowEvent{}, ui.StdColor}, -// "add": {render.RowEvent{Kind: render.EventAdd}, ui.AddColor}, -// "delete": {render.RowEvent{Kind: render.EventDelete}, ui.KillColor}, -// "update": {render.RowEvent{Kind: render.EventUpdate}, ui.ModColor}, -// } - -// for k := range uu { -// u := uu[k] -// t.Run(k, func(t *testing.T) { -// assert.Equal(t, u.e, ui.DefaultColorer("", u.re)) -// }) -// } -// } - -// func TestEvColorer(t *testing.T) { -// var ( -// ns = Row{Fields: Fields{"", "blee", "fred", "Normal"}} -// nonNS = Row{Fields: Fields{"", "fred", "Normal"}} -// failNS = Row{Fields: Fields{"", "blee", "fred", "Failed"}} -// failNoNS = Row{Fields: Fields{"", "fred", "Failed"}} -// killNS = Row{Fields: Fields{"", "blee", "fred", "Killing"}} -// killNoNS = Row{Fields: Fields{"", "fred", "Killing"}} -// ) - -// uu := colorerUCs{ -// // Add AllNS -// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor}, -// // Add NS -// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor}, -// // Mod AllNS -// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor}, -// // Mod NS -// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor}, -// // Bust AllNS -// {"", RowEvent{Kind: EventUnchanged, Row: failNS}, ErrColor}, -// // Bust NS -// {"blee", RowEvent{Kind: EventUnchanged, Row: failNoNS}, ErrColor}, -// // Bust AllNS -// {"", RowEvent{Kind: EventUnchanged, Row: killNS}, KillColor}, -// // Bust NS -// {"blee", RowEvent{Kind: EventUnchanged, Row: killNoNS}, KillColor}, -// } -// for _, u := range uu { -// assert.Equal(t, u.e, evColorer(u.ns, u.r)) -// } -// } - -// func TestRSColorer(t *testing.T) { -// var ( -// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}} -// noNs = Row{Fields: Fields{"fred", "1", "1"}} -// bustNS = Row{Fields: Fields{"blee", "fred", "1", "0"}} -// bustNoNS = Row{Fields: Fields{"fred", "1", "0"}} -// ) - -// uu := colorerUCs{ -// // Add AllNS -// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor}, -// // Add NS -// {"blee", RowEvent{Kind: EventAdd, Row: noNs}, AddColor}, -// // Bust AllNS -// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor}, -// // Bust NS -// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor}, -// // Nochange AllNS -// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor}, -// // Nochange NS -// {"blee", RowEvent{Kind: EventUnchanged, Row: noNs}, StdColor}, -// } -// for _, u := range uu { -// assert.Equal(t, u.e, rsColorer(u.ns, u.r)) -// } -// } - -// func TestStsColorer(t *testing.T) { -// var ( -// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}} -// nonNS = Row{Fields: Fields{"fred", "1", "1"}} -// bustNS = Row{Fields: Fields{"blee", "fred", "2", "1"}} -// bustNoNS = Row{Fields: Fields{"fred", "2", "1"}} -// ) - -// uu := colorerUCs{ -// // Add AllNS -// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor}, -// // Add NS -// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor}, -// // Mod AllNS -// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor}, -// // Mod NS -// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor}, -// // Bust AllNS -// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor}, -// // Bust NS -// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor}, -// // Unchanged cool AllNS -// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor}, -// } -// for _, u := range uu { -// assert.Equal(t, u.e, stsColorer(u.ns, u.r)) -// } -// } - -// func TestDpColorer(t *testing.T) { -// var ( -// ns = Row{Fields: Fields{"blee", "fred", "1", "1"}} -// nonNS = Row{Fields: Fields{"fred", "1", "1"}} -// bustNS = Row{Fields: Fields{"blee", "fred", "2", "1"}} -// bustNoNS = Row{Fields: Fields{"fred", "2", "1"}} -// ) - -// uu := colorerUCs{ -// // Add AllNS -// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor}, -// // Add NS -// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor}, -// // Mod AllNS -// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor}, -// // Mod NS -// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor}, -// // Unchanged cool -// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor}, -// // Bust AllNS -// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor}, -// // Bust NS -// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor}, -// } -// for _, u := range uu { -// assert.Equal(t, u.e, dpColorer(u.ns, u.r)) -// } -// } - -// func TestPdbColorer(t *testing.T) { -// var ( -// ns = Row{Fields: Fields{"blee", "fred", "1", "1", "1", "1", "1"}} -// nonNS = Row{Fields: Fields{"fred", "1", "1", "1", "1", "1"}} -// bustNS = Row{Fields: Fields{"blee", "fred", "1", "1", "1", "1", "2"}} -// bustNoNS = Row{Fields: Fields{"fred", "1", "1", "1", "1", "2"}} -// ) - -// uu := colorerUCs{ -// // Add AllNS -// {"", RowEvent{Kind: EventAdd, Row: ns}, AddColor}, -// // Add NS -// {"blee", RowEvent{Kind: EventAdd, Row: nonNS}, AddColor}, -// // Mod AllNS -// {"", RowEvent{Kind: EventUpdate, Row: ns}, ModColor}, -// // Mod NS -// {"blee", RowEvent{Kind: EventUpdate, Row: nonNS}, ModColor}, -// // Unchanged cool -// {"", RowEvent{Kind: EventUnchanged, Row: ns}, StdColor}, -// // Bust AllNS -// {"", RowEvent{Kind: EventUnchanged, Row: bustNS}, ErrColor}, -// // Bust NS -// {"blee", RowEvent{Kind: EventUnchanged, Row: bustNoNS}, ErrColor}, -// } -// for _, u := range uu { -// assert.Equal(t, u.e, pdbColorer(u.ns, u.r)) -// } -// } - -// func TestPVColorer(t *testing.T) { -// var ( -// pv = Row{Fields: Fields{"blee", "1G", "RO", "Duh", "Bound"}} -// bustPv = Row{Fields: Fields{"blee", "1G", "RO", "Duh", "UnBound"}} -// ) - -// uu := colorerUCs{ -// // Add Normal -// {"", RowEvent{Kind: EventAdd, Row: pv}, AddColor}, -// // Unchanged Bound -// {"", RowEvent{Kind: EventUnchanged, Row: pv}, StdColor}, -// // Unchanged Bound -// {"", RowEvent{Kind: EventUnchanged, Row: bustPv}, ErrColor}, -// } -// for _, u := range uu { -// assert.Equal(t, u.e, pvColorer(u.ns, u.r)) -// } -// } - -// func TestPVCColorer(t *testing.T) { -// var ( -// pvc = Row{Fields: Fields{"blee", "fred", "Bound"}} -// bustPvc = Row{Fields: Fields{"blee", "fred", "UnBound"}} -// ) - -// uu := colorerUCs{ -// // Add Normal -// {"", RowEvent{Kind: EventAdd, Row: pvc}, AddColor}, -// // Add Bound -// {"", RowEvent{Kind: EventUnchanged, Row: bustPvc}, ErrColor}, -// } -// for _, u := range uu { -// assert.Equal(t, u.e, pvcColorer(u.ns, u.r)) -// } -// } - -// func TestCtxColorer(t *testing.T) { -// var ( -// ctx = Row{Fields: Fields{"blee"}} -// defCtx = Row{Fields: Fields{"blee*"}} -// ) - -// uu := colorerUCs{ -// // Add Normal -// {"", RowEvent{Kind: EventAdd, Row: ctx}, AddColor}, -// // Add Default -// {"", RowEvent{Kind: EventAdd, Row: defCtx}, AddColor}, -// // Mod Normal -// {"", RowEvent{Kind: EventUpdate, Row: ctx}, ModColor}, -// // Mod Default -// {"", RowEvent{Kind: EventUpdate, Row: defCtx}, ModColor}, -// // Unchanged Normal -// {"", RowEvent{Kind: EventUnchanged, Row: ctx}, StdColor}, -// // Unchanged Default -// {"", RowEvent{Kind: EventUnchanged, Row: defCtx}, HighlightColor}, -// } -// for _, u := range uu { -// assert.Equal(t, u.e, ctxColorer(u.ns, u.r)) -// } -// } - -// func TestPodColorer(t *testing.T) { -// var ( -// nsRow = Row{Fields: Fields{"blee", "fred", "1/1", "Running"}} -// toastNS = Row{Fields: Fields{"blee", "fred", "1/1", "Boom"}} -// notReadyNS = Row{Fields: Fields{"blee", "fred", "0/1", "Boom"}} -// row = Row{Fields: Fields{"fred", "1/1", "Running"}} -// toast = Row{Fields: Fields{"fred", "1/1", "Boom"}} -// notReady = Row{Fields: Fields{"fred", "0/1", "Boom"}} -// ) - -// uu := colorerUCs{ -// // Add allNS -// {"", RowEvent{Kind: EventAdd, Row: nsRow}, AddColor}, -// // Add Namespaced -// {"blee", RowEvent{Kind: EventAdd, Row: row}, AddColor}, -// // Mod AllNS -// {"", RowEvent{Kind: EventUpdate, Row: nsRow}, ModColor}, -// // Mod Namespaced -// {"blee", RowEvent{Kind: EventUpdate, Row: row}, ModColor}, -// // Mod Busted AllNS -// {"", RowEvent{Kind: EventUpdate, Row: toastNS}, ErrColor}, -// // Mod Busted Namespaced -// {"blee", RowEvent{Kind: EventUpdate, Row: toast}, ErrColor}, -// // NotReady AllNS -// {"", RowEvent{Kind: EventUpdate, Row: notReadyNS}, ErrColor}, -// // NotReady Namespaced -// {"blee", RowEvent{Kind: EventUpdate, Row: notReady}, ErrColor}, -// } -// for _, u := range uu { -// assert.Equal(t, u.e, podColorer(u.ns, u.r)) -// } -// } diff --git a/internal/render/container_test.go b/internal/render/container_test.go new file mode 100644 index 00000000..b3947e73 --- /dev/null +++ b/internal/render/container_test.go @@ -0,0 +1,116 @@ +package render_test + +import ( + "fmt" + "testing" + "time" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestContainer(t *testing.T) { + var c render.Container + + var cm coMX + var r render.Row + assert.Nil(t, c.Render(cm, "blee", &r)) + assert.Equal(t, "fred", r.ID) + assert.Equal(t, render.Fields{ + "fred", + "img", + "false", + "Running", + "false", + "0", + "off:off", + "10", + "20", + "50", + "20", + "", + }, + r.Fields[:len(r.Fields)-1], + ) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func toQty(s string) resource.Quantity { + q, _ := resource.ParseQuantity(s) + return q + +} + +type coMX struct{} + +var _ render.ContainerWithMetrics = coMX{} + +func (c coMX) Container() *v1.Container { + return makeContainer() +} + +func (c coMX) ContainerStatus() *v1.ContainerStatus { + return makeContainerStatus() +} + +func (c coMX) Metrics() *mv1beta1.ContainerMetrics { + return &mv1beta1.ContainerMetrics{ + Name: "fred", + Usage: v1.ResourceList{ + v1.ResourceCPU: toQty("10m"), + v1.ResourceMemory: toQty("20Mi"), + }, + } +} + +func (c coMX) Age() metav1.Time { + return metav1.Time{Time: testTime()} +} + +func (c coMX) IsInit() bool { + return false +} + +func makeContainer() *v1.Container { + return &v1.Container{ + Name: "fred", + Image: "img", + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + v1.ResourceCPU: toQty("20m"), + v1.ResourceMemory: toQty("100Mi"), + }, + }, + Env: []v1.EnvVar{ + { + Name: "fred", + Value: "1", + ValueFrom: &v1.EnvVarSource{ + ConfigMapKeyRef: &v1.ConfigMapKeySelector{Key: "blee"}, + }, + }, + }, + } +} + +func makeContainerStatus() *v1.ContainerStatus { + return &v1.ContainerStatus{ + Name: "fred", + State: v1.ContainerState{Running: &v1.ContainerStateRunning{}}, + RestartCount: 0, + } +} + +func testTime() time.Time { + t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00") + if err != nil { + fmt.Println("TestTime Failed", err) + } + return t +} diff --git a/internal/render/generic.go b/internal/render/generic.go index cd9e1742..3ac4f362 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -46,7 +46,7 @@ func (g *Generic) Header(ns string) HeaderRow { func (g *Generic) Render(o interface{}, ns string, r *Row) error { row, ok := o.(*metav1beta1.TableRow) if !ok { - return fmt.Errorf("expecting a table but got %#v", o) + return fmt.Errorf("expecting a TableRow but got %T", o) } count := len(row.Cells) @@ -57,8 +57,8 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { if !ok { return fmt.Errorf("expecting row id to be a string but got %#v", row.Cells[0]) } - r.Fields = make(Fields, count) + r.Fields = make(Fields, count) var index int if ns == AllNamespaces { rns, err := extractNamespace(row.Object.Raw) diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go new file mode 100644 index 00000000..236170c1 --- /dev/null +++ b/internal/render/generic_test.go @@ -0,0 +1,51 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestGenericRender(t *testing.T) { + var g render.Generic + + var r render.Row + row := makeGeneric().Rows[0] + assert.Nil(t, g.Render(&row, "blee", &r)) + + assert.Equal(t, "a", r.ID) + assert.Equal(t, render.Fields{"a", "b", "c"}, r.Fields) +} + +// Helpers... + +func makeGeneric() *metav1beta1.Table { + return &metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "A"}, + {Name: "B"}, + {Name: "C"}, + }, + Rows: []metav1beta1.TableRow{ + { + Object: runtime.RawExtension{ + Raw: []byte(`{ + "kind": "fred", + "apiVersion": "v1", + "metadata": { + "namespace": "blee", + "name": "fred" + }}`), + }, + Cells: []interface{}{ + "a", + "b", + "c", + }, + }, + }, + } +} diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go index 510f4807..9b8b6765 100644 --- a/internal/render/helpers_test.go +++ b/internal/render/helpers_test.go @@ -378,14 +378,3 @@ func BenchmarkAsPerc(b *testing.B) { AsPerc(v) } } - -// Helpers... - -// BOZO!! -// func testTime() time.Time { -// t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00") -// if err != nil { -// fmt.Println("TestTime Failed", err) -// } -// return t -// } diff --git a/internal/render/pod.go b/internal/render/pod.go index 42a0a48d..060f762c 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -159,12 +159,14 @@ func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, p metric) { func containerResources(co v1.Container) (cpu, mem *resource.Quantity) { req, limit := co.Resources.Requests, co.Resources.Limits + switch { case len(req) != 0: cpu, mem = req.Cpu(), req.Memory() case len(limit) != 0: cpu, mem = limit.Cpu(), limit.Memory() } + return } diff --git a/internal/render/policy.go b/internal/render/policy.go index b90e8b9f..ad6fbdcc 100644 --- a/internal/render/policy.go +++ b/internal/render/policy.go @@ -1,7 +1,11 @@ package render import ( + "fmt" + "github.com/gdamore/tcell" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" ) func rbacVerbHeader() HeaderRow { @@ -36,11 +40,81 @@ func (Policy) Header(ns string) HeaderRow { Header{Name: "API GROUP"}, Header{Name: "BINDING"}, } - return append(h, rbacVerbHeader()...) } // Render renders a K8s resource to screen. func (Policy) Render(o interface{}, gvr string, r *Row) error { + p, ok := o.(PolicyRes) + if !ok { + return fmt.Errorf("expecting PolicyRes but got %T", o) + } + + r.ID = FQN(p.Namespace, p.Resource) + r.Fields = append(r.Fields, p.Namespace, cleanseResource(p.Resource), p.Group, p.Binding) + r.Fields = append(r.Fields, asVerbs(p.Verbs)...) + return nil } + +// ---------------------------------------------------------------------------- +// Helpers... + +func cleanseResource(r string) string { + if r[0] == '/' { + return r + } + _, n := Namespaced(r) + return n +} + +type PolicyRes struct { + Namespace, Binding string + Resource, Group string + ResourceName string + NonResourceURL string + Verbs []string +} + +func NewPolicyRes(ns, binding, res, grp string, vv []string) PolicyRes { + return PolicyRes{ + Namespace: ns, + Binding: binding, + Resource: res, + Group: grp, + Verbs: vv, + } +} + +// GetObjectKind returns a schema object. +func (p PolicyRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (p PolicyRes) DeepCopyObject() runtime.Object { + return p +} + +type Policies []PolicyRes + +func (pp Policies) Upsert(p PolicyRes) Policies { + idx, ok := pp.findPol(p.Resource) + if !ok { + return append(pp, p) + } + pp[idx] = p + + return pp +} + +// Find locates a row by id. Retturns false is not found. +func (pp Policies) findPol(res string) (int, bool) { + for i, p := range pp { + if p.Resource == res { + return i, true + } + } + + return 0, false +} diff --git a/internal/render/policy_test.go b/internal/render/policy_test.go new file mode 100644 index 00000000..61a30ddf --- /dev/null +++ b/internal/render/policy_test.go @@ -0,0 +1,41 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestPolicyRender(t *testing.T) { + var p render.Policy + + var r render.Row + o := render.PolicyRes{ + Namespace: "blee", + Binding: "fred", + Resource: "res", + Group: "grp", + ResourceName: "bob", + NonResourceURL: "/blee", + Verbs: []string{"get", "list", "watch"}, + } + + assert.Nil(t, p.Render(o, "fred", &r)) + assert.Equal(t, "blee/res", r.ID) + assert.Equal(t, render.Fields{ + "blee", + "res", + "grp", + "fred", + "[green::b] ✓ [::]", + "[green::b] ✓ [::]", + "[green::b] ✓ [::]", + "[orangered::b] 𐄂 [::]", + "[orangered::b] 𐄂 [::]", + "[orangered::b] 𐄂 [::]", + "[orangered::b] 𐄂 [::]", + "[orangered::b] 𐄂 [::]", + "", + }, r.Fields) +} diff --git a/internal/render/port_forward_test.go b/internal/render/port_forward_test.go new file mode 100644 index 00000000..3ce102f1 --- /dev/null +++ b/internal/render/port_forward_test.go @@ -0,0 +1,59 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestPortForwardRender(t *testing.T) { + var p render.PortForward + var r render.Row + o := render.ForwardRes{ + Forwarder: fwd{}, + Config: render.BenchCfg{ + C: 1, + N: 1, + Host: "0.0.0.0", + Path: "/", + }, + } + + assert.Nil(t, p.Render(o, "fred", &r)) + assert.Equal(t, "blee/fred", r.ID) + assert.Equal(t, render.Fields{ + "blee", + "fred", + "co", + "p1", + "http://0.0.0.0:p1/", + "1", + "1", + "2m", + }, r.Fields) +} + +// Helpers... + +type fwd struct{} + +func (f fwd) Path() string { + return "blee/fred" +} + +func (f fwd) Container() string { + return "co" +} + +func (f fwd) Ports() []string { + return []string{"p1"} +} + +func (f fwd) Active() bool { + return true +} + +func (f fwd) Age() string { + return "2m" +} diff --git a/internal/render/rbac.go b/internal/render/rbac.go index 38f8464e..87396131 100644 --- a/internal/render/rbac.go +++ b/internal/render/rbac.go @@ -51,23 +51,19 @@ func (Rbac) Header(ns string) HeaderRow { // Render renders a K8s resource to screen. func (Rbac) Render(o interface{}, gvr string, r *Row) error { - p, ok := o.(*PolicyRes) + p, ok := o.(PolicyRes) if !ok { - return fmt.Errorf("expecting policyres in renderer for %q", gvr) + return fmt.Errorf("expecting RuleRes but got %T", o) } - if p.Group != "" { - p.Group = toGroup(p.Group) - } else { - p.Group = "core" - } - r.Fields = append(r.Fields, p.Resource, p.Group) - r.Fields = append(r.Fields, asVerbs(p.Verbs)...) r.ID = p.Resource + r.Fields = append(r.Fields, cleanseResource(p.Resource), p.Group) + r.Fields = append(r.Fields, asVerbs(p.Verbs)...) return nil } +// ---------------------------------------------------------------------------- // Helpers... func asVerbs(verbs []string) []string { @@ -120,26 +116,50 @@ func hasVerb(verbs []string, verb string) bool { return false } -func toGroup(g string) string { - if g == "" { - return "v1" - } - return g -} - -type PolicyRes struct { +type RuleRes struct { Resource, Group string ResourceName string NonResourceURL string Verbs []string } +func NewRuleRes(res, grp string, vv []string) RuleRes { + return RuleRes{ + Resource: res, + Group: grp, + Verbs: vv, + } +} + // GetObjectKind returns a schema object. -func (p PolicyRes) GetObjectKind() schema.ObjectKind { +func (r RuleRes) GetObjectKind() schema.ObjectKind { return nil } // DeepCopyObject returns a container copy. -func (p PolicyRes) DeepCopyObject() runtime.Object { - return p +func (r RuleRes) DeepCopyObject() runtime.Object { + return r +} + +type Rules []RuleRes + +func (rr Rules) Upsert(r RuleRes) Rules { + idx, ok := rr.find(r.Resource) + if !ok { + return append(rr, r) + } + rr[idx] = r + + return rr +} + +// Find locates a row by id. Retturns false is not found. +func (rr Rules) find(res string) (int, bool) { + for i, r := range rr { + if r.Resource == res { + return i, true + } + } + + return 0, false } diff --git a/internal/render/sc_test.go b/internal/render/sc_test.go new file mode 100644 index 00000000..70096e8d --- /dev/null +++ b/internal/render/sc_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestStorageClassRender(t *testing.T) { + c := render.StorageClass{} + r := render.NewRow(4) + c.Render(load(t, "sc"), "", &r) + + assert.Equal(t, "-/standard", r.ID) + assert.Equal(t, render.Fields{"standard", "kubernetes.io/gce-pd"}, r.Fields[:2]) +} diff --git a/internal/render/screen_dump_test.go b/internal/render/screen_dump_test.go new file mode 100644 index 00000000..ce6413ab --- /dev/null +++ b/internal/render/screen_dump_test.go @@ -0,0 +1,38 @@ +package render_test + +import ( + "os" + "testing" + "time" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestScreenDumpRender(t *testing.T) { + var s render.ScreenDump + var r render.Row + o := render.FileRes{ + File: fileInfo{}, + Dir: "fred/blee", + } + + assert.Nil(t, s.Render(o, "fred", &r)) + assert.Equal(t, "fred/blee/bob", r.ID) + assert.Equal(t, render.Fields{ + "bob", + }, r.Fields[:len(r.Fields)-1]) +} + +// Helpers... + +type fileInfo struct{} + +var _ os.FileInfo = fileInfo{} + +func (f fileInfo) Name() string { return "bob" } +func (f fileInfo) Size() int64 { return 100 } +func (f fileInfo) Mode() os.FileMode { return os.FileMode(644) } +func (f fileInfo) ModTime() time.Time { return testTime() } +func (f fileInfo) IsDir() bool { return false } +func (f fileInfo) Sys() interface{} { return nil } diff --git a/internal/render/sts_test.go b/internal/render/sts_test.go new file mode 100644 index 00000000..6fe8e4ae --- /dev/null +++ b/internal/render/sts_test.go @@ -0,0 +1,17 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + "github.com/stretchr/testify/assert" +) + +func TestStatefulSetRender(t *testing.T) { + c := render.StatefulSet{} + r := render.NewRow(4) + + assert.Nil(t, c.Render(load(t, "sts"), "", &r)) + assert.Equal(t, "default/nginx-sts", r.ID) + assert.Equal(t, render.Fields{"default", "nginx-sts", "4/4", "app=nginx-sts", "nginx-sts"}, r.Fields[:len(r.Fields)-1]) +} diff --git a/internal/render/subject.go b/internal/render/subject.go index a11e4dc2..505b42df 100644 --- a/internal/render/subject.go +++ b/internal/render/subject.go @@ -1,7 +1,11 @@ package render import ( + "fmt" + "github.com/gdamore/tcell" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" ) // Subject renders a rbac to screen. @@ -24,6 +28,36 @@ func (Subject) Header(ns string) HeaderRow { } // Render renders a K8s resource to screen. -func (Subject) Render(o interface{}, gvr string, r *Row) error { +func (s Subject) Render(o interface{}, ns string, r *Row) error { + res, ok := o.(SubjectRef) + if !ok { + return fmt.Errorf("Expected SubjectRef, but got %T", s) + } + + r.ID = res.Name + r.Fields = make(Fields, 0, len(s.Header(ns))) + r.Fields = append(r.Fields, + res.Name, + res.Kind, + res.FirstLocation, + ) + return nil } + +// ---------------------------------------------------------------------------- +// Helpers... + +type SubjectRef struct { + Name, Kind, FirstLocation string +} + +// GetObjectKind returns a schema object. +func (SubjectRef) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (s SubjectRef) DeepCopyObject() runtime.Object { + return s +} diff --git a/internal/render/table.go b/internal/render/table.go index 08de9276..cfb64ff1 100644 --- a/internal/render/table.go +++ b/internal/render/table.go @@ -6,11 +6,3 @@ type TableData struct { RowEvents RowEvents Namespace string } - -func (t TableData) Clone() TableData { - return TableData{ - Header: t.Header, - RowEvents: t.RowEvents.Clone(), - Namespace: t.Namespace, - } -} diff --git a/internal/render/yaml.go b/internal/render/yaml.go deleted file mode 100644 index ae62c08d..00000000 --- a/internal/render/yaml.go +++ /dev/null @@ -1,54 +0,0 @@ -package render - -// BOZO!! -// import ( -// "fmt" -// "regexp" -// "strings" - -// "github.com/derailed/k9s/internal/config" -// ) - -// var ( -// keyValRX = regexp.MustCompile(`\A(\s*)([\w|\-|\.|\/|\s]+):\s(.+)\z`) -// keyRX = regexp.MustCompile(`\A(\s*)([\w|\-|\.|\/|\s]+):\s*\z`) -// ) - -// const ( -// yamlFullFmt = "%s[key::b]%s[colon::-]: [val::]%s" -// yamlKeyFmt = "%s[key::b]%s[colon::-]:" -// yamlValueFmt = "[val::]%s" -// ) - -// // ColorizeYAML color YAML output. -// func ColorizeYAML(style config.Yaml, raw string) string { -// lines := strings.Split(raw, "\n") - -// fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.KeyColor, 1) -// fullFmt = strings.Replace(fullFmt, "[colon", "["+style.ColonColor, 1) -// fullFmt = strings.Replace(fullFmt, "[val", "["+style.ValueColor, 1) - -// keyFmt := strings.Replace(yamlKeyFmt, "[key", "["+style.KeyColor, 1) -// keyFmt = strings.Replace(keyFmt, "[colon", "["+style.ColonColor, 1) - -// valFmt := strings.Replace(yamlValueFmt, "[val", "["+style.ValueColor, 1) - -// buff := make([]string, 0, len(lines)) -// for _, l := range lines { -// res := keyValRX.FindStringSubmatch(l) -// if len(res) == 4 { -// buff = append(buff, fmt.Sprintf(fullFmt, res[1], res[2], res[3])) -// continue -// } - -// res = keyRX.FindStringSubmatch(l) -// if len(res) == 3 { -// buff = append(buff, fmt.Sprintf(keyFmt, res[1], res[2])) -// continue -// } - -// buff = append(buff, fmt.Sprintf(valFmt, l)) -// } - -// return strings.Join(buff, "\n") -// } diff --git a/internal/render/yaml_test.go b/internal/render/yaml_test.go deleted file mode 100644 index 45bdb417..00000000 --- a/internal/render/yaml_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package render - -// import ( -// "testing" - -// "github.com/derailed/k9s/internal/config" -// "github.com/stretchr/testify/assert" -// ) - -// func TestYaml(t *testing.T) { -// uu := []struct { -// s, e string -// }{ -// { -// `api: fred -// version: v1`, -// `[steelblue::b]api[white::-]: [papayawhip::]fred -// [steelblue::b]version[white::-]: [papayawhip::]v1`, -// }, -// { -// `api: -// version: v1`, -// `[steelblue::b]api[white::-]: -// [steelblue::b]version[white::-]: [papayawhip::]v1`, -// }, -// { -// " fred:blee", -// "[papayawhip::] fred:blee", -// }, -// { -// "fred blee: blee", -// "[steelblue::b]fred blee[white::-]: [papayawhip::]blee", -// }, -// { -// "Node-Selectors: ", -// "[steelblue::b]Node-Selectors[white::-]: [papayawhip::] ", -// }, -// { -// "fred.blee: ", -// "[steelblue::b]fred.blee[white::-]: [papayawhip::] ", -// }, -// { -// "certmanager.k8s.io/cluster-issuer: nameOfClusterIssuer", -// "[steelblue::b]certmanager.k8s.io/cluster-issuer[white::-]: [papayawhip::]nameOfClusterIssuer", -// }, -// } - -// s, _ := config.NewStyles("skins/stock.yml") -// for _, u := range uu { -// assert.Equal(t, u.e, ColorizeYAML(s.Views().Yaml, u.s)) -// } -// } diff --git a/internal/ui/flash.go b/internal/ui/flash.go index fe79ab2f..1efc2a69 100644 --- a/internal/ui/flash.go +++ b/internal/ui/flash.go @@ -124,7 +124,6 @@ func (v *FlashView) refresh(ctx1, ctx2 context.Context, cancel context.CancelFun case <-ctx2.Done(): v.app.QueueUpdateDraw(func() { v.Clear() - v.app.Draw() }) return } diff --git a/internal/ui/pages.go b/internal/ui/pages.go index 4514b3db..6b0a4283 100644 --- a/internal/ui/pages.go +++ b/internal/ui/pages.go @@ -66,7 +66,6 @@ func (p *Pages) StackPushed(c model.Component) { } func (p *Pages) StackPopped(o, top model.Component) { - log.Debug().Msgf("UI STACK POPPED!!!") p.delete(o) } diff --git a/internal/ui/table.go b/internal/ui/table.go index 0da8f270..a4a7e3d6 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -47,7 +47,7 @@ func NewTable(title string) *Table { actions: make(KeyActions), cmdBuff: NewCmdBuff('/', FilterBuff), BaseTitle: title, - sortCol: SortColumn{index: 0, colCount: 0, asc: true}, + sortCol: SortColumn{index: -1, colCount: 0, asc: true}, } } @@ -91,7 +91,7 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { if t.SearchBuff().IsActive() { t.SearchBuff().Add(evt.Rune()) t.ClearSelection() - t.doUpdate(t.filtered()) + t.doUpdate(t.filtered(t.Data), len(t.Data.RowEvents) > 0) t.UpdateTitle() t.SelectFirstRow() return nil @@ -112,7 +112,7 @@ func (t *Table) Hints() model.MenuHints { // GetFilteredData fetch filtered tabular data. func (t *Table) GetFilteredData() render.TableData { - return t.filtered() + return t.filtered(t.Data) } // SetDecorateFn specifies the default row decorator. @@ -132,31 +132,31 @@ func (t *Table) SetSortCol(index, count int, asc bool) { // Update table content. func (t *Table) Update(data render.TableData) { + var firstRow bool + if len(t.Data.RowEvents) == 0 { + firstRow = true + } t.Data = data + if t.decorateFn != nil { data = t.decorateFn(data) } - - if t.cmdBuff.Empty() { - t.doUpdate(data) - } else { - t.doUpdate(t.filtered()) + if !t.cmdBuff.Empty() { + data = t.filtered(data) } - + t.doUpdate(data, firstRow) t.UpdateTitle() - t.updateSelection(true) } -func (t *Table) doUpdate(data render.TableData) { +func (t *Table) doUpdate(data render.TableData, firstRow bool) { if data.Namespace == render.AllNamespaces { t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd(-2, true), false) } else { t.actions.Delete(KeyShiftP) } + t.Clear() - t.adjustSorter(data) - fg := config.AsColor(t.styles.GetTable().Header.FgColor) bg := config.AsColor(t.styles.GetTable().Header.BgColor) for col, h := range data.Header { @@ -172,6 +172,10 @@ func (t *Table) doUpdate(data render.TableData) { for i, r := range data.RowEvents { t.buildRow(data.Namespace, i+1, r, data.Header, pads) } + + if firstRow { + t.SelectFirstRow() + } t.updateSelection(false) } @@ -193,7 +197,6 @@ func (t *Table) SortColCmd(col int, asc bool) func(evt *tcell.EventKey) *tcell.E } t.sortCol.index = index t.Refresh() - return nil } } @@ -278,24 +281,22 @@ func (t *Table) AddHeaderCell(col int, h render.Header) { t.SetCell(0, col, c) } -func (t *Table) filtered() render.TableData { +func (t *Table) filtered(data render.TableData) render.TableData { if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.String()) { - return t.Data + return data } - q := t.cmdBuff.String() if isFuzzySelector(q) { - return fuzzyFilter(q[2:], t.NameColIndex(), t.Data) + return fuzzyFilter(q[2:], t.NameColIndex(), data) } - data, err := rxFilter(t.cmdBuff.String(), t.Data) + filtered, err := rxFilter(t.cmdBuff.String(), data) if err != nil { log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp") t.cmdBuff.Clear() - return t.Data + return data } - - return data + return filtered } // SearchBuff returns the associated command buffer. diff --git a/internal/view/browser.go b/internal/view/browser.go index 7ce292c1..16935515 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + rt "runtime" "strconv" "time" @@ -93,6 +94,8 @@ func (b *Browser) Init(ctx context.Context) error { func (b *Browser) Start() { b.Stop() + log.Debug().Msgf("GOROUTINE %d", rt.NumGoroutine()) + log.Debug().Msgf("BROWSER START %s", b.gvr) b.Table.Start() @@ -109,24 +112,28 @@ func (b *Browser) Stop() { } } -func (b *Browser) Refresh() { - b.refresh() +func (b *Browser) update(ctx context.Context) { + defer log.Debug().Msgf("UPDATER BAIL For %s", b.gvr) + for { + select { + case <-ctx.Done(): + log.Debug().Msgf("BROWSER <> -- %s", b.gvr) + return + case <-time.After(time.Duration(b.app.Config.K9s.GetRefreshRate()) * time.Second): + log.Debug().Msgf("GOROUTINE %d", rt.NumGoroutine()) + b.refresh() + } + } } // Name returns the component name. -func (b *Browser) Name() string { - return b.meta.Kind -} +func (b *Browser) Name() string { return b.meta.Kind } // SetContextFn populates a custom context. -func (b *Browser) SetContextFn(f ContextFunc) { - b.contextFn = f -} +func (b *Browser) SetContextFn(f ContextFunc) { b.contextFn = f } // SetBindKeysFn adds additional key bindings. -func (b *Browser) SetBindKeysFn(f BindKeysFunc) { - b.bindKeysFn = f -} +func (b *Browser) SetBindKeysFn(f BindKeysFunc) { b.bindKeysFn = f } // SetEnvFn sets a function to pull viewer env vars for plugins. func (b *Browser) SetEnvFn(f EnvFunc) { b.envFn = f } @@ -134,23 +141,8 @@ func (b *Browser) SetEnvFn(f EnvFunc) { b.envFn = f } // GVR returns a resource descriptor. func (b *Browser) GVR() string { return string(b.gvr) } -func (b *Browser) GetTable() *Table { - return b.Table -} - -func (b *Browser) update(ctx context.Context) { - for { - select { - case <-ctx.Done(): - log.Debug().Msgf("BROWSER <> -- %s", b.gvr) - return - case <-time.After(time.Duration(b.app.Config.K9s.GetRefreshRate()) * time.Second): - b.app.QueueUpdateDraw(func() { - b.refresh() - }) - } - } -} +// GetTable returns the underlying table. +func (b *Browser) GetTable() *Table { return b.Table } // ---------------------------------------------------------------------------- // Actions()... @@ -171,15 +163,12 @@ func (b *Browser) cpCmd(evt *tcell.EventKey) *tcell.EventKey { } func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey { - log.Debug().Msgf("GENERIC RES ENTER CMD FOR %q...", b.gvr) - // If in command mode run filter otherwise enter function. if b.filterCmd(evt) == nil || !b.RowSelected() { return nil } - f := b.defaultEnter + f := b.describeResource if b.enterFn != nil { - log.Debug().Msgf("Found custom enter") f = b.enterFn } f(b.app, b.Data.Namespace, string(b.gvr), b.GetSelectedItem()) @@ -188,7 +177,7 @@ func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey { } func (b *Browser) refreshCmd(*tcell.EventKey) *tcell.EventKey { - b.app.Flash().Info("Refreshinb...") + b.app.Flash().Info("Refreshing...") b.refresh() return nil } @@ -253,8 +242,17 @@ func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (b *Browser) defaultEnter(app *App, _, _, sel string) { - log.Debug().Msgf("--------- Resource %q Verbs %v", sel, b.meta.Verbs) +func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey { + log.Debug().Msgf("DESCRIBE %t -- %#v", b.RowSelected(), b.GetSelectedItems()) + if !b.RowSelected() { + return evt + } + b.describeResource(b.app, b.Data.Namespace, string(b.gvr), b.GetSelectedItem()) + + return nil +} + +func (b *Browser) describeResource(app *App, _, _, sel string) { ns, n := client.Namespaced(sel) yaml, err := dao.Describe(b.app.Conn(), b.gvr, ns, n) if err != nil { @@ -272,16 +270,6 @@ func (b *Browser) defaultEnter(app *App, _, _, sel string) { } } -func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey { - log.Debug().Msgf("DESCRIBE %t -- %#v", b.RowSelected(), b.GetSelectedItems()) - if !b.RowSelected() { - return evt - } - b.defaultEnter(b.app, b.Data.Namespace, string(b.gvr), b.GetSelectedItem()) - - return nil -} - func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey { if !b.RowSelected() { return evt @@ -394,27 +382,24 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { } func (b *Browser) refresh() { - log.Debug().Msgf("REFRESHING (%q) in ns %q -- %q", b.gvr, b.Data.Namespace, b.Path) - if b.app.Conn() == nil { - log.Error().Msg("No api connection") return } - ctx := b.defaultContext() if b.contextFn != nil { - log.Debug().Msgf("GOT CUSTOM CTX") ctx = b.contextFn(ctx) } if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" { b.Path = path } data, err := dao.Reconcile(ctx, b.Table.Data, b.gvr) - if err != nil { - b.app.Flash().Err(err) - } - b.refreshActions() - b.Update(data) + b.app.QueueUpdateDraw(func() { + if err != nil { + b.app.Flash().Err(err) + } + b.refreshActions() + b.Update(data) + }) } func (b *Browser) defaultContext() context.Context { @@ -429,6 +414,7 @@ func (b *Browser) defaultContext() context.Context { func (b *Browser) namespaceActions(aa ui.KeyActions) { if b.app.Conn() == nil || !b.meta.Namespaced || b.GetTable().Path != "" { + log.Warn().Msgf("NOT NAMESPACE RES %q -- %t -- %q", b.gvr, b.meta.Namespaced, b.GetTable().Path) return } b.namespaces = make(map[int]string, config.MaxFavoritesNS) @@ -471,6 +457,7 @@ func (b *Browser) refreshActions() { if b.bindKeysFn != nil { b.bindKeysFn(b.Actions()) } + b.app.Menu().HydrateMenu(b.Hints()) } func (b *Browser) customActions(aa ui.KeyActions) { diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index 7dd685e0..3f75a1aa 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -23,17 +23,6 @@ type clusterInfoView struct { mxs *client.MetricsServer } -// ClusterInfo tracks Kubernetes cluster and K9s information. -type ClusterInfo interface { - ContextName() string - ClusterName() string - UserName() string - K9sVersion() string - K8sVersion() string - CurrentCPU() float64 - CurrentMEM() float64 -} - func newClusterInfoView(app *App, mx *client.MetricsServer) *clusterInfoView { return &clusterInfoView{ app: app, diff --git a/internal/view/command.go b/internal/view/command.go index d4fb5f9e..9c29e63c 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -37,7 +37,7 @@ func (c *command) defaultCmd() error { return c.run(c.app.Config.ActiveView()) } -var authRX = regexp.MustCompile(`\Apol\s([u|g|s]):([\w-:]+)\b`) +var canRX = regexp.MustCompile(`\Acan\s([u|g|s]):([\w-:]+)\b`) func (c *command) isK9sCmd(cmd string) bool { cmds := strings.Split(cmd, " ") @@ -52,13 +52,15 @@ func (c *command) isK9sCmd(cmd string) bool { c.app.aliasCmd(nil) return true default: - if !authRX.MatchString(cmd) { + if !canRX.MatchString(cmd) { return false } - tokens := authRX.FindAllStringSubmatch(cmd, -1) + tokens := canRX.FindAllStringSubmatch(cmd, -1) if len(tokens) == 1 && len(tokens[0]) == 3 { - // BOZO!! - // c.app.inject(NewPolicy(c.app, tokens[0][1], tokens[0][2])) + if err := c.app.inject(NewPolicy(c.app, tokens[0][1], tokens[0][2])); err != nil { + log.Error().Err(err).Msgf("policy view load failed") + return false + } return true } } diff --git a/internal/view/container_test.go b/internal/view/container_test.go index 3323e34c..15e85d8c 100644 --- a/internal/view/container_test.go +++ b/internal/view/container_test.go @@ -9,9 +9,9 @@ import ( ) func TestContainerNew(t *testing.T) { - po := view.NewContainer(client.GVR("containers")) + c := view.NewContainer(client.GVR("containers")) - assert.Nil(t, po.Init(makeCtx())) - assert.Equal(t, "Containers", po.Name()) - assert.Equal(t, 17, len(po.Hints())) + assert.Nil(t, c.Init(makeCtx())) + assert.Equal(t, "Containers", c.Name()) + assert.Equal(t, 17, len(c.Hints())) } diff --git a/internal/view/group.go b/internal/view/group.go new file mode 100644 index 00000000..43ec7957 --- /dev/null +++ b/internal/view/group.go @@ -0,0 +1,48 @@ +package view + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" +) + +// Group presents a RBAC group viewer. +type Group struct { + ResourceViewer +} + +// NewGroup returns a new subject viewer. +func NewGroup(gvr client.GVR) ResourceViewer { + s := Group{ResourceViewer: NewBrowser(gvr)} + s.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) + s.SetBindKeysFn(s.bindKeys) + s.SetContextFn(s.subjectCtx) + return &s +} + +func (s *Group) bindKeys(aa ui.KeyActions) { + aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace) + aa.Add(ui.KeyActions{ + tcell.KeyEnter: ui.NewKeyAction("Rules", s.policyCmd, true), + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.GetTable().SortColCmd(1, true), false), + }) +} + +func (s *Group) subjectCtx(ctx context.Context) context.Context { + return context.WithValue(ctx, internal.KeySubjectKind, "Group") +} + +func (s *Group) policyCmd(evt *tcell.EventKey) *tcell.EventKey { + if !s.GetTable().RowSelected() { + return evt + } + if err := s.App().inject(NewPolicy(s.App(), "Group", s.GetTable().GetSelectedItem())); err != nil { + s.App().Flash().Err(err) + } + + return nil +} diff --git a/internal/view/page_stack.go b/internal/view/page_stack.go index 311b7b8b..9a8d8c0e 100644 --- a/internal/view/page_stack.go +++ b/internal/view/page_stack.go @@ -5,7 +5,6 @@ import ( "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" - "github.com/rs/zerolog/log" ) type PageStack struct { @@ -24,26 +23,17 @@ func (p *PageStack) Init(ctx context.Context) (err error) { if p.app, err = extractApp(ctx); err != nil { return err } - p.Stack.AddListener(p) return nil } func (p *PageStack) StackPushed(c model.Component) { - log.Debug().Msgf("Stack PUSHED!!!") - // ctx := context.WithValue(context.Background(), ui.KeyApp, p.app) - // if err := c.Init(ctx); err != nil { - // log.Error().Err(err).Msgf("Component Init failed!") - // p.app.Flash().Err(err) - // return - // } c.Start() p.app.SetFocus(c) } func (p *PageStack) StackPopped(o, top model.Component) { - log.Debug().Msgf("PS STACK POPPED!!!") o.Stop() p.StackTop(top) } diff --git a/internal/view/policy.go b/internal/view/policy.go index 9b564741..098711eb 100644 --- a/internal/view/policy.go +++ b/internal/view/policy.go @@ -1,8 +1,9 @@ package view import ( - "strings" + "context" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" @@ -16,34 +17,41 @@ const ( allVerbs = "*" ) -// Policy presents a RBAC policy viewer. +// Policy presents a RBAC rules viewer. type Policy struct { ResourceViewer + + subjectKind, subjectName string } // NewPolicy returns a new viewer. -func NewPolicy(gvr client.GVR) *Policy { +func NewPolicy(app *App, subject, name string) *Policy { p := Policy{ - ResourceViewer: NewBrowser(gvr), + ResourceViewer: NewBrowser(client.GVR("policy")), + subjectKind: subject, + subjectName: name, } p.GetTable().SetColorerFn(render.Policy{}.ColorerFunc()) p.SetBindKeysFn(p.bindKeys) p.GetTable().SetSortCol(1, len(render.Policy{}.Header(render.AllNamespaces)), false) + p.SetContextFn(p.subjectCtx) + p.GetTable().SetEnterFn(blankEnterFn) return &p } -func (p *Policy) Name() string { - return "policy" +func (p *Policy) subjectCtx(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, internal.KeySubjectKind, mapSubject(p.subjectKind)) + ctx = context.WithValue(ctx, internal.KeyPath, mapSubject(p.subjectKind)+":"+p.subjectName) + return context.WithValue(ctx, internal.KeySubjectName, p.subjectName) } func (p *Policy) bindKeys(aa ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) aa.Add(ui.KeyActions{ - ui.KeyShiftP: ui.NewKeyAction("Sort Namespace", p.GetTable().SortColCmd(0, true), false), - ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.GetTable().SortColCmd(1, true), false), - ui.KeyShiftO: ui.NewKeyAction("Sort Group", p.GetTable().SortColCmd(2, true), false), - ui.KeyShiftB: ui.NewKeyAction("Sort Binding", p.GetTable().SortColCmd(3, true), false), + ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.GetTable().SortColCmd(0, true), false), + ui.KeyShiftO: ui.NewKeyAction("Sort Group", p.GetTable().SortColCmd(1, true), false), + ui.KeyShiftB: ui.NewKeyAction("Sort Binding", p.GetTable().SortColCmd(2, true), false), }) } @@ -53,57 +61,9 @@ func mapSubject(subject string) string { return group case "s": return sa - default: + case "u": return user + default: + return subject } } - -func hasVerb(verbs []string, verb string) bool { - if len(verbs) == 1 && verbs[0] == allVerbs { - 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 toVerbIcon(ok bool) string { - if ok { - return "[green::b] ✓ [::]" - } - return "[orangered::b] 𐄂 [::]" -} - -func asVerbs(verbs []string) []string { - const ( - verbLen = 4 - unknownLen = 30 - ) - - r := make([]string, 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 != allVerbs { - unknowns = append(unknowns, v) - } - } - - return append(r, render.Truncate(strings.Join(unknowns, ","), unknownLen)) -} diff --git a/internal/view/rbac.go b/internal/view/rbac.go index 12770d8c..e72002ea 100644 --- a/internal/view/rbac.go +++ b/internal/view/rbac.go @@ -8,34 +8,8 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" ) -const ( - ClusterRole roleKind = iota - Role -) - -var ( - k8sVerbs = []string{ - "get", - "list", - "watch", - "create", - "patch", - "update", - "delete", - "deletecollection", - } - - httpTok8sVerbs = map[string]string{ - "post": "create", - "put": "update", - } -) - -type roleKind = int8 - // Rbac presents an RBAC policy viewer. type Rbac struct { ResourceViewer @@ -43,13 +17,13 @@ type Rbac struct { // NewRbac returns a new viewer. func NewRbac(gvr client.GVR) ResourceViewer { - log.Debug().Msgf(">>>>> NEWRBAC %v!!!!!", gvr) r := Rbac{ ResourceViewer: NewBrowser(gvr), } r.GetTable().SetColorerFn(render.Rbac{}.ColorerFunc()) r.SetBindKeysFn(r.bindKeys) r.GetTable().SetSortCol(1, len(render.Rbac{}.Header(render.ClusterScope)), true) + r.GetTable().SetEnterFn(blankEnterFn) return &r } @@ -61,31 +35,7 @@ func (r *Rbac) bindKeys(aa ui.KeyActions) { }) } -// BOZO!! -// func showClusterRoleBinding(app *App, ns, gvr, path string) { -// o, err := app.factory.Get("rbac.authorization.k8s.io/v1/clusterrolebindings", path, labels.Everything()) -// if err != nil { -// app.Flash().Err(err) -// return -// } - -// var crb rbacv1.ClusterRoleBinding -// err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb) -// if err != nil { -// app.Flash().Errf("Unable to retrieve clusterrolebindings for %s", path) -// return -// } - -// // BOZO!! Must make sure cluster roles are in cache prior to loading rbac view. -// app.factory.ForResource("-", "rbac.authorization.k8s.io/v1/clusterroles") -// app.factory.WaitForCacheSync() - -// // BOZO!! -// // app.inject(NewRbac(crb.RoleRef.Name, ClusterRole, selection)) -// } - -func showRBAC(app *App, _, gvr, path string) { - log.Debug().Msgf("Showing RBAC %q--%q", gvr, path) +func showRules(app *App, _, gvr, path string) { v := NewRbac(client.GVR("rbac")) v.SetContextFn(rbacCtxt(gvr, path)) @@ -100,3 +50,5 @@ func rbacCtxt(gvr, path string) ContextFunc { return context.WithValue(ctx, internal.KeyGVR, gvr) } } + +func blankEnterFn(_ *App, _, _, _ string) {} diff --git a/internal/view/rbac_int_test.go b/internal/view/rbac_int_test.go deleted file mode 100644 index 79da5c7c..00000000 --- a/internal/view/rbac_int_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package view - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -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, e []string - }{ - { - []string{"*"}, - []string{ok, ok, ok, ok, ok, ok, ok, ok, ""}, - }, - { - []string{"get", "list", "patch"}, - []string{ok, ok, nok, nok, ok, nok, nok, nok, ""}, - }, - { - []string{"get", "list", "deletecollection", "post"}, - []string{ok, ok, nok, ok, nok, nok, nok, ok, ""}, - }, - { - []string{"get", "list", "blee"}, - []string{ok, ok, nok, nok, nok, nok, nok, nok, "blee"}, - }, - } - - for _, u := range uu { - assert.Equal(t, u.e, asVerbs(u.vv)) - } -} diff --git a/internal/view/registrar.go b/internal/view/registrar.go index 944927d6..502bd694 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -88,6 +88,12 @@ func miscRes(vv MetaViewers) { vv["aliases"] = MetaViewer{ viewerFn: NewAlias, } + vv["users"] = MetaViewer{ + viewerFn: NewUser, + } + vv["groups"] = MetaViewer{ + viewerFn: NewGroup, + } } func appsRes(vv MetaViewers) { @@ -110,19 +116,19 @@ func appsRes(vv MetaViewers) { func rbacRes(vv MetaViewers) { vv["rbac"] = MetaViewer{ - enterFn: showRBAC, + enterFn: showRules, } vv["rbac.authorization.k8s.io/v1/clusterroles"] = MetaViewer{ - enterFn: showRBAC, + enterFn: showRules, } vv["rbac.authorization.k8s.io/v1/roles"] = MetaViewer{ - enterFn: showRBAC, + enterFn: showRules, } vv["rbac.authorization.k8s.io/v1/clusterrolebindings"] = MetaViewer{ - enterFn: showRBAC, + enterFn: showRules, } vv["rbac.authorization.k8s.io/v1/rolebindings"] = MetaViewer{ - enterFn: showRBAC, + enterFn: showRules, } } diff --git a/internal/view/subject.go b/internal/view/subject.go index 5fa1c66c..4db71477 100644 --- a/internal/view/subject.go +++ b/internal/view/subject.go @@ -1,6 +1,9 @@ package view import ( + "context" + + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" @@ -29,7 +32,7 @@ func NewSubject(gvr client.GVR) ResourceViewer { // BOZO!! // s.GetTable().SetSortCol(1, len(s.Header()), true) s.SetBindKeysFn(s.bindKeys) - + s.SetContextFn(s.subjectCtx) return &s } @@ -46,6 +49,10 @@ func (s *Subject) bindKeys(aa ui.KeyActions) { }) } +func (s *Subject) subjectCtx(ctx context.Context) context.Context { + return context.WithValue(ctx, internal.KeySubjectKind, mapSubject(s.subjectKind)) +} + // SetSubject sets the subject name. func (s *Subject) SetSubject(n string) { s.subjectKind = mapSubject(n) diff --git a/internal/view/table.go b/internal/view/table.go index a23b47ef..811ac996 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -3,7 +3,6 @@ package view import ( "context" - "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" @@ -58,17 +57,8 @@ func (t *Table) Stop() { t.SearchBuff().RemoveListener(t) } -// MasterComponent returns the master component. -func (t *Table) MasterComponent() model.Component { - return t -} - // SetEnterFn specifies the default enter behavior. func (t *Table) SetEnterFn(f EnterFunc) { - if f == nil { - return - } - log.Debug().Msgf("Setting ENTERFN on %s -- %v", t.BaseTitle, f) t.enterFn = f } @@ -153,12 +143,10 @@ func (t *Table) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { } func (t *Table) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - log.Debug().Msgf("Table Escape") if !t.SearchBuff().InCmdMode() { t.SearchBuff().Reset() return t.app.PrevCmd(evt) } - log.Debug().Msgf("\tClearing filter") if ui.IsLabelSelector(t.SearchBuff().String()) { t.filterFn("") } diff --git a/internal/view/user.go b/internal/view/user.go new file mode 100644 index 00000000..6969eed7 --- /dev/null +++ b/internal/view/user.go @@ -0,0 +1,48 @@ +package view + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" +) + +// User presents a user viewer. +type User struct { + ResourceViewer +} + +// NewUser returns a new subject viewer. +func NewUser(gvr client.GVR) ResourceViewer { + s := User{ResourceViewer: NewBrowser(gvr)} + s.GetTable().SetColorerFn(render.Subject{}.ColorerFunc()) + s.SetBindKeysFn(s.bindKeys) + s.SetContextFn(s.subjectCtx) + return &s +} + +func (s *User) bindKeys(aa ui.KeyActions) { + aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace) + aa.Add(ui.KeyActions{ + tcell.KeyEnter: ui.NewKeyAction("Rules", s.policyCmd, true), + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.GetTable().SortColCmd(1, true), false), + }) +} + +func (s *User) subjectCtx(ctx context.Context) context.Context { + return context.WithValue(ctx, internal.KeySubjectKind, "User") +} + +func (s *User) policyCmd(evt *tcell.EventKey) *tcell.EventKey { + if !s.GetTable().RowSelected() { + return evt + } + if err := s.App().inject(NewPolicy(s.App(), "User", s.GetTable().GetSelectedItem())); err != nil { + s.App().Flash().Err(err) + } + + return nil +} diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 06ed861f..017505fc 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -15,6 +15,7 @@ import ( "k8s.io/client-go/informers" ) +// Factory - *factories(ns) -> *informers const ( defaultResync = 10 * time.Minute allNamespaces = "" @@ -73,21 +74,19 @@ func (f *Factory) List(gvr, ns string, sel labels.Selector) ([]runtime.Object, e return nil, fmt.Errorf("User has insufficient access to list %s", gvr) } - log.Debug().Msgf(">>> FACTORY LISTING %q -- %q", ns, gvr) inf := f.ForResource(ns, gvr) if inf == nil { return nil, fmt.Errorf("No resource for GVR %s", gvr) } - if ns == clusterScope { return inf.Lister().List(sel) } + return inf.Lister().ByNamespace(ns).List(sel) } func (f *Factory) Get(gvr, path string, sel labels.Selector) (runtime.Object, error) { ns, n := namespaced(path) - log.Debug().Msgf(">>> FACTORY GET %q --- %q:%q -- %q", gvr, ns, n, path) auth, err := f.Client().CanI(ns, gvr, []string{"get"}) if err != nil { return nil, err @@ -96,31 +95,22 @@ func (f *Factory) Get(gvr, path string, sel labels.Selector) (runtime.Object, er return nil, fmt.Errorf("User has insufficient access to get %s", gvr) } - fac := f.ensureFactory(ns) - log.Debug().Msgf("GVR: %#v", toGVR(gvr)) - inf := fac.ForResource(toGVR(gvr)) + inf := f.ForResource(ns, gvr) if inf == nil { return nil, fmt.Errorf("No resource for GVR %s", gvr) } - if ns == clusterScope { return inf.Lister().Get(n) } + + log.Debug().Msgf("GET %q--%q:%q", gvr, ns, path) return inf.Lister().ByNamespace(ns).Get(n) } -func (f *Factory) WaitForCacheSync() map[schema.GroupVersionResource]bool { - r := make(map[schema.GroupVersionResource]bool) - for n, fac := range f.factories { - log.Debug().Msgf(">>> WAITING FOR FACTORY SYNC -- %q", n) - res := fac.WaitForCacheSync(f.stopChan) - for k, v := range res { - r[k] = v - log.Debug().Msgf(" GVR resource %v -- %v", k, v) - } - log.Debug().Msgf("<<< DONE!") +func (f *Factory) WaitForCacheSync() { + for _, fac := range f.factories { + fac.WaitForCacheSync(f.stopChan) } - return r } func (f *Factory) Init() { @@ -172,13 +162,13 @@ func (f *Factory) Start(stopChan chan struct{}) { // BOZO!! Check ns access for resource?? func (f *Factory) SetActive(ns string) { - if !f.isclusterScope() { + if !f.isClusterWide() { f.ensureFactory(ns) } f.activeNS = ns } -func (f *Factory) isclusterScope() bool { +func (f *Factory) isClusterWide() bool { _, ok := f.factories[allNamespaces] return ok } @@ -207,7 +197,7 @@ func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer { } func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory { - if f.isclusterScope() { + if f.isClusterWide() { ns = allNamespaces } if fac, ok := f.factories[ns]; ok {