checkpoint

mine
derailed 2019-12-24 22:38:54 -07:00
parent 4127da865f
commit add0d678f0
59 changed files with 1428 additions and 1163 deletions

View File

@ -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()
}

View File

@ -60,6 +60,18 @@ func (g GVR) ToV() string {
return tokens[len(tokens)-2] 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. // ToR returns the resource name.
func (g GVR) ToR() string { func (g GVR) ToR() string {
tokens := strings.Split(string(g), "/") tokens := strings.Split(string(g), "/")
@ -77,6 +89,7 @@ func (g GVR) ToG() string {
} }
} }
//
type GVRs []GVR type GVRs []GVR
func (g GVRs) Len() int { func (g GVRs) Len() int {

View File

@ -1,6 +1,7 @@
package client_test package client_test
import ( import (
"sort"
"testing" "testing"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
@ -8,6 +9,52 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "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) { func TestAsGV(t *testing.T) {
uu := map[string]struct { uu := map[string]struct {
gvr string 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 { uu := map[string]struct {
gvr string gvr string
}{ }{

View File

@ -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))
}
}

View File

@ -12,10 +12,10 @@ import (
var toFileName = regexp.MustCompile(`[^(\w/\.)]`) var toFileName = regexp.MustCompile(`[^(\w/\.)]`)
// Namespaced converts a resource path to namespace and resource name. // Namespaced converts a resource path to namespace and resource name.
func Namespaced(n string) (string, string) { func Namespaced(p string) (string, string) {
ns, po := path.Split(n) ns, n := path.Split(p)
return strings.Trim(ns, "/"), po return strings.Trim(ns, "/"), n
} }
// FQN returns a fully qualified resource name. // FQN returns a fully qualified resource name.

View File

@ -1,8 +1,5 @@
package config package config
// BOZO!! Once yaml is stable implement validation
// go get gopkg.in/validator.v2
import ( import (
"errors" "errors"
"fmt" "fmt"

View File

@ -2,6 +2,7 @@ package dao
import ( import (
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
) )
@ -24,9 +25,12 @@ func (g *Generic) Delete(path string, cascade, force bool) error {
} }
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
return g.dynClient().Namespace(ns).Delete(n, &metav1.DeleteOptions{ log.Debug().Msgf("DELETING %q:%q -- %q", ns, n, path)
PropagationPolicy: &p, 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 { func (g *Generic) dynClient() dynamic.NamespaceableResourceInterface {

View File

@ -15,14 +15,14 @@ import (
// Reconcile previous vs current state and emits delta events. // Reconcile previous vs current state and emits delta events.
func Reconcile(ctx context.Context, table render.TableData, gvr client.GVR) (render.TableData, error) { func Reconcile(ctx context.Context, table render.TableData, gvr client.GVR) (render.TableData, error) {
defer func(t time.Time) { 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()) }(time.Now())
path, ok := ctx.Value(internal.KeyPath).(string) path, ok := ctx.Value(internal.KeyPath).(string)
if !ok { if !ok {
return table, fmt.Errorf("no path specified for %s", gvr) 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) factory, ok := ctx.Value(internal.KeyFactory).(Factory)
if !ok { if !ok {
return table, fmt.Errorf("no factory found for %s", gvr) return table, fmt.Errorf("no factory found for %s", gvr)

View File

@ -45,7 +45,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
r, ok := m[gvr] r, ok := m[gvr]
if !ok { if !ok {
r = &Generic{} 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) r.Init(f, gvr)
@ -57,7 +57,7 @@ func RegisterMeta(gvr string, res metav1.APIResource) {
resMetas[client.GVR(gvr)] = res resMetas[client.GVR(gvr)] = res
} }
func AllGVRs() []client.GVR { func AllGVRs() client.GVRs {
kk := make(client.GVRs, 0, len(resMetas)) kk := make(client.GVRs, 0, len(resMetas))
for k := range resMetas { for k := range resMetas {
kk = append(kk, k) kk = append(kk, k)
@ -137,7 +137,13 @@ func loadNonResource(m ResourceMetas) error {
} }
m["rbac"] = metav1.APIResource{ m["rbac"] = metav1.APIResource{
Name: "Rbac", Name: "Rbac",
Kind: "RBAC", Kind: "Rules",
Categories: []string{"k9s"},
}
m["policy"] = metav1.APIResource{
Name: "Policy",
Kind: "Rules",
Namespaced: true,
Categories: []string{"k9s"}, Categories: []string{"k9s"},
} }
m["containers"] = metav1.APIResource{ m["containers"] = metav1.APIResource{

View File

@ -8,7 +8,6 @@ import (
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
restclient "k8s.io/client-go/rest" restclient "k8s.io/client-go/rest"
) )
@ -27,7 +26,7 @@ type Factory interface {
ForResource(ns, gvr string) informers.GenericInformer ForResource(ns, gvr string) informers.GenericInformer
// WaitForCacheSync synchronize the cache. // WaitForCacheSync synchronize the cache.
WaitForCacheSync() map[schema.GroupVersionResource]bool WaitForCacheSync()
// DeleteForwarder deletes a pod forwarder. // DeleteForwarder deletes a pod forwarder.
DeleteForwarder(path string) DeleteForwarder(path string)

View File

@ -5,17 +5,19 @@ type ContextKey string
// A collection of context keys. // A collection of context keys.
const ( const (
KeyFactory ContextKey = "factory" KeyFactory ContextKey = "factory"
KeyLabels ContextKey = "labels" KeyLabels ContextKey = "labels"
KeyFields ContextKey = "fields" KeyFields ContextKey = "fields"
KeyTable ContextKey = "table" KeyTable ContextKey = "table"
KeyDir ContextKey = "dir" KeyDir ContextKey = "dir"
KeyPath ContextKey = "path" KeyPath ContextKey = "path"
KeySubject ContextKey = "subject" KeySubject ContextKey = "subject"
KeyGVR ContextKey = "gvr" KeyGVR ContextKey = "gvr"
KeyForwards ContextKey = "forwards" KeyForwards ContextKey = "forwards"
KeyContainers ContextKey = "containers" KeyContainers ContextKey = "containers"
KeyBenchCfg ContextKey = "benchcfg" KeyBenchCfg ContextKey = "benchcfg"
KeyAliases ContextKey = "aliases" KeyAliases ContextKey = "aliases"
KeyUID ContextKey = "uid" KeyUID ContextKey = "uid"
KeySubjectKind ContextKey = "subjectKind"
KeySubjectName ContextKey = "subjectName"
) )

View File

@ -7,17 +7,6 @@ import (
) )
type ( 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 gather metrics information from pods and nodes.
MetricsServer interface { MetricsServer interface {
MetricsService MetricsService
@ -36,34 +25,34 @@ type (
// Cluster represents a kubernetes resource. // Cluster represents a kubernetes resource.
Cluster struct { Cluster struct {
api ClusterMeta client client.Connection
mx MetricsServer mx MetricsServer
} }
) )
// NewCluster returns a new cluster info resource. // NewCluster returns a new cluster info resource.
func NewCluster(c client.Connection, mx MetricsServer) *Cluster { func NewCluster(c client.Connection, mx MetricsServer) *Cluster {
return NewClusterWithArgs(client.NewCluster(c), mx) return NewClusterWithArgs(c, mx)
} }
// NewClusterWithArgs for tests only! // NewClusterWithArgs for tests only!
func NewClusterWithArgs(ci ClusterMeta, mx MetricsServer) *Cluster { func NewClusterWithArgs(c client.Connection, mx MetricsServer) *Cluster {
return &Cluster{api: ci, mx: mx} return &Cluster{client: c, mx: mx}
} }
// Version returns the current K8s cluster version. // Version returns the current K8s cluster version.
func (c *Cluster) Version() string { func (c *Cluster) Version() string {
info, err := c.api.Version() info, err := c.client.ServerVersion()
if err != nil { if err != nil {
return "n/a" return "n/a"
} }
return info return info.GitVersion
} }
// ContextName returns the context name. // ContextName returns the context name.
func (c *Cluster) ContextName() string { func (c *Cluster) ContextName() string {
n, err := c.api.ContextName() n, err := c.client.Config().CurrentContextName()
if err != nil { if err != nil {
return "n/a" return "n/a"
} }
@ -72,7 +61,7 @@ func (c *Cluster) ContextName() string {
// ClusterName returns the cluster name. // ClusterName returns the cluster name.
func (c *Cluster) ClusterName() string { func (c *Cluster) ClusterName() string {
n, err := c.api.ClusterName() n, err := c.client.Config().CurrentClusterName()
if err != nil { if err != nil {
return "n/a" return "n/a"
} }
@ -81,7 +70,7 @@ func (c *Cluster) ClusterName() string {
// UserName returns the user name. // UserName returns the user name.
func (c *Cluster) UserName() string { func (c *Cluster) UserName() string {
n, err := c.api.UserName() n, err := c.client.Config().CurrentUserName()
if err != nil { if err != nil {
return "n/a" return "n/a"
} }

View File

@ -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,
}
}

View File

@ -3,6 +3,7 @@ package model
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
@ -26,6 +27,10 @@ type Generic struct {
// List returns a collection of node resources. // List returns a collection of node resources.
func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { 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 // Ensures the factory is tracking this resource
_ = g.factory.ForResource(g.namespace, g.gvr) _ = 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) table, ok := o.(*metav1beta1.Table)
if !ok { 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 g.table = table
res := make([]runtime.Object, len(g.table.Rows)) 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. // Hydrate returns nodes as rows.
func (g *Generic) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { 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) gr, ok := re.(*render.Generic)
if !ok { if !ok {
return fmt.Errorf("expecting generic renderer for %s but got %T", g.gvr, re) return fmt.Errorf("expecting generic renderer for %s but got %T", g.gvr, re)

237
internal/model/policy.go Normal file
View File

@ -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
}

View File

@ -7,17 +7,25 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
rbacv1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "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 { type Rbac struct {
Resource Resource
} }
// List lists out rbac resources.
func (r *Rbac) List(ctx context.Context) ([]runtime.Object, error) { func (r *Rbac) List(ctx context.Context) ([]runtime.Object, error) {
gvr, ok := ctx.Value(internal.KeyGVR).(string) gvr, ok := ctx.Value(internal.KeyGVR).(string)
if !ok { if !ok {
@ -25,7 +33,6 @@ func (r *Rbac) List(ctx context.Context) ([]runtime.Object, error) {
} }
r.gvr = gvr r.gvr = gvr
path, ok := ctx.Value(internal.KeyPath).(string) path, ok := ctx.Value(internal.KeyPath).(string)
log.Debug().Msgf("LISTING RBACK %q--%q", r.gvr, path)
if !ok || path == "" { if !ok || path == "" {
return r.Resource.List(ctx) 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -56,8 +64,7 @@ func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) {
return nil, err return nil, err
} }
kind := "rbac.authorization.k8s.io/v1/clusterroles" crbo, err := r.factory.Get(crGVR, client.FQN("-", crb.RoleRef.Name), labels.Everything())
crbo, err := r.factory.Get(kind, client.FQN("-", crb.RoleRef.Name), labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -66,11 +73,12 @@ func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) {
if err != nil { if err != nil {
return nil, err 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -81,8 +89,7 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
} }
if rb.RoleRef.Kind == "ClusterRole" { if rb.RoleRef.Kind == "ClusterRole" {
kind := "rbac.authorization.k8s.io/v1/clusterroles" o, e := r.factory.Get(crGVR, client.FQN("-", rb.RoleRef.Name), labels.Everything())
o, e := r.factory.Get(kind, client.FQN("-", rb.RoleRef.Name), labels.Everything())
if e != nil { if e != nil {
return nil, e return nil, e
} }
@ -91,11 +98,10 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
if err != nil { if err != nil {
return nil, err 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(rGVR, client.FQN(rb.Namespace, rb.RoleRef.Name), labels.Everything())
ro, err := r.factory.Get(kind, client.FQN(rb.Namespace, rb.RoleRef.Name), labels.Everything())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -105,11 +111,11 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
return nil, err 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -120,11 +126,11 @@ func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) {
return nil, err 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -135,65 +141,14 @@ func (r *Rbac) loadRole(path string) ([]runtime.Object, error) {
return nil, err 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 { func asRuntimeObjects(rr render.Policies) []runtime.Object {
return &render.PolicyRes{ oo := make([]runtime.Object, len(rr))
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) {
for i, r := range rr { for i, r := range rr {
p, ok := r.(*render.PolicyRes) oo[i] = r
if !ok {
log.Error().Err(fmt.Errorf("expecting policyres but got `%T", r))
return 0, false
}
if p.Resource == res {
return i, true
}
} }
return 0, false return oo
} }

View File

@ -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)
// }
// }
// }

View File

@ -23,6 +23,18 @@ var Registry = map[string]ResourceMeta{
Model: &Rbac{}, Model: &Rbac{},
Renderer: &render.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{ "portforwards": ResourceMeta{
Model: &PortForward{}, Model: &PortForward{},
Renderer: &render.PortForward{}, Renderer: &render.PortForward{},

View File

@ -2,6 +2,7 @@ package model
import ( import (
"context" "context"
"time"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render" "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. // List returns a collection of nodes.
func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) { 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) strLabel, ok := ctx.Value(internal.KeyLabels).(string)
lsel := labels.Everything() lsel := labels.Everything()
if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil { if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil {
lsel = sel.AsSelector() lsel = sel.AsSelector()
} }
log.Debug().Msgf("^^^^^Listing with selector %q:%q--%#v", r.namespace, r.gvr, lsel) return r.factory.List(r.gvr, r.namespace, lsel)
oo, err := r.factory.List(r.gvr, r.namespace, lsel)
r.factory.WaitForCacheSync()
return oo, err
} }
// Render returns a node as a row. // Render returns a node as a row.
func (r *Resource) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { 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 var index int
for _, o := range oo { for _, o := range oo {

View File

@ -7,44 +7,63 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render" "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"
"k8s.io/apimachinery/pkg/runtime/schema"
) )
// Subject represents a subject model. // Subject represents a subject model.
type Subject struct { type Subject struct {
Resource Resource
subjectKind string
} }
// List returns a collection of subjects. // List returns a collection of subjects.
func (s *Subject) List(ctx context.Context) ([]runtime.Object, error) { func (s *Subject) List(ctx context.Context) ([]runtime.Object, error) {
var ok bool kind, ok := ctx.Value(internal.KeySubjectKind).(string)
s.subjectKind, ok = ctx.Value(internal.KeySubject).(string)
if !ok { 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 { if err != nil {
return nil, err 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 { if err != nil {
return nil, err 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. // Hydrate returns a pod as container rows.
func (s *Subject) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error { func (s *Subject) Hydrate(oo []runtime.Object, rr render.Rows, re Renderer) error {
for i, o := range oo { for i, o := range oo {
res, ok := o.(*unstructured.Unstructured) res, ok := o.(render.SubjectRef)
if !ok { if !ok {
return fmt.Errorf("expecting unstructured but got %T", o) 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 return nil
} }
// BOZO!! func inSubjectRes(oo []runtime.Object, match string) bool {
// func (s *Subject) fetchClusterRoleBindings() ([]runtime.Object, error) { for _, o := range oo {
// oo, err := s.factory.List(render.ClusterScope, "rbac.authorization.k8s.io/v1/clusterrolebindings", labels.Everything()) res, ok := o.(render.SubjectRef)
// if err != nil { if !ok {
// return nil, err continue
// } }
if res.Name == match {
// rows := make([]runtime.Object, 0, len(oo)) return true
// for _, o := range oo { }
// var crb rbacv1.ClusterRoleBinding }
// err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb) return false
// 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
} }

View File

@ -9,7 +9,6 @@ import (
"github.com/derailed/tview" "github.com/derailed/tview"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
) )
@ -84,7 +83,7 @@ type Factory interface {
ForResource(ns, gvr string) informers.GenericInformer ForResource(ns, gvr string) informers.GenericInformer
// WaitForCacheSync synchronize the cache. // WaitForCacheSync synchronize the cache.
WaitForCacheSync() map[schema.GroupVersionResource]bool WaitForCacheSync()
// Forwards returns all portforwards. // Forwards returns all portforwards.
Forwarders() watch.Forwarders Forwarders() watch.Forwarders

View File

@ -26,25 +26,26 @@ func (Alias) Header(ns string) HeaderRow {
Header{Name: "RESOURCE"}, Header{Name: "RESOURCE"},
Header{Name: "COMMAND"}, Header{Name: "COMMAND"},
Header{Name: "APIGROUP"}, Header{Name: "APIGROUP"},
// Header{Name: "AGE", Decorator: AgeDecorator},
} }
} }
// Render renders a K8s resource to screen. // 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 { func (Alias) Render(o interface{}, gvr string, r *Row) error {
a, ok := o.(AliasRes) a, ok := o.(AliasRes)
if !ok { 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 = gvr
r.ID = string(g) gvr1 := client.GVR(a.GVR)
r.Fields = Fields{ grp, res := gvr1.ToRAndG()
g.ToR(), r.Fields = append(r.Fields,
res,
strings.Join(a.Aliases, ","), strings.Join(a.Aliases, ","),
g.ToG(), grp,
// time.Now().String(), )
}
return nil return nil
} }

View File

@ -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)
}
}

View File

@ -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"
}

View File

@ -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
}
}

View File

@ -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))
// }
// }

View File

@ -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
}

View File

@ -46,7 +46,7 @@ func (g *Generic) Header(ns string) HeaderRow {
func (g *Generic) Render(o interface{}, ns string, r *Row) error { func (g *Generic) Render(o interface{}, ns string, r *Row) error {
row, ok := o.(*metav1beta1.TableRow) row, ok := o.(*metav1beta1.TableRow)
if !ok { 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) count := len(row.Cells)
@ -57,8 +57,8 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
if !ok { if !ok {
return fmt.Errorf("expecting row id to be a string but got %#v", row.Cells[0]) 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 var index int
if ns == AllNamespaces { if ns == AllNamespaces {
rns, err := extractNamespace(row.Object.Raw) rns, err := extractNamespace(row.Object.Raw)

View File

@ -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",
},
},
},
}
}

View File

@ -378,14 +378,3 @@ func BenchmarkAsPerc(b *testing.B) {
AsPerc(v) 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
// }

View File

@ -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) { func containerResources(co v1.Container) (cpu, mem *resource.Quantity) {
req, limit := co.Resources.Requests, co.Resources.Limits req, limit := co.Resources.Requests, co.Resources.Limits
switch { switch {
case len(req) != 0: case len(req) != 0:
cpu, mem = req.Cpu(), req.Memory() cpu, mem = req.Cpu(), req.Memory()
case len(limit) != 0: case len(limit) != 0:
cpu, mem = limit.Cpu(), limit.Memory() cpu, mem = limit.Cpu(), limit.Memory()
} }
return return
} }

View File

@ -1,7 +1,11 @@
package render package render
import ( import (
"fmt"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
) )
func rbacVerbHeader() HeaderRow { func rbacVerbHeader() HeaderRow {
@ -36,11 +40,81 @@ func (Policy) Header(ns string) HeaderRow {
Header{Name: "API GROUP"}, Header{Name: "API GROUP"},
Header{Name: "BINDING"}, Header{Name: "BINDING"},
} }
return append(h, rbacVerbHeader()...) return append(h, rbacVerbHeader()...)
} }
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (Policy) Render(o interface{}, gvr string, r *Row) error { 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 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
}

View File

@ -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)
}

View File

@ -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"
}

View File

@ -51,23 +51,19 @@ func (Rbac) Header(ns string) HeaderRow {
// Render renders a K8s resource to screen. // Render renders a K8s resource to screen.
func (Rbac) Render(o interface{}, gvr string, r *Row) error { func (Rbac) Render(o interface{}, gvr string, r *Row) error {
p, ok := o.(*PolicyRes) p, ok := o.(PolicyRes)
if !ok { 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.ID = p.Resource
r.Fields = append(r.Fields, cleanseResource(p.Resource), p.Group)
r.Fields = append(r.Fields, asVerbs(p.Verbs)...)
return nil return nil
} }
// ----------------------------------------------------------------------------
// Helpers... // Helpers...
func asVerbs(verbs []string) []string { func asVerbs(verbs []string) []string {
@ -120,26 +116,50 @@ func hasVerb(verbs []string, verb string) bool {
return false return false
} }
func toGroup(g string) string { type RuleRes struct {
if g == "" {
return "v1"
}
return g
}
type PolicyRes struct {
Resource, Group string Resource, Group string
ResourceName string ResourceName string
NonResourceURL string NonResourceURL string
Verbs []string Verbs []string
} }
func NewRuleRes(res, grp string, vv []string) RuleRes {
return RuleRes{
Resource: res,
Group: grp,
Verbs: vv,
}
}
// GetObjectKind returns a schema object. // GetObjectKind returns a schema object.
func (p PolicyRes) GetObjectKind() schema.ObjectKind { func (r RuleRes) GetObjectKind() schema.ObjectKind {
return nil return nil
} }
// DeepCopyObject returns a container copy. // DeepCopyObject returns a container copy.
func (p PolicyRes) DeepCopyObject() runtime.Object { func (r RuleRes) DeepCopyObject() runtime.Object {
return p 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
} }

View File

@ -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])
}

View File

@ -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 }

View File

@ -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])
}

View File

@ -1,7 +1,11 @@
package render package render
import ( import (
"fmt"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
) )
// Subject renders a rbac to screen. // Subject renders a rbac to screen.
@ -24,6 +28,36 @@ func (Subject) Header(ns string) HeaderRow {
} }
// Render renders a K8s resource to screen. // 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 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
}

View File

@ -6,11 +6,3 @@ type TableData struct {
RowEvents RowEvents RowEvents RowEvents
Namespace string Namespace string
} }
func (t TableData) Clone() TableData {
return TableData{
Header: t.Header,
RowEvents: t.RowEvents.Clone(),
Namespace: t.Namespace,
}
}

View File

@ -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")
// }

View File

@ -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: <none>",
// "[steelblue::b]Node-Selectors[white::-]: [papayawhip::] <none>",
// },
// {
// "fred.blee: <none>",
// "[steelblue::b]fred.blee[white::-]: [papayawhip::] <none>",
// },
// {
// "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))
// }
// }

View File

@ -124,7 +124,6 @@ func (v *FlashView) refresh(ctx1, ctx2 context.Context, cancel context.CancelFun
case <-ctx2.Done(): case <-ctx2.Done():
v.app.QueueUpdateDraw(func() { v.app.QueueUpdateDraw(func() {
v.Clear() v.Clear()
v.app.Draw()
}) })
return return
} }

View File

@ -66,7 +66,6 @@ func (p *Pages) StackPushed(c model.Component) {
} }
func (p *Pages) StackPopped(o, top model.Component) { func (p *Pages) StackPopped(o, top model.Component) {
log.Debug().Msgf("UI STACK POPPED!!!")
p.delete(o) p.delete(o)
} }

View File

@ -47,7 +47,7 @@ func NewTable(title string) *Table {
actions: make(KeyActions), actions: make(KeyActions),
cmdBuff: NewCmdBuff('/', FilterBuff), cmdBuff: NewCmdBuff('/', FilterBuff),
BaseTitle: title, 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() { if t.SearchBuff().IsActive() {
t.SearchBuff().Add(evt.Rune()) t.SearchBuff().Add(evt.Rune())
t.ClearSelection() t.ClearSelection()
t.doUpdate(t.filtered()) t.doUpdate(t.filtered(t.Data), len(t.Data.RowEvents) > 0)
t.UpdateTitle() t.UpdateTitle()
t.SelectFirstRow() t.SelectFirstRow()
return nil return nil
@ -112,7 +112,7 @@ func (t *Table) Hints() model.MenuHints {
// GetFilteredData fetch filtered tabular data. // GetFilteredData fetch filtered tabular data.
func (t *Table) GetFilteredData() render.TableData { func (t *Table) GetFilteredData() render.TableData {
return t.filtered() return t.filtered(t.Data)
} }
// SetDecorateFn specifies the default row decorator. // SetDecorateFn specifies the default row decorator.
@ -132,31 +132,31 @@ func (t *Table) SetSortCol(index, count int, asc bool) {
// Update table content. // Update table content.
func (t *Table) Update(data render.TableData) { func (t *Table) Update(data render.TableData) {
var firstRow bool
if len(t.Data.RowEvents) == 0 {
firstRow = true
}
t.Data = data t.Data = data
if t.decorateFn != nil { if t.decorateFn != nil {
data = t.decorateFn(data) data = t.decorateFn(data)
} }
if !t.cmdBuff.Empty() {
if t.cmdBuff.Empty() { data = t.filtered(data)
t.doUpdate(data)
} else {
t.doUpdate(t.filtered())
} }
t.doUpdate(data, firstRow)
t.UpdateTitle() 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 { if data.Namespace == render.AllNamespaces {
t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd(-2, true), false) t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd(-2, true), false)
} else { } else {
t.actions.Delete(KeyShiftP) t.actions.Delete(KeyShiftP)
} }
t.Clear() t.Clear()
t.adjustSorter(data) t.adjustSorter(data)
fg := config.AsColor(t.styles.GetTable().Header.FgColor) fg := config.AsColor(t.styles.GetTable().Header.FgColor)
bg := config.AsColor(t.styles.GetTable().Header.BgColor) bg := config.AsColor(t.styles.GetTable().Header.BgColor)
for col, h := range data.Header { for col, h := range data.Header {
@ -172,6 +172,10 @@ func (t *Table) doUpdate(data render.TableData) {
for i, r := range data.RowEvents { for i, r := range data.RowEvents {
t.buildRow(data.Namespace, i+1, r, data.Header, pads) t.buildRow(data.Namespace, i+1, r, data.Header, pads)
} }
if firstRow {
t.SelectFirstRow()
}
t.updateSelection(false) 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.sortCol.index = index
t.Refresh() t.Refresh()
return nil return nil
} }
} }
@ -278,24 +281,22 @@ func (t *Table) AddHeaderCell(col int, h render.Header) {
t.SetCell(0, col, c) 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()) { if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.String()) {
return t.Data return data
} }
q := t.cmdBuff.String() q := t.cmdBuff.String()
if isFuzzySelector(q) { 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 { if err != nil {
log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp") log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp")
t.cmdBuff.Clear() t.cmdBuff.Clear()
return t.Data return data
} }
return filtered
return data
} }
// SearchBuff returns the associated command buffer. // SearchBuff returns the associated command buffer.

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
rt "runtime"
"strconv" "strconv"
"time" "time"
@ -93,6 +94,8 @@ func (b *Browser) Init(ctx context.Context) error {
func (b *Browser) Start() { func (b *Browser) Start() {
b.Stop() b.Stop()
log.Debug().Msgf("GOROUTINE %d", rt.NumGoroutine())
log.Debug().Msgf("BROWSER START %s", b.gvr) log.Debug().Msgf("BROWSER START %s", b.gvr)
b.Table.Start() b.Table.Start()
@ -109,24 +112,28 @@ func (b *Browser) Stop() {
} }
} }
func (b *Browser) Refresh() { func (b *Browser) update(ctx context.Context) {
b.refresh() defer log.Debug().Msgf("UPDATER BAIL For %s", b.gvr)
for {
select {
case <-ctx.Done():
log.Debug().Msgf("BROWSER <<CANCELED>> -- %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. // Name returns the component name.
func (b *Browser) Name() string { func (b *Browser) Name() string { return b.meta.Kind }
return b.meta.Kind
}
// SetContextFn populates a custom context. // SetContextFn populates a custom context.
func (b *Browser) SetContextFn(f ContextFunc) { func (b *Browser) SetContextFn(f ContextFunc) { b.contextFn = f }
b.contextFn = f
}
// SetBindKeysFn adds additional key bindings. // SetBindKeysFn adds additional key bindings.
func (b *Browser) SetBindKeysFn(f BindKeysFunc) { func (b *Browser) SetBindKeysFn(f BindKeysFunc) { b.bindKeysFn = f }
b.bindKeysFn = f
}
// SetEnvFn sets a function to pull viewer env vars for plugins. // SetEnvFn sets a function to pull viewer env vars for plugins.
func (b *Browser) SetEnvFn(f EnvFunc) { b.envFn = f } 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. // GVR returns a resource descriptor.
func (b *Browser) GVR() string { return string(b.gvr) } func (b *Browser) GVR() string { return string(b.gvr) }
func (b *Browser) GetTable() *Table { // GetTable returns the underlying table.
return b.Table func (b *Browser) GetTable() *Table { return b.Table }
}
func (b *Browser) update(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Debug().Msgf("BROWSER <<CANCELED>> -- %s", b.gvr)
return
case <-time.After(time.Duration(b.app.Config.K9s.GetRefreshRate()) * time.Second):
b.app.QueueUpdateDraw(func() {
b.refresh()
})
}
}
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Actions()... // Actions()...
@ -171,15 +163,12 @@ func (b *Browser) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
func (b *Browser) enterCmd(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() { if b.filterCmd(evt) == nil || !b.RowSelected() {
return nil return nil
} }
f := b.defaultEnter f := b.describeResource
if b.enterFn != nil { if b.enterFn != nil {
log.Debug().Msgf("Found custom enter")
f = b.enterFn f = b.enterFn
} }
f(b.app, b.Data.Namespace, string(b.gvr), b.GetSelectedItem()) 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 { func (b *Browser) refreshCmd(*tcell.EventKey) *tcell.EventKey {
b.app.Flash().Info("Refreshinb...") b.app.Flash().Info("Refreshing...")
b.refresh() b.refresh()
return nil return nil
} }
@ -253,8 +242,17 @@ func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
func (b *Browser) defaultEnter(app *App, _, _, sel string) { func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("--------- Resource %q Verbs %v", sel, b.meta.Verbs) 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) ns, n := client.Namespaced(sel)
yaml, err := dao.Describe(b.app.Conn(), b.gvr, ns, n) yaml, err := dao.Describe(b.app.Conn(), b.gvr, ns, n)
if err != nil { 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 { func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
if !b.RowSelected() { if !b.RowSelected() {
return evt return evt
@ -394,27 +382,24 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
func (b *Browser) refresh() { 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 { if b.app.Conn() == nil {
log.Error().Msg("No api connection")
return return
} }
ctx := b.defaultContext() ctx := b.defaultContext()
if b.contextFn != nil { if b.contextFn != nil {
log.Debug().Msgf("GOT CUSTOM CTX")
ctx = b.contextFn(ctx) ctx = b.contextFn(ctx)
} }
if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" { if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" {
b.Path = path b.Path = path
} }
data, err := dao.Reconcile(ctx, b.Table.Data, b.gvr) data, err := dao.Reconcile(ctx, b.Table.Data, b.gvr)
if err != nil { b.app.QueueUpdateDraw(func() {
b.app.Flash().Err(err) if err != nil {
} b.app.Flash().Err(err)
b.refreshActions() }
b.Update(data) b.refreshActions()
b.Update(data)
})
} }
func (b *Browser) defaultContext() context.Context { func (b *Browser) defaultContext() context.Context {
@ -429,6 +414,7 @@ func (b *Browser) defaultContext() context.Context {
func (b *Browser) namespaceActions(aa ui.KeyActions) { func (b *Browser) namespaceActions(aa ui.KeyActions) {
if b.app.Conn() == nil || !b.meta.Namespaced || b.GetTable().Path != "" { 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 return
} }
b.namespaces = make(map[int]string, config.MaxFavoritesNS) b.namespaces = make(map[int]string, config.MaxFavoritesNS)
@ -471,6 +457,7 @@ func (b *Browser) refreshActions() {
if b.bindKeysFn != nil { if b.bindKeysFn != nil {
b.bindKeysFn(b.Actions()) b.bindKeysFn(b.Actions())
} }
b.app.Menu().HydrateMenu(b.Hints())
} }
func (b *Browser) customActions(aa ui.KeyActions) { func (b *Browser) customActions(aa ui.KeyActions) {

View File

@ -23,17 +23,6 @@ type clusterInfoView struct {
mxs *client.MetricsServer 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 { func newClusterInfoView(app *App, mx *client.MetricsServer) *clusterInfoView {
return &clusterInfoView{ return &clusterInfoView{
app: app, app: app,

View File

@ -37,7 +37,7 @@ func (c *command) defaultCmd() error {
return c.run(c.app.Config.ActiveView()) 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 { func (c *command) isK9sCmd(cmd string) bool {
cmds := strings.Split(cmd, " ") cmds := strings.Split(cmd, " ")
@ -52,13 +52,15 @@ func (c *command) isK9sCmd(cmd string) bool {
c.app.aliasCmd(nil) c.app.aliasCmd(nil)
return true return true
default: default:
if !authRX.MatchString(cmd) { if !canRX.MatchString(cmd) {
return false return false
} }
tokens := authRX.FindAllStringSubmatch(cmd, -1) tokens := canRX.FindAllStringSubmatch(cmd, -1)
if len(tokens) == 1 && len(tokens[0]) == 3 { if len(tokens) == 1 && len(tokens[0]) == 3 {
// BOZO!! if err := c.app.inject(NewPolicy(c.app, tokens[0][1], tokens[0][2])); err != nil {
// c.app.inject(NewPolicy(c.app, tokens[0][1], tokens[0][2])) log.Error().Err(err).Msgf("policy view load failed")
return false
}
return true return true
} }
} }

View File

@ -9,9 +9,9 @@ import (
) )
func TestContainerNew(t *testing.T) { func TestContainerNew(t *testing.T) {
po := view.NewContainer(client.GVR("containers")) c := view.NewContainer(client.GVR("containers"))
assert.Nil(t, po.Init(makeCtx())) assert.Nil(t, c.Init(makeCtx()))
assert.Equal(t, "Containers", po.Name()) assert.Equal(t, "Containers", c.Name())
assert.Equal(t, 17, len(po.Hints())) assert.Equal(t, 17, len(c.Hints()))
} }

48
internal/view/group.go Normal file
View File

@ -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
}

View File

@ -5,7 +5,6 @@ import (
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/rs/zerolog/log"
) )
type PageStack struct { type PageStack struct {
@ -24,26 +23,17 @@ func (p *PageStack) Init(ctx context.Context) (err error) {
if p.app, err = extractApp(ctx); err != nil { if p.app, err = extractApp(ctx); err != nil {
return err return err
} }
p.Stack.AddListener(p) p.Stack.AddListener(p)
return nil return nil
} }
func (p *PageStack) StackPushed(c model.Component) { 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() c.Start()
p.app.SetFocus(c) p.app.SetFocus(c)
} }
func (p *PageStack) StackPopped(o, top model.Component) { func (p *PageStack) StackPopped(o, top model.Component) {
log.Debug().Msgf("PS STACK POPPED!!!")
o.Stop() o.Stop()
p.StackTop(top) p.StackTop(top)
} }

View File

@ -1,8 +1,9 @@
package view package view
import ( import (
"strings" "context"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
@ -16,34 +17,41 @@ const (
allVerbs = "*" allVerbs = "*"
) )
// Policy presents a RBAC policy viewer. // Policy presents a RBAC rules viewer.
type Policy struct { type Policy struct {
ResourceViewer ResourceViewer
subjectKind, subjectName string
} }
// NewPolicy returns a new viewer. // NewPolicy returns a new viewer.
func NewPolicy(gvr client.GVR) *Policy { func NewPolicy(app *App, subject, name string) *Policy {
p := Policy{ p := Policy{
ResourceViewer: NewBrowser(gvr), ResourceViewer: NewBrowser(client.GVR("policy")),
subjectKind: subject,
subjectName: name,
} }
p.GetTable().SetColorerFn(render.Policy{}.ColorerFunc()) p.GetTable().SetColorerFn(render.Policy{}.ColorerFunc())
p.SetBindKeysFn(p.bindKeys) p.SetBindKeysFn(p.bindKeys)
p.GetTable().SetSortCol(1, len(render.Policy{}.Header(render.AllNamespaces)), false) p.GetTable().SetSortCol(1, len(render.Policy{}.Header(render.AllNamespaces)), false)
p.SetContextFn(p.subjectCtx)
p.GetTable().SetEnterFn(blankEnterFn)
return &p return &p
} }
func (p *Policy) Name() string { func (p *Policy) subjectCtx(ctx context.Context) context.Context {
return "policy" 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) { func (p *Policy) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Add(ui.KeyActions{ 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(0, true), false),
ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.GetTable().SortColCmd(1, true), false), ui.KeyShiftO: ui.NewKeyAction("Sort Group", 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(2, true), false),
ui.KeyShiftB: ui.NewKeyAction("Sort Binding", p.GetTable().SortColCmd(3, true), false),
}) })
} }
@ -53,57 +61,9 @@ func mapSubject(subject string) string {
return group return group
case "s": case "s":
return sa return sa
default: case "u":
return user 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))
}

View File

@ -8,34 +8,8 @@ import (
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell" "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. // Rbac presents an RBAC policy viewer.
type Rbac struct { type Rbac struct {
ResourceViewer ResourceViewer
@ -43,13 +17,13 @@ type Rbac struct {
// NewRbac returns a new viewer. // NewRbac returns a new viewer.
func NewRbac(gvr client.GVR) ResourceViewer { func NewRbac(gvr client.GVR) ResourceViewer {
log.Debug().Msgf(">>>>> NEWRBAC %v!!!!!", gvr)
r := Rbac{ r := Rbac{
ResourceViewer: NewBrowser(gvr), ResourceViewer: NewBrowser(gvr),
} }
r.GetTable().SetColorerFn(render.Rbac{}.ColorerFunc()) r.GetTable().SetColorerFn(render.Rbac{}.ColorerFunc())
r.SetBindKeysFn(r.bindKeys) r.SetBindKeysFn(r.bindKeys)
r.GetTable().SetSortCol(1, len(render.Rbac{}.Header(render.ClusterScope)), true) r.GetTable().SetSortCol(1, len(render.Rbac{}.Header(render.ClusterScope)), true)
r.GetTable().SetEnterFn(blankEnterFn)
return &r return &r
} }
@ -61,31 +35,7 @@ func (r *Rbac) bindKeys(aa ui.KeyActions) {
}) })
} }
// BOZO!! func showRules(app *App, _, gvr, path string) {
// 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)
v := NewRbac(client.GVR("rbac")) v := NewRbac(client.GVR("rbac"))
v.SetContextFn(rbacCtxt(gvr, path)) v.SetContextFn(rbacCtxt(gvr, path))
@ -100,3 +50,5 @@ func rbacCtxt(gvr, path string) ContextFunc {
return context.WithValue(ctx, internal.KeyGVR, gvr) return context.WithValue(ctx, internal.KeyGVR, gvr)
} }
} }
func blankEnterFn(_ *App, _, _, _ string) {}

View File

@ -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))
}
}

View File

@ -88,6 +88,12 @@ func miscRes(vv MetaViewers) {
vv["aliases"] = MetaViewer{ vv["aliases"] = MetaViewer{
viewerFn: NewAlias, viewerFn: NewAlias,
} }
vv["users"] = MetaViewer{
viewerFn: NewUser,
}
vv["groups"] = MetaViewer{
viewerFn: NewGroup,
}
} }
func appsRes(vv MetaViewers) { func appsRes(vv MetaViewers) {
@ -110,19 +116,19 @@ func appsRes(vv MetaViewers) {
func rbacRes(vv MetaViewers) { func rbacRes(vv MetaViewers) {
vv["rbac"] = MetaViewer{ vv["rbac"] = MetaViewer{
enterFn: showRBAC, enterFn: showRules,
} }
vv["rbac.authorization.k8s.io/v1/clusterroles"] = MetaViewer{ vv["rbac.authorization.k8s.io/v1/clusterroles"] = MetaViewer{
enterFn: showRBAC, enterFn: showRules,
} }
vv["rbac.authorization.k8s.io/v1/roles"] = MetaViewer{ vv["rbac.authorization.k8s.io/v1/roles"] = MetaViewer{
enterFn: showRBAC, enterFn: showRules,
} }
vv["rbac.authorization.k8s.io/v1/clusterrolebindings"] = MetaViewer{ vv["rbac.authorization.k8s.io/v1/clusterrolebindings"] = MetaViewer{
enterFn: showRBAC, enterFn: showRules,
} }
vv["rbac.authorization.k8s.io/v1/rolebindings"] = MetaViewer{ vv["rbac.authorization.k8s.io/v1/rolebindings"] = MetaViewer{
enterFn: showRBAC, enterFn: showRules,
} }
} }

View File

@ -1,6 +1,9 @@
package view package view
import ( import (
"context"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
@ -29,7 +32,7 @@ func NewSubject(gvr client.GVR) ResourceViewer {
// BOZO!! // BOZO!!
// s.GetTable().SetSortCol(1, len(s.Header()), true) // s.GetTable().SetSortCol(1, len(s.Header()), true)
s.SetBindKeysFn(s.bindKeys) s.SetBindKeysFn(s.bindKeys)
s.SetContextFn(s.subjectCtx)
return &s 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. // SetSubject sets the subject name.
func (s *Subject) SetSubject(n string) { func (s *Subject) SetSubject(n string) {
s.subjectKind = mapSubject(n) s.subjectKind = mapSubject(n)

View File

@ -3,7 +3,6 @@ package view
import ( import (
"context" "context"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -58,17 +57,8 @@ func (t *Table) Stop() {
t.SearchBuff().RemoveListener(t) t.SearchBuff().RemoveListener(t)
} }
// MasterComponent returns the master component.
func (t *Table) MasterComponent() model.Component {
return t
}
// SetEnterFn specifies the default enter behavior. // SetEnterFn specifies the default enter behavior.
func (t *Table) SetEnterFn(f EnterFunc) { func (t *Table) SetEnterFn(f EnterFunc) {
if f == nil {
return
}
log.Debug().Msgf("Setting ENTERFN on %s -- %v", t.BaseTitle, f)
t.enterFn = 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 { func (t *Table) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("Table Escape")
if !t.SearchBuff().InCmdMode() { if !t.SearchBuff().InCmdMode() {
t.SearchBuff().Reset() t.SearchBuff().Reset()
return t.app.PrevCmd(evt) return t.app.PrevCmd(evt)
} }
log.Debug().Msgf("\tClearing filter")
if ui.IsLabelSelector(t.SearchBuff().String()) { if ui.IsLabelSelector(t.SearchBuff().String()) {
t.filterFn("") t.filterFn("")
} }

48
internal/view/user.go Normal file
View File

@ -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
}

View File

@ -15,6 +15,7 @@ import (
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
) )
// Factory - *factories(ns) -> *informers
const ( const (
defaultResync = 10 * time.Minute defaultResync = 10 * time.Minute
allNamespaces = "" 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) 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) inf := f.ForResource(ns, gvr)
if inf == nil { if inf == nil {
return nil, fmt.Errorf("No resource for GVR %s", gvr) return nil, fmt.Errorf("No resource for GVR %s", gvr)
} }
if ns == clusterScope { if ns == clusterScope {
return inf.Lister().List(sel) return inf.Lister().List(sel)
} }
return inf.Lister().ByNamespace(ns).List(sel) return inf.Lister().ByNamespace(ns).List(sel)
} }
func (f *Factory) Get(gvr, path string, sel labels.Selector) (runtime.Object, error) { func (f *Factory) Get(gvr, path string, sel labels.Selector) (runtime.Object, error) {
ns, n := namespaced(path) 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"}) auth, err := f.Client().CanI(ns, gvr, []string{"get"})
if err != nil { if err != nil {
return nil, err 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) return nil, fmt.Errorf("User has insufficient access to get %s", gvr)
} }
fac := f.ensureFactory(ns) inf := f.ForResource(ns, gvr)
log.Debug().Msgf("GVR: %#v", toGVR(gvr))
inf := fac.ForResource(toGVR(gvr))
if inf == nil { if inf == nil {
return nil, fmt.Errorf("No resource for GVR %s", gvr) return nil, fmt.Errorf("No resource for GVR %s", gvr)
} }
if ns == clusterScope { if ns == clusterScope {
return inf.Lister().Get(n) return inf.Lister().Get(n)
} }
log.Debug().Msgf("GET %q--%q:%q", gvr, ns, path)
return inf.Lister().ByNamespace(ns).Get(n) return inf.Lister().ByNamespace(ns).Get(n)
} }
func (f *Factory) WaitForCacheSync() map[schema.GroupVersionResource]bool { func (f *Factory) WaitForCacheSync() {
r := make(map[schema.GroupVersionResource]bool) for _, fac := range f.factories {
for n, fac := range f.factories { fac.WaitForCacheSync(f.stopChan)
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!")
} }
return r
} }
func (f *Factory) Init() { func (f *Factory) Init() {
@ -172,13 +162,13 @@ func (f *Factory) Start(stopChan chan struct{}) {
// BOZO!! Check ns access for resource?? // BOZO!! Check ns access for resource??
func (f *Factory) SetActive(ns string) { func (f *Factory) SetActive(ns string) {
if !f.isclusterScope() { if !f.isClusterWide() {
f.ensureFactory(ns) f.ensureFactory(ns)
} }
f.activeNS = ns f.activeNS = ns
} }
func (f *Factory) isclusterScope() bool { func (f *Factory) isClusterWide() bool {
_, ok := f.factories[allNamespaces] _, ok := f.factories[allNamespaces]
return ok return ok
} }
@ -207,7 +197,7 @@ func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer {
} }
func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory { func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory {
if f.isclusterScope() { if f.isClusterWide() {
ns = allNamespaces ns = allNamespaces
} }
if fac, ok := f.factories[ns]; ok { if fac, ok := f.factories[ns]; ok {