checkpoint
parent
4127da865f
commit
add0d678f0
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}{
|
}{
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,6 @@ const (
|
||||||
KeyBenchCfg ContextKey = "benchcfg"
|
KeyBenchCfg ContextKey = "benchcfg"
|
||||||
KeyAliases ContextKey = "aliases"
|
KeyAliases ContextKey = "aliases"
|
||||||
KeyUID ContextKey = "uid"
|
KeyUID ContextKey = "uid"
|
||||||
|
KeySubjectKind ContextKey = "subjectKind"
|
||||||
|
KeySubjectName ContextKey = "subjectName"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
@ -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{},
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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])
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
// }
|
|
||||||
|
|
@ -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))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 data
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchBuff returns the associated command buffer.
|
// SearchBuff returns the associated command buffer.
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
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()
|
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)
|
||||||
|
b.app.QueueUpdateDraw(func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.app.Flash().Err(err)
|
b.app.Flash().Err(err)
|
||||||
}
|
}
|
||||||
b.refreshActions()
|
b.refreshActions()
|
||||||
b.Update(data)
|
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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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) {}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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("")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue