checkpoint

mine
derailed 2020-01-18 08:13:36 -07:00
parent 530a23e28c
commit 860728c083
88 changed files with 4039 additions and 141 deletions

View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var toFileName = regexp.MustCompile(`[^(\w/\.)]`)
@ -37,7 +38,7 @@ func IsAllNamespaces(ns string) bool {
// IsNamespaced returns true if a specific ns is given.
func IsNamespaced(ns string) bool {
return !IsClusterScoped(ns)
return !IsAllNamespaces(ns)
}
// IsClusterScoped returns true if resource is not namespaced.
@ -60,6 +61,15 @@ func FQN(ns, n string) string {
return ns + "/" + n
}
// MetaFQN returns a fully qualified resource name.
func MetaFQN(m metav1.ObjectMeta) string {
if m.Namespace == "" {
return FQN(ClusterScope, m.Name)
}
return FQN(m.Namespace, m.Name)
}
func mustHomeDir() string {
usr, err := user.Current()
if err != nil {

View File

@ -39,7 +39,7 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
return nil, err
}
ns, _ := render.Namespaced(fqn)
ns, _ := client.Namespaced(fqn)
var pmx *mv1beta1.PodMetrics
if c.Client().HasMetrics() {
mx := client.NewMetricsServer(c.Client())

View File

@ -16,6 +16,8 @@ import (
var _ Describer = (*Generic)(nil)
var defaultKillGrace int64 = 0
// Generic represents a generic resource.
type Generic struct {
NonResource
@ -37,10 +39,10 @@ func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error)
ll *unstructured.UnstructuredList
err error
)
if client.IsNamespaced(ns) {
ll, err = g.dynClient().Namespace(ns).List(metav1.ListOptions{LabelSelector: labelSel})
} else {
if client.IsClusterScoped(ns) {
ll, err = g.dynClient().List(metav1.ListOptions{LabelSelector: labelSel})
} else {
ll, err = g.dynClient().Namespace(ns).List(metav1.ListOptions{LabelSelector: labelSel})
}
if err != nil {
return nil, err
@ -59,12 +61,12 @@ func (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error)
var opts metav1.GetOptions
ns, n := client.Namespaced(path)
req := g.dynClient()
dial := g.dynClient()
if client.IsClusterScoped(ns) {
return req.Get(n, opts)
return dial.Get(n, opts)
}
return req.Namespace(ns).Get(n, opts)
return dial.Namespace(ns).Get(n, opts)
}
// Describe describes a resource.
@ -98,7 +100,14 @@ func (g *Generic) Delete(path string, cascade, force bool) error {
if cascade {
p = metav1.DeletePropagationBackground
}
opts := metav1.DeleteOptions{PropagationPolicy: &p}
var grace *int64
if force {
grace = &defaultKillGrace
}
opts := metav1.DeleteOptions{
PropagationPolicy: &p,
GracePeriodSeconds: grace,
}
if client.IsClusterScoped(ns) {
return g.dynClient().Delete(n, &opts)
}

View File

@ -30,12 +30,17 @@ type Node struct {
func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
log.Debug().Msgf("NODE-LIST %q:%q", ns, n.gvr)
labels, ok := ctx.Value(internal.KeyLabels).(string)
if !ok {
log.Warn().Msgf("No label selector found in context")
}
nmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.NodeMetricsList)
if !ok {
log.Warn().Msgf("No node metrics available in context")
}
nn, err := FetchNodes(n.Factory)
nn, err := FetchNodes(n.Factory, labels)
if err != nil {
return nil, err
}
@ -59,13 +64,15 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// Helpers...
// FetchNodes retrieves all nodes.
func FetchNodes(f Factory) (*v1.NodeList, error) {
func FetchNodes(f Factory, labelsSel string) (*v1.NodeList, error) {
auth, err := f.Client().CanI("", "v1/nodes", []string{client.ListVerb})
if !auth || err != nil {
return nil, err
}
return f.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{})
return f.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{
LabelSelector: labelsSel,
})
}
func nodeMetricsFor(fqn string, mmx *mv1beta1.NodeMetricsList) *mv1beta1.NodeMetrics {

View File

@ -37,13 +37,28 @@ type Pod struct {
Resource
}
// List returns a collection of nodes.
func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// Get returns a resource instance if found, else an error.
func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
o, err := p.Resource.Get(ctx, path)
if err != nil {
return o, err
}
u, ok := o.(*unstructured.Unstructured)
if !ok {
return nil, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
}
pmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList)
if !ok {
log.Warn().Msgf("no metrics available for %q", p.gvr)
}
return &render.PodWithMetrics{Raw: u, MX: podMetricsFor(o, pmx)}, nil
}
// List returns a collection of nodes.
func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
sel, ok := ctx.Value(internal.KeyFields).(string)
if !ok {
return nil, fmt.Errorf("expecting a fieldSelector in context")
@ -59,6 +74,11 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
return oo, err
}
pmx, ok := ctx.Value(internal.KeyMetrics).(*mv1beta1.PodMetricsList)
if !ok {
log.Warn().Msgf("no metrics available for %q", p.gvr)
}
var res []runtime.Object
for _, o := range oo {
u, ok := o.(*unstructured.Unstructured)

View File

@ -112,6 +112,11 @@ func loadNonResource(m ResourceMetas) {
}
func loadK9s(m ResourceMetas) {
m[client.NewGVR("xrays")] = metav1.APIResource{
Name: "xray",
Kind: "XRays",
Categories: []string{"k9s"},
}
m[client.NewGVR("aliases")] = metav1.APIResource{
Name: "aliases",
Kind: "Aliases",

View File

@ -5,7 +5,6 @@ import (
"fmt"
"github.com/derailed/k9s/internal"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
@ -23,7 +22,6 @@ type Resource struct {
// List returns a collection of resources.
func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error) {
log.Debug().Msgf("INF-LIST %q:%q", ns, r.gvr)
strLabel, ok := ctx.Value(internal.KeyLabels).(string)
lsel := labels.Everything()
if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil {

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
@ -17,6 +18,32 @@ type Table struct {
Generic
}
// Get returns a given resource.
func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
ns, n := client.Namespaced(path)
log.Debug().Msgf("TABLE-GET %q:%q", ns, t.gvr)
a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)
_, codec := t.codec()
c, err := t.getClient()
if err != nil {
return nil, err
}
o, err := c.Get().
SetHeader("Accept", a).
Namespace(ns).
Name(n).
Resource(t.gvr.ToR()).
VersionedParams(&metav1beta1.TableOptions{}, codec).
Do().Get()
if err != nil {
return nil, err
}
return o, nil
}
// List all Resources in a given namespace.
func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
log.Debug().Msgf("TABLE-LIST %q:%q", ns, t.gvr)

View File

@ -3,6 +3,7 @@ package model
import (
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/xray"
)
// Registry tracks resources metadata.
@ -14,8 +15,9 @@ var Registry = map[string]ResourceMeta{
Renderer: &render.Chart{},
},
"containers": {
DAO: &dao.Container{},
Renderer: &render.Container{},
DAO: &dao.Container{},
Renderer: &render.Container{},
TreeRenderer: &xray.Container{},
},
"contexts": {
DAO: &dao.Context{},
@ -62,8 +64,9 @@ var Registry = map[string]ResourceMeta{
Renderer: &render.Event{},
},
"v1/pods": {
DAO: &dao.Pod{},
Renderer: &render.Pod{},
DAO: &dao.Pod{},
Renderer: &render.Pod{},
TreeRenderer: &xray.Pod{},
},
"v1/namespaces": {
Renderer: &render.Namespace{},
@ -73,8 +76,9 @@ var Registry = map[string]ResourceMeta{
Renderer: &render.Node{},
},
"v1/services": {
DAO: &dao.Service{},
Renderer: &render.Service{},
DAO: &dao.Service{},
Renderer: &render.Service{},
TreeRenderer: &xray.Service{},
},
"v1/serviceaccounts": {
Renderer: &render.ServiceAccount{},
@ -88,19 +92,22 @@ var Registry = map[string]ResourceMeta{
// Apps...
"apps/v1/deployments": {
DAO: &dao.Deployment{},
Renderer: &render.Deployment{},
DAO: &dao.Deployment{},
Renderer: &render.Deployment{},
TreeRenderer: &xray.Deployment{},
},
"apps/v1/replicasets": {
Renderer: &render.ReplicaSet{},
},
"apps/v1/statefulsets": {
DAO: &dao.StatefulSet{},
Renderer: &render.StatefulSet{},
DAO: &dao.StatefulSet{},
Renderer: &render.StatefulSet{},
TreeRenderer: &xray.StatefulSet{},
},
"apps/v1/daemonsets": {
DAO: &dao.DaemonSet{},
Renderer: &render.DaemonSet{},
DAO: &dao.DaemonSet{},
Renderer: &render.DaemonSet{},
TreeRenderer: &xray.DaemonSet{},
},
// Extensions...

View File

@ -34,6 +34,7 @@ type Table struct {
listeners []TableListener
inUpdate int32
refreshRate time.Duration
instance string
}
// NewTable returns a new table model.
@ -45,6 +46,10 @@ func NewTable(gvr string) *Table {
}
}
func (t *Table) SetInstance(s string) {
t.instance = s
}
// AddListener adds a new model listener.
func (t *Table) AddListener(l TableListener) {
t.listeners = append(t.listeners, l)
@ -217,25 +222,36 @@ func (t *Table) reconcile(ctx context.Context) error {
}(time.Now())
meta := t.resourceMeta()
oo, err := t.list(ctx, meta.DAO)
var (
oo []runtime.Object
err error
)
if t.instance == "" {
oo, err = t.list(ctx, meta.DAO)
} else {
o, e := t.Get(ctx, t.instance)
oo, err = []runtime.Object{o}, e
}
if err != nil {
return err
}
log.Debug().Msgf(" LIST returned %d rows", len(oo))
var rows render.Rows
ns := client.CleanseNamespace(t.namespace)
if _, ok := meta.Renderer.(*render.Generic); ok {
table, ok := oo[0].(*metav1beta1.Table)
if !ok {
return fmt.Errorf("expecting a meta table but got %T", oo[0])
}
log.Debug().Msgf("!!!!YO!!!")
rows = make(render.Rows, len(table.Rows))
if err := genericHydrate(client.CleanseNamespace(t.namespace), table, rows, meta.Renderer); err != nil {
if err := genericHydrate(ns, table, rows, meta.Renderer); err != nil {
return err
}
} else {
rows = make(render.Rows, len(oo))
if err := hydrate(client.CleanseNamespace(t.namespace), oo, rows, meta.Renderer); err != nil {
if err := hydrate(ns, oo, rows, meta.Renderer); err != nil {
return err
}
}
@ -293,6 +309,7 @@ func (t *Table) fireTableLoadFailed(err error) {
}
}
// ----------------------------------------------------------------------------
// Helpers...
func hydrate(ns string, oo []runtime.Object, rr render.Rows, re Renderer) error {

View File

@ -58,7 +58,7 @@ func TestTableGet(t *testing.T) {
row, err := ta.Get(ctx, "fred")
assert.Nil(t, err)
assert.NotNil(t, row)
assert.Equal(t, 5, len(row.(*unstructured.Unstructured).Object))
assert.Equal(t, 5, len(row.(*render.PodWithMetrics).Raw.Object))
}
func TestTableMeta(t *testing.T) {
@ -172,6 +172,10 @@ func raw(t *testing.T, n string) []byte {
// ----------------------------------------------------------------------------
func makeFactory() testFactory {
return testFactory{}
}
type testFactory struct {
rows []runtime.Object
}
@ -205,10 +209,6 @@ func (f testFactory) Forwarders() watch.Forwarders {
}
func (f testFactory) DeleteForwarder(string) {}
func makeFactory() testFactory {
return testFactory{}
}
// ----------------------------------------------------------------------------
type accessor struct {

298
internal/model/tree.go Normal file
View File

@ -0,0 +1,298 @@
package model
import (
"context"
"fmt"
"regexp"
"strings"
"sync/atomic"
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/xray"
"github.com/rs/zerolog/log"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
)
// TreeListener represents a tree model listener.
type TreeListener interface {
// TreeChanged notifies the model data changed.
TreeChanged(*xray.TreeNode)
// TreeLoadFailed notifies the load failed.
TreeLoadFailed(error)
}
// Tree represents a tree model.
type Tree struct {
gvr string
namespace string
root *xray.TreeNode
listeners []TreeListener
inUpdate int32
refreshRate time.Duration
query string
}
// NewTree returns a new model.
func NewTree(gvr string) *Tree {
return &Tree{
gvr: gvr,
refreshRate: 2 * time.Second,
}
}
func (t *Tree) ClearFilter() {
t.query = ""
}
func (t *Tree) SetFilter(q string) {
t.query = q
}
// AddListener adds a listener.
func (t *Tree) AddListener(l TreeListener) {
t.listeners = append(t.listeners, l)
}
// RemoveListener delete a listener.
func (t *Tree) RemoveListener(l TreeListener) {
victim := -1
for i, lis := range t.listeners {
if lis == l {
victim = i
break
}
}
if victim >= 0 {
t.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...)
}
}
// Watch initiates model updates.
func (t *Tree) Watch(ctx context.Context) {
t.Refresh(ctx)
go t.updater(ctx)
}
// Refresh update the model now.
func (t *Tree) Refresh(ctx context.Context) {
t.refresh(ctx)
}
// GetNamespace returns the model namespace.
func (t *Tree) GetNamespace() string {
return t.namespace
}
// SetNamespace sets up model namespace.
func (t *Tree) SetNamespace(ns string) {
t.namespace = ns
if t.root == nil {
return
}
t.root.Clear()
}
// SetRefreshRate sets model refresh duration.
func (t *Tree) SetRefreshRate(d time.Duration) {
t.refreshRate = d
}
// ClusterWide checks if resource is scope for all namespaces.
func (t *Tree) ClusterWide() bool {
return client.IsClusterWide(t.namespace)
}
// InNamespace checks if current namespace matches desired namespace.
func (t *Tree) InNamespace(ns string) bool {
return t.namespace == ns
}
// Empty return true if no model data.
func (t *Tree) Empty() bool {
return t.root.Empty()
}
// Peek returns model data.
func (t *Tree) Peek() *xray.TreeNode {
return t.root
}
func (t *Tree) updater(ctx context.Context) {
defer log.Debug().Msgf("Model canceled -- %q", t.gvr)
rate := iniRefreshRate
for {
select {
case <-ctx.Done():
t.root = nil
return
case <-time.After(rate):
rate = t.refreshRate
t.refresh(ctx)
}
}
}
func (t *Tree) refresh(ctx context.Context) {
if !atomic.CompareAndSwapInt32(&t.inUpdate, 0, 1) {
log.Debug().Msgf("Dropping update...")
return
}
defer atomic.StoreInt32(&t.inUpdate, 0)
if err := t.reconcile(ctx); err != nil {
log.Error().Err(err).Msg("Reconcile failed")
t.fireTreeLoadFailed(err)
return
}
}
func (t *Tree) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) {
defer func(ti time.Time) {
log.Debug().Msgf(" TREE-LIST %q:%q elapsed %v", t.namespace, t.gvr, time.Since(ti))
}(time.Now())
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
a.Init(factory, client.NewGVR(t.gvr))
return a.List(ctx, client.CleanseNamespace(t.namespace))
}
func (t *Tree) reconcile(ctx context.Context) error {
defer func(ti time.Time) {
log.Debug().Msgf("TREE-RECONCILE %q:%q elapsed %v", t.namespace, t.gvr, time.Since(ti))
}(time.Now())
meta := t.resourceMeta()
oo, err := t.list(ctx, meta.DAO)
if err != nil {
return err
}
log.Debug().Msgf(" TREE returned %d rows", len(oo))
ns := client.CleanseNamespace(t.namespace)
root := xray.NewTreeNode(t.gvr, client.NewGVR(t.gvr).ToR())
ctx = context.WithValue(ctx, xray.KeyParent, root)
if _, ok := meta.TreeRenderer.(*xray.Generic); ok {
table, ok := oo[0].(*metav1beta1.Table)
if !ok {
return fmt.Errorf("expecting a Table but got %T", oo[0])
}
if err := genericTreeHydrate(ctx, ns, table, meta.TreeRenderer); err != nil {
return err
}
} else {
if err := treeHydrate(ctx, ns, oo, meta.TreeRenderer); err != nil {
return err
}
}
root.Sort()
if t.query != "" {
t.root = root.Filter(t.query, rxFilter)
}
if t.root == nil || t.root.Diff(root) {
log.Debug().Msgf(">>>> DIFFERENCE!!!!")
t.root = root
t.fireTreeTreeChanged(t.root)
}
log.Debug().Msgf("TREE ROOT returns %d children", len(t.root.Children))
return nil
}
func (t *Tree) getMeta(ctx context.Context) (ResourceMeta, error) {
meta := t.resourceMeta()
factory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return ResourceMeta{}, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
meta.DAO.Init(factory, client.NewGVR(t.gvr))
return meta, nil
}
func (t *Tree) resourceMeta() ResourceMeta {
meta, ok := Registry[t.gvr]
if !ok {
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
meta = ResourceMeta{
DAO: &dao.Table{},
Renderer: &render.Generic{},
}
}
if meta.DAO == nil {
meta.DAO = &dao.Resource{}
}
return meta
}
func (t *Tree) fireTreeTreeChanged(root *xray.TreeNode) {
for _, l := range t.listeners {
l.TreeChanged(root)
}
}
func (t *Tree) fireTreeLoadFailed(err error) {
for _, l := range t.listeners {
l.TreeLoadFailed(err)
}
}
// ----------------------------------------------------------------------------
// Helpers...
func rxFilter(q, path string) bool {
rx := regexp.MustCompile(`(?i)` + q)
tokens := strings.Split(path, "::")
for _, t := range tokens {
if rx.MatchString(t) {
return true
}
}
return false
}
func treeHydrate(ctx context.Context, ns string, oo []runtime.Object, re TreeRenderer) error {
defer func(t time.Time) {
log.Debug().Msgf(" TREE-HYDRATE elapsed %v", time.Since(t))
}(time.Now())
for _, o := range oo {
if err := re.Render(ctx, ns, o); err != nil {
return err
}
}
return nil
}
func genericTreeHydrate(ctx context.Context, ns string, table *metav1beta1.Table, re TreeRenderer) error {
tre, ok := re.(*xray.Generic)
if !ok {
return fmt.Errorf("expecting xray.Generic renderer but got %T", re)
}
tre.SetTable(table)
// BOZO!! Need table row sorter!!
for _, row := range table.Rows {
if err := tre.Render(ctx, ns, row); err != nil {
return err
}
}
return nil
}

View File

@ -81,8 +81,13 @@ type Describer interface {
Describe(client client.Connection, gvr, path string) (string, error)
}
type TreeRenderer interface {
Render(ctx context.Context, ns string, o interface{}) error
}
// ResourceMeta represents model info about a resource.
type ResourceMeta struct {
DAO dao.Accessor
Renderer Renderer
DAO dao.Accessor
Renderer Renderer
TreeRenderer TreeRenderer
}

View File

@ -46,7 +46,7 @@ func (c Chart) Render(o interface{}, ns string, r *Row) error {
return fmt.Errorf("expected ChartRes, but got %T", o)
}
r.ID = FQN(h.Release.Namespace, h.Release.Name)
r.ID = client.FQN(h.Release.Namespace, h.Release.Name)
r.Fields = make(Fields, 0, len(c.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, h.Release.Namespace)

View File

@ -3,6 +3,7 @@ package render
import (
"fmt"
"github.com/derailed/k9s/internal/client"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -36,7 +37,7 @@ func (ClusterRole) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = FQN("-", cr.ObjectMeta.Name)
r.ID = client.FQN("-", cr.ObjectMeta.Name)
r.Fields = Fields{
cr.Name,
toAge(cr.ObjectMeta.CreationTimestamp),

View File

@ -3,6 +3,7 @@ package render
import (
"fmt"
"github.com/derailed/k9s/internal/client"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -41,7 +42,7 @@ func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error {
kind, ss := renderSubjects(crb.Subjects)
r.ID = FQN("-", crb.ObjectMeta.Name)
r.ID = client.FQN("-", crb.ObjectMeta.Name)
r.Fields = Fields{
crb.Name,
crb.RoleRef.Name,

View File

@ -42,7 +42,7 @@ func (CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error {
log.Error().Err(err).Msgf("Fields timestamp %v", err)
}
r.ID = FQN(client.ClusterScope, extractMetaField(meta, "name"))
r.ID = client.FQN(client.ClusterScope, extractMetaField(meta, "name"))
r.Fields = Fields{
extractMetaField(meta, "name"),
toAge(metav1.Time{Time: t}),

View File

@ -52,7 +52,7 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error {
lastScheduled = toAgeHuman(toAge(*cj.Status.LastScheduleTime))
}
r.ID = MetaFQN(cj.ObjectMeta)
r.ID = client.MetaFQN(cj.ObjectMeta)
r.Fields = make(Fields, 0, len(c.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, cj.Namespace)

View File

@ -66,7 +66,7 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(dp.ObjectMeta)
r.ID = client.MetaFQN(dp.ObjectMeta)
r.Fields = make(Fields, 0, len(d.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, dp.Namespace)

View File

@ -66,7 +66,7 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(ds.ObjectMeta)
r.ID = client.MetaFQN(ds.ObjectMeta)
r.Fields = make(Fields, 0, len(d.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, ds.Namespace)

View File

@ -45,7 +45,7 @@ func (e Endpoints) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(ep.ObjectMeta)
r.ID = client.MetaFQN(ep.ObjectMeta)
r.Fields = make(Fields, 0, len(e.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, ep.Namespace)

View File

@ -65,7 +65,7 @@ func (e Event) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(ev.ObjectMeta)
r.ID = client.MetaFQN(ev.ObjectMeta)
r.Fields = make(Fields, 0, len(e.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, ev.Namespace)

View File

@ -69,7 +69,7 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0])
}
r.ID = FQN(nns, n)
r.ID = client.FQN(nns, n)
r.Fields = make(Fields, 0, len(g.Header(ns)))
if client.IsAllNamespaces(ns) && nns != "" {
r.Fields = append(r.Fields, nns)

View File

@ -1,13 +1,11 @@
package render
import (
"path"
"sort"
"strconv"
"strings"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth"
"github.com/rs/zerolog/log"
@ -40,23 +38,6 @@ func noMetric() metric {
return metric{cpu: NAValue, mem: NAValue}
}
// MetaFQN returns a fully qualified resource name.
func MetaFQN(m metav1.ObjectMeta) string {
if m.Namespace == "" {
return FQN(client.ClusterScope, m.Name)
}
return FQN(m.Namespace, m.Name)
}
// FQN returns a fully qualified resource name.
func FQN(ns, n string) string {
if ns == "" {
return n
}
return ns + "/" + n
}
// ToSelector flattens a map selector to a string selector.
func toSelector(m map[string]string) string {
s := make([]string, 0, len(m))
@ -125,13 +106,6 @@ func toPerc(v1, v2 float64) float64 {
return (v1 / v2) * 100
}
// Namespaced return a namesapace and a name.
func Namespaced(n string) (string, string) {
ns, po := path.Split(n)
return strings.Trim(ns, "/"), po
}
func missing(s string) string {
return check(s, MissingValue)
}

View File

@ -4,6 +4,7 @@ import (
"testing"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -120,7 +121,7 @@ func TestNamespaced(t *testing.T) {
}
for _, u := range uu {
ns, n := Namespaced(u.p)
ns, n := client.Namespaced(u.p)
assert.Equal(t, u.ns, ns)
assert.Equal(t, u.n, n)
}
@ -276,7 +277,7 @@ func TestMetaFQN(t *testing.T) {
for k := range uu {
uc := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, uc.e, MetaFQN(uc.m))
assert.Equal(t, uc.e, client.MetaFQN(uc.m))
})
}
}
@ -293,7 +294,7 @@ func TestFQN(t *testing.T) {
for k := range uu {
uc := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, uc.e, FQN(uc.ns, uc.n))
assert.Equal(t, uc.e, client.FQN(uc.ns, uc.n))
})
}
}

View File

@ -68,7 +68,7 @@ func (h HorizontalPodAutoscaler) renderV1(raw *unstructured.Unstructured, ns str
return err
}
r.ID = MetaFQN(hpa.ObjectMeta)
r.ID = client.MetaFQN(hpa.ObjectMeta)
r.Fields = make(Fields, 0, len(h.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, hpa.Namespace)
@ -93,7 +93,7 @@ func (h HorizontalPodAutoscaler) renderV2b1(raw *unstructured.Unstructured, ns s
return err
}
r.ID = MetaFQN(hpa.ObjectMeta)
r.ID = client.MetaFQN(hpa.ObjectMeta)
r.Fields = make(Fields, 0, len(h.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, hpa.Namespace)
@ -119,7 +119,7 @@ func (h HorizontalPodAutoscaler) renderV2b2(raw *unstructured.Unstructured, ns s
return err
}
r.ID = MetaFQN(hpa.ObjectMeta)
r.ID = client.MetaFQN(hpa.ObjectMeta)
r.Fields = make(Fields, 0, len(h.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, hpa.Namespace)

View File

@ -47,7 +47,7 @@ func (i Ingress) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(ing.ObjectMeta)
r.ID = client.MetaFQN(ing.ObjectMeta)
r.Fields = make(Fields, 0, len(i.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, ing.Namespace)

View File

@ -53,7 +53,7 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(job.ObjectMeta)
r.ID = client.MetaFQN(job.ObjectMeta)
r.Fields = make(Fields, 0, len(j.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, job.Namespace)

View File

@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/tview"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -73,7 +74,7 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
ro := make([]string, 10)
nodeRoles(&no, ro)
r.ID = FQN("", na)
r.ID = client.FQN("", na)
r.Fields = make(Fields, 0, len(n.Header(ns)))
r.Fields = append(r.Fields,
no.Name,

View File

@ -53,7 +53,7 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
ip, is, ib := ingress(np.Spec.Ingress)
ep, es, eb := egress(np.Spec.Egress)
r.ID = MetaFQN(np.ObjectMeta)
r.ID = client.MetaFQN(np.ObjectMeta)
r.Fields = make(Fields, 0, len(n.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, np.Namespace)

View File

@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -57,7 +58,7 @@ func (Namespace) Render(o interface{}, _ string, r *Row) error {
return err
}
r.ID = MetaFQN(ns.ObjectMeta)
r.ID = client.MetaFQN(ns.ObjectMeta)
r.Fields = Fields{
ns.Name,
string(ns.Status.Phase),

View File

@ -69,7 +69,7 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(pdb.ObjectMeta)
r.ID = client.MetaFQN(pdb.ObjectMeta)
r.Fields = make(Fields, 0, len(p.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, pdb.Namespace)

View File

@ -89,22 +89,22 @@ func (Pod) Header(ns string) HeaderRow {
// Render renders a K8s resource to screen.
func (p Pod) Render(o interface{}, ns string, r *Row) error {
oo, ok := o.(*PodWithMetrics)
pwm, ok := o.(*PodWithMetrics)
if !ok {
return fmt.Errorf("Expected PodWithMetrics, but got %T", o)
}
var po v1.Pod
err := runtime.DefaultUnstructuredConverter.FromUnstructured(oo.Raw.Object, &po)
err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object, &po)
if err != nil {
return err
}
ss := po.Status.ContainerStatuses
cr, _, rc := p.statuses(ss)
c, perc := p.gatherPodMX(&po, oo.MX)
c, perc := p.gatherPodMX(&po, pwm.MX)
r.ID = MetaFQN(po.ObjectMeta)
r.ID = client.MetaFQN(po.ObjectMeta)
r.Fields = make(Fields, 0, len(p.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, po.Namespace)

View File

@ -3,6 +3,7 @@ package render
import (
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -50,7 +51,7 @@ func (Policy) Render(o interface{}, gvr string, r *Row) error {
return fmt.Errorf("expecting PolicyRes but got %T", o)
}
r.ID = FQN(p.Namespace, p.Resource)
r.ID = client.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)...)
@ -64,7 +65,7 @@ func cleanseResource(r string) string {
if r[0] == '/' {
return r
}
_, n := Namespaced(r)
_, n := client.Namespaced(r)
return n
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -59,7 +60,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
}
ports := strings.Split(pf.Ports()[0], ":")
ns, n := Namespaced(pf.Path())
ns, n := client.Namespaced(pf.Path())
r.ID = pf.Path()
r.Fields = Fields{

View File

@ -5,6 +5,7 @@ import (
"path"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/gdamore/tcell"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -79,7 +80,7 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error {
size := pv.Spec.Capacity[v1.ResourceStorage]
r.ID = MetaFQN(pv.ObjectMeta)
r.ID = client.MetaFQN(pv.ObjectMeta)
r.Fields = Fields{
pv.Name,
size.String(),

View File

@ -84,7 +84,7 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
}
}
r.ID = MetaFQN(pvc.ObjectMeta)
r.ID = client.MetaFQN(pvc.ObjectMeta)
r.Fields = make(Fields, 0, len(p.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, pvc.Namespace)

View File

@ -42,7 +42,7 @@ func (r Role) Render(o interface{}, ns string, row *Row) error {
return err
}
row.ID = MetaFQN(ro.ObjectMeta)
row.ID = client.MetaFQN(ro.ObjectMeta)
row.Fields = make(Fields, 0, len(r.Header(ns)))
if client.IsAllNamespaces(ns) {
row.Fields = append(row.Fields, ro.Namespace)

View File

@ -48,7 +48,7 @@ func (r RoleBinding) Render(o interface{}, ns string, row *Row) error {
kind, ss := renderSubjects(rb.Subjects)
row.ID = MetaFQN(rb.ObjectMeta)
row.ID = client.MetaFQN(rb.ObjectMeta)
row.Fields = make(Fields, 0, len(r.Header(ns)))
if client.IsAllNamespaces(ns) {
row.Fields = append(row.Fields, rb.Namespace)

View File

@ -65,7 +65,7 @@ func (s ReplicaSet) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(rs.ObjectMeta)
r.ID = client.MetaFQN(rs.ObjectMeta)
r.Fields = make(Fields, 0, len(s.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, rs.Namespace)

View File

@ -44,7 +44,7 @@ func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(sa.ObjectMeta)
r.ID = client.MetaFQN(sa.ObjectMeta)
r.Fields = make(Fields, 0, len(s.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, sa.Namespace)

View File

@ -38,7 +38,7 @@ func (StorageClass) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = FQN(client.ClusterScope, sc.ObjectMeta.Name)
r.ID = client.FQN(client.ClusterScope, sc.ObjectMeta.Name)
r.Fields = Fields{
sc.Name,
string(sc.Provisioner),

View File

@ -65,7 +65,7 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(sts.ObjectMeta)
r.ID = client.MetaFQN(sts.ObjectMeta)
r.Fields = make(Fields, 0, len(s.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, sts.Namespace)

View File

@ -50,7 +50,7 @@ func (s Service) Render(o interface{}, ns string, r *Row) error {
return err
}
r.ID = MetaFQN(svc.ObjectMeta)
r.ID = client.MetaFQN(svc.ObjectMeta)
r.Fields = make(Fields, 0, len(s.Header(ns)))
if client.IsAllNamespaces(ns) {
r.Fields = append(r.Fields, svc.Namespace)

View File

@ -108,8 +108,7 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if t.SearchBuff().IsActive() {
t.SearchBuff().Add(evt.Rune())
t.ClearSelection()
data := t.GetModel().Peek()
t.doUpdate(t.filtered(data))
t.doUpdate(t.filtered(t.GetModel().Peek()))
t.UpdateTitle()
t.SelectFirstRow()
return nil
@ -340,7 +339,6 @@ func (t *Table) UpdateTitle() {
t.SetTitle(t.styleTitle())
}
// UpdateTitle refreshes the table title.
func (t *Table) styleTitle() string {
rc := t.GetRowCount()
if rc > 0 {
@ -365,9 +363,9 @@ func (t *Table) styleTitle() string {
buff := t.SearchBuff().String()
var title string
if ns == client.ClusterScope {
title = SkinTitle(fmt.Sprintf(titleFmt, base, rc), t.styles.Frame())
title = SkinTitle(fmt.Sprintf(TitleFmt, base, rc), t.styles.Frame())
} else {
title = SkinTitle(fmt.Sprintf(nsTitleFmt, base, ns, rc), t.styles.Frame())
title = SkinTitle(fmt.Sprintf(NSTitleFmt, base, ns, rc), t.styles.Frame())
}
if buff == "" {
return title

View File

@ -17,8 +17,12 @@ const (
// SearchFmt represents a filter view title.
SearchFmt = "<[filter:bg:r]/%s[fg:bg:-]> "
nsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
titleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
// NSTitleFmt represents a namespaced view title.
NSTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
// TitleFmt represents a standard view title.
TitleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] "
descIndicator = "↓"
ascIndicator = "↑"

View File

@ -60,6 +60,7 @@ type testModel struct{}
var _ ui.Tabular = &testModel{}
func (t *testModel) SetInstance(string) {}
func (t *testModel) Empty() bool { return false }
func (t *testModel) Peek() render.TableData { return makeTableData() }
func (t *testModel) ClusterWide() bool { return false }

View File

@ -53,6 +53,8 @@ type Tabular interface {
Namespaceable
Lister
SetInstance(string)
// Empty returns true if model has no data.
Empty() bool

View File

@ -3,6 +3,7 @@ package view
import (
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
@ -116,22 +117,23 @@ func execCmd(r Runner, bin string, bg bool, args ...string) ui.ActionHandler {
return evt
}
ns, _ := client.Namespaced(path)
var (
env = r.EnvFn()()
aa = make([]string, len(args))
err error
)
for i, a := range args {
aa[i], err = env.envFor(a)
aa[i], err = env.envFor(ns, a)
if err != nil {
log.Error().Err(err).Msg("Args match failed")
log.Error().Err(err).Msg("Plugin Args match failed")
return nil
}
}
if run(true, r.App(), bin, bg, aa...) {
r.App().Flash().Info("Custom CMD launched!")
r.App().Flash().Info("Plugin command launched successfully!")
} else {
r.App().Flash().Info("Custom CMD failed!")
r.App().Flash().Info("Plugin command failed!")
}
return nil

View File

@ -99,6 +99,7 @@ type testModel struct{}
var _ ui.Tabular = &testModel{}
func (t *testModel) SetInstance(string) {}
func (t *testModel) Empty() bool { return false }
func (t *testModel) Peek() render.TableData { return makeTableData() }
func (t *testModel) ClusterWide() bool { return false }

View File

@ -102,7 +102,7 @@ func (a *App) Init(version string, rate int) error {
func (a *App) bindKeys() {
a.AddActions(ui.KeyActions{
ui.KeyH: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
tcell.KeyCtrlH: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false),
tcell.KeyCtrlA: ui.NewSharedKeyAction("Aliases", a.aliasCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false),
@ -427,8 +427,12 @@ func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
func (a *App) gotoResource(res string, clearStack bool) error {
return a.command.run(res, clearStack)
func (a *App) viewResource(gvr, path string, clearStack bool) error {
return a.command.run(gvr, path, clearStack)
}
func (a *App) gotoResource(cmd string, clearStack bool) error {
return a.command.run(cmd, "", clearStack)
}
func (a *App) inject(c model.Component) error {

View File

@ -83,6 +83,10 @@ func (b *Browser) bindKeys() {
})
}
func (b *Browser) SetInstance(path string) {
b.GetModel().SetInstance(path)
}
// Start initializes browser updates.
func (b *Browser) Start() {
b.Stop()

View File

@ -144,7 +144,7 @@ func (c *ClusterInfo) updateStyle() {
}
func fetchResources(app *App) (*v1.NodeList, *mv1beta1.NodeMetricsList, error) {
nn, err := dao.FetchNodes(app.factory)
nn, err := dao.FetchNodes(app.factory, "")
if err != nil {
return nil, nil, err
}

View File

@ -1,6 +1,7 @@
package view
import (
"errors"
"fmt"
"regexp"
"strings"
@ -42,8 +43,42 @@ func (c *Command) Init() error {
return nil
}
func (c *Command) xrayCmd(cmd string) error {
// if _, ok := c.app.Content.GetPrimitive("main").(*Xray); ok {
// return errors.New("unable to locate main panel")
// }
// if c.app.Content.Top() != nil && c.app.Content.Top().Name() == xrayTitle {
// c.app.Content.Pop()
// return nil
// }
tokens := strings.Split(cmd, " ")
if len(tokens) < 2 {
return errors.New("You must specify a resource")
}
gvr, ok := c.alias.AsGVR(tokens[1])
if !ok {
return fmt.Errorf("Huh? `%s` Command not found", cmd)
}
return c.exec(cmd, "xrays", NewXray(gvr), true)
// if err := c.app.inject(NewXray(gvr)); err != nil {
// c.app.Flash().Err(err)
// return nil
// }
// c.app.Config.SetActiveView(cmd)
// if err := c.app.Config.Save(); err != nil {
// log.Error().Err(err).Msg("Config save failed!")
// }
// return nil
}
// Exec the Command by showing associated display.
func (c *Command) run(cmd string, clearStack bool) error {
func (c *Command) run(cmd, path string, clearStack bool) error {
if c.specialCmd(cmd) {
return nil
}
@ -59,8 +94,8 @@ func (c *Command) run(cmd string, clearStack bool) error {
if len(cmds) == 2 && c.app.switchCtx(cmds[1], true) != nil {
return fmt.Errorf("context switch failed!")
}
view := c.componentFor(gvr, v)
return c.exec(gvr, view, clearStack)
view := c.componentFor(gvr, path, v)
return c.exec(cmd, gvr, view, clearStack)
default:
// checks if Command includes a namespace
ns := c.app.Config.ActiveNamespace()
@ -70,7 +105,8 @@ func (c *Command) run(cmd string, clearStack bool) error {
if !c.app.switchNS(ns) {
return fmt.Errorf("namespace switch failed for ns %q", ns)
}
return c.exec(gvr, c.componentFor(gvr, v), clearStack)
return c.exec(cmd, gvr, c.componentFor(gvr, path, v), clearStack)
}
}
@ -85,7 +121,7 @@ func (c *Command) Reset() error {
}
func (c *Command) defaultCmd() error {
return c.run(c.app.Config.ActiveView(), true)
return c.run(c.app.Config.ActiveView(), "", true)
}
func (c *Command) specialCmd(cmd string) bool {
@ -100,6 +136,11 @@ func (c *Command) specialCmd(cmd string) bool {
case "a", "alias":
c.app.aliasCmd(nil)
return true
case "x", "xray":
if err := c.xrayCmd(cmd); err != nil {
log.Error().Err(err).Msgf("Invalid command")
}
return true
default:
if !canRX.MatchString(cmd) {
return false
@ -130,7 +171,7 @@ func (c *Command) viewMetaFor(cmd string) (string, *MetaViewer, error) {
return gvr.String(), &v, nil
}
func (c *Command) componentFor(gvr string, v *MetaViewer) ResourceViewer {
func (c *Command) componentFor(gvr, path string, v *MetaViewer) ResourceViewer {
var view ResourceViewer
if v.viewerFn != nil {
log.Debug().Msgf("Custom viewer for %s", gvr)
@ -140,6 +181,7 @@ func (c *Command) componentFor(gvr string, v *MetaViewer) ResourceViewer {
view = NewBrowser(client.NewGVR(gvr))
}
view.SetInstance(path)
if v.enterFn != nil {
log.Debug().Msgf("SETTING CUSTOM ENTER ON %s", gvr)
view.GetTable().SetEnterFn(v.enterFn)
@ -148,15 +190,13 @@ func (c *Command) componentFor(gvr string, v *MetaViewer) ResourceViewer {
return view
}
func (c *Command) exec(gvr string, comp model.Component, clearStack bool) error {
func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) error {
if comp == nil {
return fmt.Errorf("No component given for %s", gvr)
}
g := client.NewGVR(gvr)
c.app.Flash().Infof("Viewing %s resource...", g.ToR())
log.Debug().Msgf("Running Command %s", gvr)
c.app.Config.SetActiveView(g.ToR())
c.app.Flash().Infof("Running command %s", cmd)
c.app.Config.SetActiveView(cmd)
if err := c.app.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!")
}

View File

@ -3,24 +3,48 @@ package view
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/derailed/k9s/internal/client"
)
// K9sEnv represent K9s available env variables.
type K9sEnv map[string]string
// EnvRX match $XXX custom arg.
var envRX = regexp.MustCompile(`\A\$([\w]+)`)
var envRX = regexp.MustCompile(`\$([\w]+)(\d*)`)
func (e K9sEnv) envFor(n string) (string, error) {
envs := envRX.FindStringSubmatch(n)
func (e K9sEnv) envFor(ns, args string) (string, error) {
envs := envRX.FindStringSubmatch(args)
if len(envs) == 0 {
return n, nil
}
env, ok := e[strings.ToUpper(envs[1])]
if !ok {
return "", fmt.Errorf("No matching for %s", n)
return args, nil
}
return envRX.ReplaceAllString(n, env), nil
q := envs[1]
if envs[2] == "" {
return e.subOut(args, q)
}
var index, err = strconv.Atoi(envs[2])
if err != nil {
return args, err
}
if client.IsNamespaced(ns) {
index -= 1
}
if index >= 0 {
q += strconv.Itoa(index)
}
return e.subOut(args, q)
}
func (e K9sEnv) subOut(args, q string) (string, error) {
env, ok := e[strings.ToUpper(q)]
if !ok {
return "", fmt.Errorf("no env vars exists for argument %q using key %q", args, q)
}
return envRX.ReplaceAllString(args, env), nil
}

View File

@ -11,14 +11,16 @@ func TestK9sEnv(t *testing.T) {
uu := map[string]struct {
q string
ns string
err error
e string
}{
"match": {q: "$A", e: "10"},
"noMatch": {q: "$BLEE", err: errors.New("No matching for $BLEE"), e: ""},
"noMatch": {q: "$BLEE", err: errors.New(`no env vars exists for argument "$BLEE" using key "BLEE"`), e: ""},
"lower": {q: "$b", e: "blee"},
"dash": {q: "$col0", e: "fred"},
"mix": {q: "$col0-blee", e: "fred-blee"},
"subs": {q: `{"spec" : {"suspend" : $COL0 }}`, e: `{"spec" : {"suspend" : fred }}`},
}
e := K9sEnv{
@ -30,7 +32,7 @@ func TestK9sEnv(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
a, err := e.envFor(u.q)
a, err := e.envFor(u.ns, u.q)
assert.Equal(t, u.err, err)
assert.Equal(t, u.e, a)
})

View File

@ -2,7 +2,6 @@ package view
import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
@ -57,7 +56,7 @@ func isResourcePath(p string) bool {
func (l *LogsExtender) showLogs(path string, prev bool) {
log.Debug().Msgf("SHOWING LOGS path %q", path)
// Need to load and wait for pods
ns, _ := render.Namespaced(path)
ns, _ := client.Namespaced(path)
_, err := l.App().factory.CanForResource(ns, "v1/pods", client.MonitorAccess)
if err != nil {
l.App().Flash().Err(err)

View File

@ -91,6 +91,7 @@ type testTableModel struct{}
var _ ui.Tabular = &testTableModel{}
func (t *testTableModel) SetInstance(string) {}
func (t *testTableModel) Empty() bool { return false }
func (t *testTableModel) Peek() render.TableData { return makeTableData() }
func (t *testTableModel) ClusterWide() bool { return false }

View File

@ -77,6 +77,7 @@ type ResourceViewer interface {
// SetBindKeys provision additional key bindings.
SetBindKeysFn(BindKeysFunc)
SetInstance(string)
}
// LogViewer represents a log viewer.

484
internal/view/xray.go Normal file
View File

@ -0,0 +1,484 @@
package view
import (
"context"
"fmt"
"regexp"
"strings"
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/xray"
"github.com/derailed/tview"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
"github.com/sahilm/fuzzy"
)
const xrayTitle = "Xray"
// Xray represents an xray tree view.
type Xray struct {
*tview.TreeView
actions ui.KeyActions
app *App
gvr client.GVR
selectedNode string
model *model.Tree
cancelFn context.CancelFunc
cmdBuff *ui.CmdBuff
expandNodes bool
}
var _ ResourceViewer = (*Xray)(nil)
// NewXray returns a new view.
func NewXray(gvr client.GVR) ResourceViewer {
a := Xray{
TreeView: tview.NewTreeView(),
model: model.NewTree(gvr.String()),
expandNodes: true,
actions: make(ui.KeyActions),
cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff),
}
return &a
}
// Init initializes the view
func (x *Xray) Init(ctx context.Context) error {
var err error
if x.app, err = extractApp(ctx); err != nil {
return err
}
x.bindKeys()
x.SetBorder(true)
x.SetBorderAttributes(tcell.AttrBold)
x.SetBorderPadding(0, 0, 1, 1)
x.SetBackgroundColor(config.AsColor(x.app.Styles.GetTable().BgColor))
x.SetBorderColor(config.AsColor(x.app.Styles.GetTable().FgColor))
x.SetBorderFocusColor(config.AsColor(x.app.Styles.Frame().Border.FocusColor))
x.SetTitle(" Xray ")
x.SetGraphics(true)
x.SetGraphicsColor(tcell.ColorDimGray)
x.SetInputCapture(x.keyboard)
x.model.SetRefreshRate(time.Duration(x.app.Config.K9s.GetRefreshRate()) * time.Second)
x.model.SetNamespace(client.AllNamespaces)
x.model.AddListener(x)
x.SetChangedFunc(func(n *tview.TreeNode) {
ref, ok := n.GetReference().(xray.NodeSpec)
if !ok {
log.Error().Msgf("No ref found on node %s", n.GetText())
return
}
x.selectedNode = ref.Path
})
return nil
}
// SetInstance sets specific resource instance.
func (x *Xray) SetInstance(string) {}
// Actions returns active menu bindings.
func (x *Xray) Actions() ui.KeyActions {
return x.actions
}
// Hints returns the view hints.
func (x *Xray) Hints() model.MenuHints {
return x.actions.Hints()
}
func (x *Xray) bindKeys() {
x.Actions().Add(ui.KeyActions{
ui.KeySpace: ui.NewKeyAction("Expand/Collapse", x.noopCmd, true),
ui.KeyE: ui.NewKeyAction("Expand/Collapse All", x.toggleCollapseCmd, true),
ui.KeyV: ui.NewKeyAction("Goto", x.gotoCmd, true),
tcell.KeyEnter: ui.NewKeyAction("Goto", x.gotoCmd, true),
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", x.activateCmd, false),
tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", x.eraseCmd, false),
tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", x.eraseCmd, false),
tcell.KeyDelete: ui.NewSharedKeyAction("Erase", x.eraseCmd, false),
tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", x.clearCmd, false),
tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", x.resetCmd, false),
})
}
func (x *Xray) keyboard(evt *tcell.EventKey) *tcell.EventKey {
key := evt.Key()
if key == tcell.KeyRune {
if x.cmdBuff.IsActive() {
x.cmdBuff.Add(evt.Rune())
x.ClearSelection()
x.update(x.filter(x.model.Peek()))
x.UpdateTitle()
return nil
}
key = mapKey(evt)
}
if a, ok := x.actions[key]; ok {
return a.Action(evt)
}
return evt
}
func (x *Xray) noopCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
func (x *Xray) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if x.app.InCmdMode() {
return evt
}
x.app.Flash().Info("Filter mode activated.")
x.cmdBuff.SetActive(true)
return nil
}
func (x *Xray) clearCmd(evt *tcell.EventKey) *tcell.EventKey {
if !x.cmdBuff.IsActive() {
return evt
}
x.cmdBuff.Clear()
x.model.ClearFilter()
x.Start()
return nil
}
func (x *Xray) eraseCmd(evt *tcell.EventKey) *tcell.EventKey {
if x.cmdBuff.IsActive() {
x.cmdBuff.Delete()
}
x.UpdateTitle()
return nil
}
func (x *Xray) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
if !x.cmdBuff.IsActive() {
return evt
}
x.cmdBuff.SetActive(false)
cmd := x.cmdBuff.String()
x.model.SetFilter(cmd)
x.Start()
return nil
}
func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !x.cmdBuff.InCmdMode() {
x.cmdBuff.Reset()
return x.app.PrevCmd(evt)
}
x.app.Flash().Info("Clearing filter...")
x.cmdBuff.Reset()
x.model.ClearFilter()
x.Start()
return nil
}
func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if x.cmdBuff.IsActive() {
if ui.IsLabelSelector(x.cmdBuff.String()) {
x.Start()
return nil
}
}
n := x.GetCurrentNode()
if n == nil {
return nil
}
ref, ok := n.GetReference().(xray.NodeSpec)
if !ok {
log.Error().Msgf("Expecting a NodeSpec!")
return nil
}
if len(strings.Split(ref.Path, "/")) == 1 {
return nil
}
if err := x.app.viewResource(client.NewGVR(ref.GVR).ToR(), ref.Path, false); err != nil {
x.app.Flash().Err(err)
}
return nil
}
func (x *Xray) toggleCollapseCmd(evt *tcell.EventKey) *tcell.EventKey {
x.expandNodes = !x.expandNodes
x.GetRoot().Walk(func(node, parent *tview.TreeNode) bool {
if parent != nil {
node.SetExpanded(x.expandNodes)
}
return true
})
return nil
}
// ClearSelection clears the currently selected node.
func (x *Xray) ClearSelection() {
x.selectedNode = ""
x.SetCurrentNode(nil)
}
func (x *Xray) filter(root *xray.TreeNode) *xray.TreeNode {
q := x.cmdBuff.String()
if x.cmdBuff.Empty() || ui.IsLabelSelector(q) {
return root
}
x.UpdateTitle()
if ui.IsFuzzySelector(q) {
return root.Filter(q, fuzzyFilter)
}
return root.Filter(q, rxFilter)
}
// TreeNodeSelected callback for node selection.
func (x *Xray) TreeNodeSelected() {
x.app.QueueUpdateDraw(func() {
n := x.GetCurrentNode()
if n != nil {
n.SetColor(config.AsColor(x.app.Styles.GetTable().CursorColor))
}
})
}
// XrayLoadFailed notifies the load failed.
func (x *Xray) TreeLoadFailed(err error) {
x.app.Flash().Err(err)
}
func (x *Xray) update(node *xray.TreeNode) {
root := makeTreeNode(node, x.expandNodes, x.app.Styles)
if node == nil {
x.app.QueueUpdateDraw(func() {
x.SetRoot(root)
})
return
}
for _, c := range node.Children {
x.hydrate(root, c)
}
if x.selectedNode == "" {
x.selectedNode = node.ID
}
x.app.QueueUpdateDraw(func() {
x.SetRoot(root)
root.Walk(func(node, parent *tview.TreeNode) bool {
ref := node.GetReference().(xray.NodeSpec)
// BOZO!! Figure this out expand/collapse but the root
if parent != nil {
node.SetExpanded(x.expandNodes)
} else {
node.SetExpanded(true)
}
ref, ok := node.GetReference().(xray.NodeSpec)
if !ok {
log.Error().Msgf("No ref found on node %s", node.GetText())
return false
}
if ref.Path == x.selectedNode {
node.SetExpanded(true).SetSelectable(true)
x.SetCurrentNode(node)
}
return true
})
})
}
// XrayDataChanged notifies the model data changed.
func (x *Xray) TreeChanged(node *xray.TreeNode) {
log.Debug().Msgf("Tree Changed %d", len(node.Children))
x.update(x.filter(node))
x.UpdateTitle()
}
func (x *Xray) hydrate(parent *tview.TreeNode, n *xray.TreeNode) {
node := makeTreeNode(n, x.expandNodes, x.app.Styles)
for _, c := range n.Children {
x.hydrate(node, c)
}
parent.AddChild(node)
}
// SetEnvFn sets the custom environment function.
func (x *Xray) SetEnvFn(EnvFunc) {}
// Refresh refresh the view
func (x *Xray) Refresh() {
}
// BufferChanged indicates the buffer was changed.
func (x *Xray) BufferChanged(s string) {}
// BufferActive indicates the buff activity changed.
func (x *Xray) BufferActive(state bool, k ui.BufferKind) {
x.app.BufferActive(state, k)
}
func (x *Xray) defaultContext() context.Context {
ctx := context.WithValue(context.Background(), internal.KeyFactory, x.app.factory)
ctx = context.WithValue(ctx, internal.KeyFields, "")
if x.cmdBuff.Empty() {
ctx = context.WithValue(ctx, internal.KeyLabels, "")
} else {
ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(x.cmdBuff.String()))
}
return ctx
}
// Start initializes resource watch loop.
func (x *Xray) Start() {
x.Stop()
log.Debug().Msgf("XRAY STARTING! -- %q", x.selectedNode)
x.cmdBuff.AddListener(x.app.Cmd())
x.cmdBuff.AddListener(x)
x.app.SetFocus(x)
ctx := x.defaultContext()
ctx, x.cancelFn = context.WithCancel(ctx)
x.model.Watch(ctx)
x.UpdateTitle()
}
// Stop terminates watch loop.
func (x *Xray) Stop() {
log.Debug().Msgf("XRAY STOPPING!")
if x.cancelFn == nil {
return
}
x.cancelFn()
x.cancelFn = nil
x.cmdBuff.RemoveListener(x.app.Cmd())
x.cmdBuff.RemoveListener(x)
}
// SetBindKeysFn sets up extra key bindings.
func (x *Xray) SetBindKeysFn(BindKeysFunc) {}
// SetContextFn sets custom context.
func (x *Xray) SetContextFn(ContextFunc) {}
// Name returns the component name.
func (x *Xray) Name() string { return "XRay" }
// GetTable returns the underlying table.
func (x *Xray) GetTable() *Table { return nil }
// GVR returns a resource descriptor.
func (x *Xray) GVR() string { return x.gvr.String() }
// App returns the current app handle.
func (x *Xray) App() *App {
return x.app
}
// UpdateTitle updates the view title.
func (x *Xray) UpdateTitle() {
x.SetTitle(x.styleTitle())
}
func (x *Xray) styleTitle() string {
rc := x.GetRowCount()
if rc > 0 {
rc--
}
base := strings.Title(xrayTitle)
ns := x.model.GetNamespace()
if client.IsAllNamespaces(ns) {
ns = client.NamespaceAll
}
buff := x.cmdBuff.String()
var title string
if ns == client.ClusterScope {
title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, rc), x.app.Styles.Frame())
} else {
title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, rc), x.app.Styles.Frame())
}
if buff == "" {
return title
}
if ui.IsLabelSelector(buff) {
buff = ui.TrimLabelSelector(buff)
}
return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), x.app.Styles.Frame())
}
// ----------------------------------------------------------------------------
// Helpers...
func mapKey(evt *tcell.EventKey) tcell.Key {
key := tcell.Key(evt.Rune())
if evt.Modifiers() == tcell.ModAlt {
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
}
return key
}
func fuzzyFilter(q, path string) bool {
q = strings.TrimSpace(q[2:])
mm := fuzzy.Find(q, []string{path})
log.Debug().Msgf("%#v", mm)
if len(mm) > 0 {
return true
}
return false
}
func rxFilter(q, path string) bool {
rx := regexp.MustCompile(`(?i)` + q)
tokens := strings.Split(path, xray.PathSeparator)
for _, t := range tokens {
if rx.MatchString(t) {
return true
}
}
return false
}
func makeTreeNode(node *xray.TreeNode, expanded bool, styles *config.Styles) *tview.TreeNode {
n := tview.NewTreeNode("No data...")
if node != nil {
n.SetText(node.Title())
n.SetReference(xray.NodeSpec{GVR: node.GVR, Path: node.ID})
}
n.SetSelectable(true)
n.SetExpanded(expanded)
n.SetColor(config.AsColor(styles.GetTable().CursorColor))
n.SetSelectedFunc(func() {
n.SetExpanded(!n.IsExpanded())
})
return n
}

101
internal/xray/container.go Normal file
View File

@ -0,0 +1,101 @@
package xray
import (
"context"
"fmt"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
)
type Container struct{}
func (c *Container) Render(ctx context.Context, ns string, o interface{}) error {
co, ok := o.(render.ContainerRes)
if !ok {
return fmt.Errorf("Expected ContainerRes, but got %T", o)
}
f, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return fmt.Errorf("no factory found in context")
}
root := NewTreeNode("containers", client.FQN(ns, co.Container.Name))
parent := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
pns, _ := client.Namespaced(parent.ID)
c.envRefs(f, root, pns, co.Container)
if !root.Empty() {
parent.Add(root)
}
return nil
}
func (c *Container) envRefs(f dao.Factory, parent *TreeNode, ns string, co *v1.Container) {
for _, e := range co.Env {
if e.ValueFrom == nil {
continue
}
c.secretRefs(f, parent, ns, e.ValueFrom.SecretKeyRef)
c.configMapRefs(f, parent, ns, e.ValueFrom.ConfigMapKeyRef)
}
for _, e := range co.EnvFrom {
if e.ConfigMapRef != nil {
gvr, id := "v1/configmaps", client.FQN(ns, e.ConfigMapRef.Name)
c.addRef(f, parent, gvr, id, e.ConfigMapRef.Optional)
}
if e.SecretRef != nil {
gvr, id := "v1/secrets", client.FQN(ns, e.SecretRef.Name)
c.addRef(f, parent, gvr, id, e.SecretRef.Optional)
}
}
}
func (c *Container) secretRefs(f dao.Factory, parent *TreeNode, ns string, ref *v1.SecretKeySelector) {
if ref == nil {
return
}
gvr, id := "v1/secrets", client.FQN(ns, ref.LocalObjectReference.Name)
c.addRef(f, parent, id, gvr, ref.Optional)
}
func (c *Container) configMapRefs(f dao.Factory, parent *TreeNode, ns string, ref *v1.ConfigMapKeySelector) {
if ref == nil {
return
}
gvr, id := "v1/configmaps", client.FQN(ns, ref.LocalObjectReference.Name)
c.addRef(f, parent, gvr, id, ref.Optional)
}
func (c *Container) addRef(f dao.Factory, parent *TreeNode, gvr, id string, optional *bool) {
if parent.Find(gvr, id) == nil {
n := NewTreeNode(gvr, id)
validate(f, n, optional)
parent.Add(n)
}
}
// Helpers...
func validate(f dao.Factory, n *TreeNode, optional *bool) {
if optional == nil || *optional {
n.Extras[StatusKey] = OkStatus
return
}
res, err := f.Get(n.GVR, n.ID, false, labels.Everything())
if err != nil || res == nil {
log.Debug().Msgf("Fail to located ref %q::%q -- %#v-%#v", n.GVR, n.ID, err, res)
n.Extras[StatusKey] = MissingRefStatus
return
}
n.Extras[StatusKey] = OkStatus
}

View File

@ -0,0 +1,244 @@
package xray_test
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/watch"
"github.com/derailed/k9s/internal/xray"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
)
func init() {
zerolog.SetGlobalLevel(zerolog.FatalLevel)
}
func TestCOConfigMapRefs(t *testing.T) {
var re xray.Container
root := xray.NewTreeNode("root", "root")
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())
assert.Nil(t, re.Render(ctx, "", render.ContainerRes{Container: makeCMContainer("c1", false)}))
assert.Equal(t, xray.MissingRefStatus, root.Children[0].Children[0].Extras[xray.StatusKey])
}
func TestCORefs(t *testing.T) {
uu := map[string]struct {
co render.ContainerRes
level1, level2 int
e string
}{
"cm_required": {
co: render.ContainerRes{Container: makeCMContainer("c1", false)},
level1: 1,
level2: 1,
e: xray.MissingRefStatus,
},
"cm_optional": {
co: render.ContainerRes{Container: makeCMContainer("c1", true)},
level1: 1,
level2: 1,
e: xray.OkStatus,
},
"cm_doubleRef": {
co: render.ContainerRes{Container: makeDoubleCMKeysContainer("c1", false)},
level1: 1,
level2: 1,
e: xray.MissingRefStatus,
},
"sec_required": {
co: render.ContainerRes{Container: makeSecContainer("c1", false)},
level1: 1,
level2: 1,
e: xray.MissingRefStatus,
},
"sec_optional": {
co: render.ContainerRes{Container: makeSecContainer("c1", true)},
level1: 1,
level2: 1,
e: xray.OkStatus,
},
"envFrom_optional": {
co: render.ContainerRes{Container: makeCMEnvFromContainer("c1", false)},
level1: 1,
level2: 2,
e: xray.MissingRefStatus,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
var re xray.Container
root := xray.NewTreeNode("root", "root")
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())
assert.Nil(t, re.Render(ctx, "", u.co))
assert.Equal(t, u.level1, root.Size())
assert.Equal(t, u.level2, root.Children[0].Size())
assert.Equal(t, u.e, root.Children[0].Children[0].Extras[xray.StatusKey])
})
}
}
// ----------------------------------------------------------------------------
// Helpers...
func makeFactory() testFactory {
return testFactory{}
}
type testFactory struct {
rows []runtime.Object
}
var _ dao.Factory = testFactory{}
func (f testFactory) Client() client.Connection {
return nil
}
func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
if len(f.rows) > 0 {
return f.rows[0], nil
}
return nil, nil
}
func (f testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) {
if len(f.rows) > 0 {
return f.rows, nil
}
return nil, nil
}
func (f testFactory) ForResource(ns, gvr string) informers.GenericInformer {
return nil
}
func (f testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
return nil, nil
}
func (f testFactory) WaitForCacheSync() {}
func (f testFactory) Forwarders() watch.Forwarders {
return nil
}
func (f testFactory) DeleteForwarder(string) {}
func makeCMEnvFromContainer(n string, optional bool) *v1.Container {
return &v1.Container{
Name: n,
EnvFrom: []v1.EnvFromSource{
{
ConfigMapRef: &v1.ConfigMapEnvSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "cm1",
},
Optional: &optional,
},
SecretRef: &v1.SecretEnvSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "sec1",
},
Optional: &optional,
},
},
},
}
}
func makeCMContainer(n string, optional bool) *v1.Container {
return &v1.Container{
Name: n,
Env: []v1.EnvVar{
{
Name: "e1",
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "cm1",
},
Key: "k1",
Optional: &optional,
},
},
},
},
}
}
func makeSecContainer(n string, optional bool) *v1.Container {
return &v1.Container{
Name: n,
Env: []v1.EnvVar{
{
Name: "e1",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "sec1",
},
Key: "k1",
Optional: &optional,
},
},
},
},
}
}
func makeDoubleCMKeysContainer(n string, optional bool) *v1.Container {
return &v1.Container{
Name: n,
Env: []v1.EnvVar{
{
Name: "e1",
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "cm1",
},
Key: "k2",
Optional: &optional,
},
},
},
{
Name: "e2",
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "cm1",
},
Key: "k1",
Optional: &optional,
},
},
},
},
}
}
func load(t *testing.T, n string) *unstructured.Unstructured {
raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n))
assert.Nil(t, err)
var o unstructured.Unstructured
err = json.Unmarshal(raw, &o)
assert.Nil(t, err)
return &o
}

94
internal/xray/dp.go Normal file
View File

@ -0,0 +1,94 @@
package xray
import (
"context"
"fmt"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
type Deployment struct{}
func (d *Deployment) Render(ctx context.Context, ns string, o interface{}) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected Unstructured, but got %T", o)
}
var dp appsv1.Deployment
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &dp)
if err != nil {
return err
}
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
nsID, gvr := client.FQN(client.ClusterScope, dp.Namespace), "v1/namespaces"
nsn := parent.Find(gvr, nsID)
if nsn == nil {
nsn = NewTreeNode(gvr, nsID)
parent.Add(nsn)
}
root := NewTreeNode("apps/v1/deployments", client.FQN(dp.Namespace, dp.Name))
nsn.Add(root)
oo, err := locatePods(ctx, dp.Namespace, dp.Spec.Selector)
if err != nil {
return err
}
ctx = context.WithValue(ctx, KeyParent, root)
var re Pod
for _, o := range oo {
p := o.(*unstructured.Unstructured)
if err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {
return err
}
}
return d.validate(root, dp)
}
func (*Deployment) validate(root *TreeNode, dp appsv1.Deployment) error {
root.Extras[StatusKey] = OkStatus
var r int32
if dp.Spec.Replicas != nil {
r = int32(*dp.Spec.Replicas)
}
a := dp.Status.AvailableReplicas
if a != r {
root.Extras[StatusKey] = ToastStatus
}
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
func locatePods(ctx context.Context, ns string, sel *metav1.LabelSelector) ([]runtime.Object, error) {
l, err := metav1.LabelSelectorAsSelector(sel)
if err != nil {
return nil, err
}
fsel, err := labels.ConvertSelectorToLabelsMap(l.String())
if err != nil {
return nil, err
}
f, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return nil, fmt.Errorf("Expecting a factory but got %T", ctx.Value(internal.KeyFactory))
}
return f.List("v1/pods", ns, false, fsel.AsSelector())
}

40
internal/xray/dp_test.go Normal file
View File

@ -0,0 +1,40 @@
package xray_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/xray"
"github.com/stretchr/testify/assert"
)
func TestDeployRender(t *testing.T) {
uu := map[string]struct {
file string
level1, level2 int
status string
}{
"plain": {
file: "dp",
level1: 1,
level2: 1,
status: xray.OkStatus,
},
}
var re xray.Deployment
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
o := load(t, u.file)
root := xray.NewTreeNode("deployments", "deployments")
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())
assert.Nil(t, re.Render(ctx, "", o))
assert.Equal(t, u.level1, root.Size())
assert.Equal(t, u.level2, root.Children[0].Size())
})
}
}

67
internal/xray/ds.go Normal file
View File

@ -0,0 +1,67 @@
package xray
import (
"context"
"fmt"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
type DaemonSet struct{}
func (d *DaemonSet) Render(ctx context.Context, ns string, o interface{}) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected Unstructured, but got %T", o)
}
var ds appsv1.DaemonSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ds)
if err != nil {
return err
}
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
nsID, gvr := client.FQN(client.ClusterScope, ds.Namespace), "v1/namespaces"
nsn := parent.Find(gvr, nsID)
if nsn == nil {
nsn = NewTreeNode(gvr, nsID)
parent.Add(nsn)
}
root := NewTreeNode("apps/v1/daemonset", client.FQN(ds.Namespace, ds.Name))
nsn.Add(root)
oo, err := locatePods(ctx, ds.Namespace, ds.Spec.Selector)
if err != nil {
return err
}
ctx = context.WithValue(ctx, KeyParent, root)
var re Pod
for _, o := range oo {
p := o.(*unstructured.Unstructured)
if err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {
return err
}
}
return d.validate(root, ds)
}
func (*DaemonSet) validate(root *TreeNode, ds appsv1.DaemonSet) error {
root.Extras[StatusKey] = OkStatus
d := ds.Status.DesiredNumberScheduled
a := ds.Status.NumberAvailable
if d != a {
root.Extras[StatusKey] = ToastStatus
}
return nil
}

40
internal/xray/ds_test.go Normal file
View File

@ -0,0 +1,40 @@
package xray_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/xray"
"github.com/stretchr/testify/assert"
)
func TestDaemonSetRender(t *testing.T) {
uu := map[string]struct {
file string
level1, level2 int
status string
}{
"plain": {
file: "ds",
level1: 1,
level2: 1,
status: xray.OkStatus,
},
}
var re xray.DaemonSet
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
o := load(t, u.file)
root := xray.NewTreeNode("daemonsets", "daemonsets")
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())
assert.Nil(t, re.Render(ctx, "", o))
assert.Equal(t, u.level1, root.Size())
assert.Equal(t, u.level2, root.Children[0].Size())
})
}
}

67
internal/xray/generic.go Normal file
View File

@ -0,0 +1,67 @@
package xray
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/derailed/k9s/internal/client"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
)
// Generic renders a generic resource to screen.
type Generic struct {
table *metav1beta1.Table
}
// SetTable sets the tabular resource.
func (g *Generic) SetTable(t *metav1beta1.Table) {
g.table = t
}
// Render renders a K8s resource to screen.
func (g *Generic) Render(ctx context.Context, ns string, o interface{}) error {
row, ok := o.(metav1beta1.TableRow)
if !ok {
return fmt.Errorf("expecting a TableRow but got %T", o)
}
n, ok := row.Cells[0].(string)
if !ok {
return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0])
}
root := NewTreeNode("generic", client.FQN(ns, n))
parent := ctx.Value(KeyParent).(*TreeNode)
parent.Add(root)
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
func resourceNS(raw []byte) (bool, string, error) {
var obj map[string]interface{}
err := json.Unmarshal(raw, &obj)
if err != nil {
return false, "", err
}
meta, ok := obj["metadata"].(map[string]interface{})
if !ok {
return false, "", errors.New("no metadata found on generic resource")
}
ns, ok := meta["namespace"]
if !ok {
return true, "", nil
}
nns, ok := ns.(string)
if !ok {
return false, "", fmt.Errorf("expecting namespace string type but got %T", ns)
}
return false, nns, nil
}

View File

@ -0,0 +1,44 @@
package xray_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/xray"
"github.com/stretchr/testify/assert"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
)
func TestGenericRender(t *testing.T) {
uu := map[string]struct {
level1 int
status string
}{
"plain": {
level1: 1,
status: xray.OkStatus,
},
}
var re xray.Generic
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
root := xray.NewTreeNode("generics", "generics")
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())
assert.Nil(t, re.Render(ctx, "", makeTable()))
assert.Equal(t, u.level1, root.Size())
})
}
}
// Helpers...
func makeTable() metav1beta1.TableRow {
return metav1beta1.TableRow{
Cells: []interface{}{"fred", "blee"},
}
}

35
internal/xray/ns.go Normal file
View File

@ -0,0 +1,35 @@
package xray
import (
"context"
"fmt"
"github.com/derailed/k9s/internal/client"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
type Namespace struct{}
func (p *Namespace) Render(ctx context.Context, ns string, o interface{}) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected NamespaceWithMetrics, but got %T", o)
}
var nss v1.Namespace
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &nss)
if err != nil {
return err
}
root := NewTreeNode("v1/namespaces", client.FQN(client.ClusterScope, nss.Name))
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
parent.Add(root)
return nil
}

38
internal/xray/ns_test.go Normal file
View File

@ -0,0 +1,38 @@
package xray_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/xray"
"github.com/stretchr/testify/assert"
)
func TestNamespaceRender(t *testing.T) {
uu := map[string]struct {
file string
level1 int
status string
}{
"plain": {
file: "ns",
level1: 1,
status: xray.OkStatus,
},
}
var re xray.Namespace
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
o := load(t, u.file)
root := xray.NewTreeNode("namespaces", "namespaces")
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())
assert.Nil(t, re.Render(ctx, "", o))
assert.Equal(t, u.level1, root.Size())
})
}
}

178
internal/xray/pod.go Normal file
View File

@ -0,0 +1,178 @@
package xray
import (
"context"
"fmt"
"strconv"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubernetes/pkg/util/node"
)
type Pod struct{}
func (p *Pod) Status(po *v1.Pod) {
}
func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error {
pwm, ok := o.(*render.PodWithMetrics)
if !ok {
return fmt.Errorf("Expected PodWithMetrics, but got %T", o)
}
var po v1.Pod
err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object, &po)
if err != nil {
return err
}
phase := p.phase(&po)
ss := po.Status.ContainerStatuses
cr, _, _ := p.statuses(ss)
status := OkStatus
if cr != len(ss) {
status = ToastStatus
}
if phase == "Completed" {
status = CompletedStatus
}
root := NewTreeNode("v1/pods", client.FQN(po.Namespace, po.Name))
root.Extras[StatusKey] = status
root.Extras[StateKey] = strconv.Itoa(cr) + "/" + strconv.Itoa(len(ss))
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
parent.Add(root)
ctx = context.WithValue(ctx, KeyParent, root)
var cre Container
for i := 0; i < len(po.Spec.InitContainers); i++ {
if err := cre.Render(ctx, ns, render.ContainerRes{Container: &po.Spec.InitContainers[i]}); err != nil {
return err
}
}
for i := 0; i < len(po.Spec.Containers); i++ {
if err := cre.Render(ctx, ns, render.ContainerRes{Container: &po.Spec.Containers[i]}); err != nil {
return err
}
}
p.podVolumeRefs(root, po.Namespace, po.Spec.Volumes)
return nil
}
func (*Pod) podVolumeRefs(parent *TreeNode, ns string, vv []v1.Volume) {
for _, v := range vv {
sv := v.VolumeSource.Secret
if sv != nil {
parent.Add(NewTreeNode("v1/secrets", client.FQN(ns, sv.SecretName)))
continue
}
cmv := v.VolumeSource.ConfigMap
if cmv != nil {
parent.Add(NewTreeNode("v1/configmaps", client.FQN(ns, cmv.LocalObjectReference.Name)))
}
}
}
// BOZO!! Dedup...
func (*Pod) statuses(ss []v1.ContainerStatus) (cr, ct, rc int) {
for _, c := range ss {
if c.State.Terminated != nil {
ct++
}
if c.Ready {
cr = cr + 1
}
rc += int(c.RestartCount)
}
return
}
func (p *Pod) phase(po *v1.Pod) string {
status := string(po.Status.Phase)
if po.Status.Reason != "" {
if po.DeletionTimestamp != nil && po.Status.Reason == node.NodeUnreachablePodReason {
return "Unknown"
}
status = po.Status.Reason
}
status, ok := p.initContainerPhase(po.Status, len(po.Spec.InitContainers), status)
if ok {
return status
}
status, ok = p.containerPhase(po.Status, status)
if ok && status == "Completed" {
status = "Running"
}
if po.DeletionTimestamp == nil {
return status
}
return "Terminated"
}
func (*Pod) containerPhase(st v1.PodStatus, status string) (string, bool) {
var running bool
for i := len(st.ContainerStatuses) - 1; i >= 0; i-- {
cs := st.ContainerStatuses[i]
switch {
case cs.State.Waiting != nil && cs.State.Waiting.Reason != "":
status = cs.State.Waiting.Reason
case cs.State.Terminated != nil && cs.State.Terminated.Reason != "":
status = cs.State.Terminated.Reason
case cs.State.Terminated != nil:
if cs.State.Terminated.Signal != 0 {
status = "Signal:" + strconv.Itoa(int(cs.State.Terminated.Signal))
} else {
status = "ExitCode:" + strconv.Itoa(int(cs.State.Terminated.ExitCode))
}
case cs.Ready && cs.State.Running != nil:
running = true
}
}
return status, running
}
func (p *Pod) initContainerPhase(st v1.PodStatus, initCount int, status string) (string, bool) {
for i, cs := range st.InitContainerStatuses {
s := checkContainerStatus(cs, i, initCount)
if s == "" {
continue
}
return s, true
}
return status, false
}
func checkContainerStatus(cs v1.ContainerStatus, i, initCount int) string {
switch {
case cs.State.Terminated != nil:
if cs.State.Terminated.ExitCode == 0 {
return ""
}
if cs.State.Terminated.Reason != "" {
return "Init:" + cs.State.Terminated.Reason
}
if cs.State.Terminated.Signal != 0 {
return "Init:Signal:" + strconv.Itoa(int(cs.State.Terminated.Signal))
}
return "Init:ExitCode:" + strconv.Itoa(int(cs.State.Terminated.ExitCode))
case cs.State.Waiting != nil && cs.State.Waiting.Reason != "" && cs.State.Waiting.Reason != "PodInitializing":
return "Init:" + cs.State.Waiting.Reason
default:
return "Init:" + strconv.Itoa(i) + "/" + strconv.Itoa(initCount)
}
}

194
internal/xray/pod_test.go Normal file
View File

@ -0,0 +1,194 @@
package xray_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/xray"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestPodRender(t *testing.T) {
uu := map[string]struct {
file string
level1, level2 int
status string
}{
"plain": {
file: "po",
level1: 1,
level2: 1,
status: xray.OkStatus,
},
"withInit": {
file: "init",
level1: 1,
level2: 1,
status: xray.OkStatus,
},
}
var re xray.Pod
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
o := load(t, u.file)
root := xray.NewTreeNode("pods", "pods")
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())
assert.Nil(t, re.Render(ctx, "", &render.PodWithMetrics{Raw: o}))
assert.Equal(t, u.level1, root.Size())
assert.Equal(t, u.level2, root.Children[0].Size())
})
}
}
// ----------------------------------------------------------------------------
// Helpers...
func makePod(n string) v1.Pod {
return v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: n,
Namespace: "default",
},
}
}
func makePodEnv(n, ref string, optional bool) v1.Pod {
po := makePod(n)
po.Spec.Containers = []v1.Container{
{
Name: "c1",
Env: []v1.EnvVar{
{
Name: "e1",
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "cm1",
},
Key: "k1",
Optional: &optional,
},
},
},
},
},
{
Name: "c2",
Env: []v1.EnvVar{
{
Name: "e2",
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "cm2",
},
Key: "k2",
Optional: &optional,
},
},
},
},
},
}
po.Spec.InitContainers = []v1.Container{
{
Name: "ic1",
Env: []v1.EnvVar{
{
Name: "e1",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{Name: "sec2"},
Key: "k2",
Optional: &optional,
},
},
},
},
},
}
return po
}
func makePodStatus(n, ref string, optional bool) v1.Pod {
po := makePod(n)
po.Status = v1.PodStatus{
Phase: v1.PodRunning,
Conditions: []v1.PodCondition{
{
Type: v1.PodReady,
Status: v1.ConditionTrue,
},
},
ContainerStatuses: []v1.ContainerStatus{
{
Name: "c1",
State: v1.ContainerState{Running: &v1.ContainerStateRunning{}},
},
},
}
po.Spec.Containers = []v1.Container{
{
Name: "c1",
Env: []v1.EnvVar{
{
Name: "e1",
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "cm1",
},
Key: "k1",
Optional: &optional,
},
},
},
},
},
{
Name: "c2",
Env: []v1.EnvVar{
{
Name: "e2",
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "cm2",
},
Key: "k2",
Optional: &optional,
},
},
},
},
},
}
po.Spec.InitContainers = []v1.Container{
{
Name: "ic1",
Env: []v1.EnvVar{
{
Name: "e1",
ValueFrom: &v1.EnvVarSource{
SecretKeyRef: &v1.SecretKeySelector{
LocalObjectReference: v1.LocalObjectReference{Name: "sec2"},
Key: "k2",
Optional: &optional,
},
},
},
},
},
}
return po
}

87
internal/xray/sts.go Normal file
View File

@ -0,0 +1,87 @@
package xray
import (
"context"
"fmt"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
type StatefulSet struct{}
func (p *StatefulSet) Render(ctx context.Context, ns string, o interface{}) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected Unstructured, but got %T", o)
}
var sts appsv1.StatefulSet
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts)
if err != nil {
return err
}
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
nsID, gvr := client.FQN(client.ClusterScope, sts.Namespace), "v1/namespaces"
nsn := parent.Find(gvr, nsID)
if nsn == nil {
nsn = NewTreeNode(gvr, nsID)
parent.Add(nsn)
}
root := NewTreeNode("apps/v1/deployments", client.FQN(sts.Namespace, sts.Name))
nsn.Add(root)
l, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector)
if err != nil {
return err
}
f, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return fmt.Errorf("Expecting a factory but got %T", ctx.Value(internal.KeyFactory))
}
fsel, err := labels.ConvertSelectorToLabelsMap(l.String())
if err != nil {
return err
}
oo, err := f.List("v1/pods", sts.Namespace, false, fsel.AsSelector())
if err != nil {
return err
}
ctx = context.WithValue(ctx, KeyParent, root)
var re Pod
for _, o := range oo {
p := o.(*unstructured.Unstructured)
if err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {
return err
}
}
root.Extras[StatusKey] = OkStatus
var r int32
if sts.Spec.Replicas != nil {
r = int32(*sts.Spec.Replicas)
}
a := sts.Status.Replicas
if a != r {
root.Extras[StatusKey] = ToastStatus
}
return nil
}

40
internal/xray/sts_test.go Normal file
View File

@ -0,0 +1,40 @@
package xray_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/xray"
"github.com/stretchr/testify/assert"
)
func TestStatefulSetRender(t *testing.T) {
uu := map[string]struct {
file string
level1, level2 int
status string
}{
"plain": {
file: "sts",
level1: 1,
level2: 1,
status: xray.OkStatus,
},
}
var re xray.StatefulSet
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
o := load(t, u.file)
root := xray.NewTreeNode("statefulsets", "statefulsets")
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())
assert.Nil(t, re.Render(ctx, "", o))
assert.Equal(t, u.level1, root.Size())
assert.Equal(t, u.level2, root.Children[0].Size())
})
}
}

82
internal/xray/svc.go Normal file
View File

@ -0,0 +1,82 @@
package xray
import (
"context"
"fmt"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)
type Service struct{}
func (s *Service) Render(ctx context.Context, ns string, o interface{}) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("Expected Unstructured, but got %T", o)
}
var svc v1.Service
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &svc)
if err != nil {
return err
}
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
}
nsID, gvr := client.FQN(client.ClusterScope, svc.Namespace), "v1/namespaces"
nsn := parent.Find(gvr, nsID)
if nsn == nil {
nsn = NewTreeNode(gvr, nsID)
parent.Add(nsn)
}
root := NewTreeNode("apps/v1/services", client.FQN(svc.Namespace, svc.Name))
nsn.Add(root)
oo, err := s.locatePods(ctx, svc.Namespace, svc.Spec.Selector)
if err != nil {
return err
}
ctx = context.WithValue(ctx, KeyParent, root)
var re Pod
for _, o := range oo {
p := o.(*unstructured.Unstructured)
if err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {
return err
}
}
root.Extras[StatusKey] = OkStatus
return nil
}
func (s *Service) locatePods(ctx context.Context, ns string, sel map[string]string) ([]runtime.Object, error) {
f, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return nil, fmt.Errorf("Expecting a factory but got %T", ctx.Value(internal.KeyFactory))
}
var ll []string
for k, v := range sel {
ll = append(ll, fmt.Sprintf("%s=%s", k, v))
}
fsel, err := labels.ConvertSelectorToLabelsMap(strings.Join(ll, ","))
if err != nil {
return nil, err
}
return f.List("v1/pods", ns, false, fsel.AsSelector())
}

40
internal/xray/svc_test.go Normal file
View File

@ -0,0 +1,40 @@
package xray_test
import (
"context"
"testing"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/xray"
"github.com/stretchr/testify/assert"
)
func TestServiceRender(t *testing.T) {
uu := map[string]struct {
file string
level1, level2 int
status string
}{
"plain": {
file: "svc",
level1: 1,
level2: 1,
status: xray.OkStatus,
},
}
var re xray.Service
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
o := load(t, u.file)
root := xray.NewTreeNode("services", "services")
ctx := context.WithValue(context.Background(), xray.KeyParent, root)
ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())
assert.Nil(t, re.Render(ctx, "", o))
assert.Equal(t, u.level1, root.Size())
assert.Equal(t, u.level2, root.Children[0].Size())
})
}
}

View File

@ -0,0 +1,118 @@
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"annotations": {
"deployment.kubernetes.io/revision": "3",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"nginx\"},\"name\":\"nginx\",\"namespace\":\"default\"},\"spec\":{\"replicas\":1,\"selector\":{\"matchLabels\":{\"app\":\"nginx\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"nginx\"}},\"spec\":{\"containers\":[{\"env\":[{\"name\":\"FRED\",\"valueFrom\":{\"configMapKeyRef\":{\"key\":\"fred\",\"name\":\"busy\"}}},{\"name\":\"PROPS\",\"valueFrom\":{\"configMapKeyRef\":{\"key\":\"props\",\"name\":\"busy\"}}}],\"image\":\"k8s.gcr.io/nginx-slim:0.8\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}],\"resources\":{\"limits\":{\"cpu\":\"100m\",\"memory\":\"200Mi\"}}}]}}}}\n"
},
"creationTimestamp": "2020-01-16T04:18:04Z",
"generation": 4,
"labels": {
"app": "nginx"
},
"name": "nginx",
"namespace": "default",
"resourceVersion": "3338230",
"selfLink": "/apis/apps/v1/namespaces/default/deployments/nginx",
"uid": "a2baf77e-5301-4efd-ac40-ff3da9716c80"
},
"spec": {
"progressDeadlineSeconds": 600,
"replicas": 1,
"revisionHistoryLimit": 10,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"strategy": {
"rollingUpdate": {
"maxSurge": "25%",
"maxUnavailable": "25%"
},
"type": "RollingUpdate"
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [
{
"env": [
{
"name": "FRED",
"valueFrom": {
"configMapKeyRef": {
"key": "fred",
"name": "busy"
}
}
},
{
"name": "PROPS",
"valueFrom": {
"configMapKeyRef": {
"key": "props",
"name": "busy"
}
}
}
],
"image": "k8s.gcr.io/nginx-slim:0.8",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
],
"resources": {
"limits": {
"cpu": "100m",
"memory": "200Mi"
}
},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
],
"dnsPolicy": "ClusterFirst",
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"terminationGracePeriodSeconds": 30
}
}
},
"status": {
"availableReplicas": 1,
"conditions": [
{
"lastTransitionTime": "2020-01-16T14:52:45Z",
"lastUpdateTime": "2020-01-16T14:52:45Z",
"message": "Deployment has minimum availability.",
"reason": "MinimumReplicasAvailable",
"status": "True",
"type": "Available"
},
{
"lastTransitionTime": "2020-01-18T01:20:50Z",
"lastUpdateTime": "2020-01-18T01:20:50Z",
"message": "ReplicaSet \"nginx-5bbc876d89\" has successfully progressed.",
"reason": "NewReplicaSetAvailable",
"status": "True",
"type": "Progressing"
}
],
"observedGeneration": 4,
"readyReplicas": 1,
"replicas": 1,
"updatedReplicas": 1
}
}

View File

@ -0,0 +1,109 @@
{
"apiVersion": "apps/v1",
"kind": "DaemonSet",
"metadata": {
"annotations": {
"deprecated.daemonset.template.generation": "1",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"DaemonSet\",\"metadata\":{\"annotations\":{},\"labels\":{\"k8s-app\":\"fluentd-logging\"},\"name\":\"fluentd-elasticsearch\",\"namespace\":\"default\"},\"spec\":{\"selector\":{\"matchLabels\":{\"name\":\"fluentd-elasticsearch\"}},\"template\":{\"metadata\":{\"labels\":{\"name\":\"fluentd-elasticsearch\"}},\"spec\":{\"containers\":[{\"image\":\"fluentd\",\"name\":\"fluentd-elasticsearch\",\"resources\":{\"limits\":{\"memory\":\"200Mi\"},\"requests\":{\"cpu\":\"100m\",\"memory\":\"200Mi\"}},\"volumeMounts\":[{\"mountPath\":\"/var/log\",\"name\":\"varlog\"},{\"mountPath\":\"/var/lib/docker/containers\",\"name\":\"varlibdockercontainers\",\"readOnly\":true}]}],\"terminationGracePeriodSeconds\":1,\"tolerations\":[{\"effect\":\"NoSchedule\",\"key\":\"node-role.kubernetes.io/master\"}],\"volumes\":[{\"hostPath\":{\"path\":\"/var/log\"},\"name\":\"varlog\"},{\"hostPath\":{\"path\":\"/var/lib/docker/containers\"},\"name\":\"varlibdockercontainers\"}]}}}}\n"
},
"creationTimestamp": "2020-01-18T14:43:04Z",
"generation": 1,
"labels": {
"k8s-app": "fluentd-logging"
},
"name": "fluentd-elasticsearch",
"namespace": "default",
"resourceVersion": "3450170",
"selfLink": "/apis/apps/v1/namespaces/default/daemonsets/fluentd-elasticsearch",
"uid": "8c03864a-a428-4769-b89c-11d66e01614d"
},
"spec": {
"revisionHistoryLimit": 10,
"selector": {
"matchLabels": {
"name": "fluentd-elasticsearch"
}
},
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"name": "fluentd-elasticsearch"
}
},
"spec": {
"containers": [
{
"image": "fluentd",
"imagePullPolicy": "Always",
"name": "fluentd-elasticsearch",
"resources": {
"limits": {
"memory": "200Mi"
},
"requests": {
"cpu": "100m",
"memory": "200Mi"
}
},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/log",
"name": "varlog"
},
{
"mountPath": "/var/lib/docker/containers",
"name": "varlibdockercontainers",
"readOnly": true
}
]
}
],
"dnsPolicy": "ClusterFirst",
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"terminationGracePeriodSeconds": 1,
"tolerations": [
{
"effect": "NoSchedule",
"key": "node-role.kubernetes.io/master"
}
],
"volumes": [
{
"hostPath": {
"path": "/var/log",
"type": ""
},
"name": "varlog"
},
{
"hostPath": {
"path": "/var/lib/docker/containers",
"type": ""
},
"name": "varlibdockercontainers"
}
]
}
},
"updateStrategy": {
"rollingUpdate": {
"maxUnavailable": 1
},
"type": "RollingUpdate"
}
},
"status": {
"currentNumberScheduled": 1,
"desiredNumberScheduled": 1,
"numberAvailable": 1,
"numberMisscheduled": 0,
"numberReady": 1,
"observedGeneration": 1,
"updatedNumberScheduled": 1
}
}

View File

@ -0,0 +1,187 @@
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"name\":\"hurry-up-and-wait\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"command\":[\"sh\",\"-c\",\"echo The app is running! \\u0026\\u0026 sleep 3600\"],\"image\":\"busybox\",\"name\":\"busy\",\"resources\":{\"limits\":{\"cpu\":\"100m\",\"memory\":\"100Mi\"}}}],\"initContainers\":[{\"command\":[\"sh\",\"-c\",\"echo \\\"sleeping...\\\"; sleep 10\"],\"image\":\"busybox\",\"name\":\"init-sleep\",\"resources\":{\"limits\":{\"cpu\":\"100m\",\"memory\":\"100Mi\"}}}]}}\n"
},
"creationTimestamp": "2020-01-18T06:31:29Z",
"name": "hurry-up-and-wait",
"namespace": "default",
"resourceVersion": "3381576",
"selfLink": "/api/v1/namespaces/default/pods/hurry-up-and-wait",
"uid": "6b29055a-433b-4398-bfde-0fd371759bbf"
},
"spec": {
"containers": [
{
"command": [
"sh",
"-c",
"echo The app is running! \u0026\u0026 sleep 3600"
],
"image": "busybox",
"imagePullPolicy": "Always",
"name": "busy",
"resources": {
"limits": {
"cpu": "100m",
"memory": "100Mi"
},
"requests": {
"cpu": "100m",
"memory": "100Mi"
}
},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-rr22g",
"readOnly": true
}
]
}
],
"dnsPolicy": "ClusterFirst",
"enableServiceLinks": true,
"initContainers": [
{
"command": [
"sh",
"-c",
"echo \"sleeping...\"; sleep 10"
],
"image": "busybox",
"imagePullPolicy": "Always",
"name": "init-sleep",
"resources": {
"limits": {
"cpu": "100m",
"memory": "100Mi"
},
"requests": {
"cpu": "100m",
"memory": "100Mi"
}
},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-rr22g",
"readOnly": true
}
]
}
],
"nodeName": "minikube",
"priority": 0,
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"serviceAccount": "default",
"serviceAccountName": "default",
"terminationGracePeriodSeconds": 30,
"tolerations": [
{
"effect": "NoExecute",
"key": "node.kubernetes.io/not-ready",
"operator": "Exists",
"tolerationSeconds": 300
},
{
"effect": "NoExecute",
"key": "node.kubernetes.io/unreachable",
"operator": "Exists",
"tolerationSeconds": 300
}
],
"volumes": [
{
"name": "default-token-rr22g",
"secret": {
"defaultMode": 420,
"secretName": "default-token-rr22g"
}
}
]
},
"status": {
"conditions": [
{
"lastProbeTime": null,
"lastTransitionTime": "2020-01-18T06:31:42Z",
"status": "True",
"type": "Initialized"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2020-01-18T06:31:44Z",
"status": "True",
"type": "Ready"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2020-01-18T06:31:44Z",
"status": "True",
"type": "ContainersReady"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2020-01-18T06:31:29Z",
"status": "True",
"type": "PodScheduled"
}
],
"containerStatuses": [
{
"containerID": "docker://3c4de1de5d3c8f78bcce5f65218d5cbe4ed7b7b86261dd74dcc0f96e832e7db3",
"image": "busybox:latest",
"imageID": "docker-pullable://busybox@sha256:6915be4043561d64e0ab0f8f098dc2ac48e077fe23f488ac24b665166898115a",
"lastState": {},
"name": "busy",
"ready": true,
"restartCount": 0,
"started": true,
"state": {
"running": {
"startedAt": "2020-01-18T06:31:43Z"
}
}
}
],
"hostIP": "192.168.64.6",
"initContainerStatuses": [
{
"containerID": "docker://87f5d5f73827b402263ef77ca72b715c4ad858e7da71abc5655cc049e4c2ae20",
"image": "busybox:latest",
"imageID": "docker-pullable://busybox@sha256:6915be4043561d64e0ab0f8f098dc2ac48e077fe23f488ac24b665166898115a",
"lastState": {},
"name": "init-sleep",
"ready": true,
"restartCount": 0,
"state": {
"terminated": {
"containerID": "docker://87f5d5f73827b402263ef77ca72b715c4ad858e7da71abc5655cc049e4c2ae20",
"exitCode": 0,
"finishedAt": "2020-01-18T06:31:42Z",
"reason": "Completed",
"startedAt": "2020-01-18T06:31:32Z"
}
}
}
],
"phase": "Running",
"podIP": "172.17.0.11",
"podIPs": [
{
"ip": "172.17.0.11"
}
],
"qosClass": "Guaranteed",
"startTime": "2020-01-18T06:31:29Z"
}
}

View File

@ -0,0 +1,19 @@
{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {
"creationTimestamp": "2019-12-31T20:49:23Z",
"name": "default",
"resourceVersion": "146",
"selfLink": "/api/v1/namespaces/default",
"uid": "3da8811c-7632-4a42-b4f5-608c21165ff7"
},
"spec": {
"finalizers": [
"kubernetes"
]
},
"status": {
"phase": "Active"
}
}

View File

@ -0,0 +1,140 @@
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"name\":\"nginx\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"image\":\"nginx:alpine\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}],\"volumeMounts\":[{\"mountPath\":\"/usr/share/nginx/html\",\"name\":\"index\"}]}],\"terminationGracePeriodSeconds\":0,\"volumes\":[{\"name\":\"index\",\"persistentVolumeClaim\":{\"claimName\":\"web\"}}]}}\n"
},
"creationTimestamp": "2019-08-09T05:12:19Z",
"name": "nginx",
"namespace": "default",
"resourceVersion": "1482816",
"selfLink": "/api/v1/namespaces/default/pods/nginx",
"uid": "614908ed-415b-4506-8370-e3e36fa8cc13"
},
"spec": {
"containers": [
{
"image": "nginx:alpine",
"imagePullPolicy": "IfNotPresent",
"name": "nginx",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
],
"resources": {
"limits": {
"memory": "170Mi"
},
"requests": {
"cpu": "100m",
"memory": "70Mi"
}
},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File",
"volumeMounts": [
{
"mountPath": "/usr/share/nginx/html",
"name": "index"
},
{
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-9ph8s",
"readOnly": true
}
]
}
],
"dnsPolicy": "ClusterFirst",
"enableServiceLinks": true,
"nodeName": "minikube",
"priority": 0,
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"serviceAccount": "default",
"serviceAccountName": "default",
"terminationGracePeriodSeconds": 0,
"tolerations": [
{
"effect": "NoExecute",
"key": "node.kubernetes.io/not-ready",
"operator": "Exists",
"tolerationSeconds": 300
},
{
"effect": "NoExecute",
"key": "node.kubernetes.io/unreachable",
"operator": "Exists",
"tolerationSeconds": 300
}
],
"volumes": [
{
"name": "index",
"persistentVolumeClaim": {
"claimName": "web"
}
},
{
"name": "default-token-9ph8s",
"secret": {
"defaultMode": 420,
"secretName": "default-token-9ph8s"
}
}
]
},
"status": {
"conditions": [
{
"lastProbeTime": null,
"lastTransitionTime": "2019-08-09T05:12:19Z",
"status": "True",
"type": "Initialized"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2019-08-09T05:12:21Z",
"status": "True",
"type": "Ready"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2019-08-09T05:12:21Z",
"status": "True",
"type": "ContainersReady"
},
{
"lastProbeTime": null,
"lastTransitionTime": "2019-08-09T05:12:19Z",
"status": "True",
"type": "PodScheduled"
}
],
"containerStatuses": [
{
"containerID": "docker://421bd26d6c682f14b5ea1dcaf06e14a509b2b702fc7793e820520eb1e28e2eaf",
"image": "nginx:alpine",
"imageID": "docker-pullable://nginx@sha256:482ead44b2203fa32b3390abdaf97cbdc8ad15c07fb03a3e68d7c35a19ad7595",
"lastState": {},
"name": "nginx",
"ready": true,
"restartCount": 0,
"state": {
"running": {
"startedAt": "2019-08-09T05:12:20Z"
}
}
}
],
"hostIP": "192.168.64.104",
"phase": "Running",
"podIP": "172.17.0.6",
"qosClass": "BestEffort",
"startTime": "2019-08-09T05:12:19Z"
}
}

View File

@ -0,0 +1,78 @@
{
"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\"}]}]}}}}\n"
},
"creationTimestamp": "2020-01-15T06:48:21Z",
"generation": 1,
"labels": {
"app": "nginx-sts"
},
"name": "nginx-sts",
"namespace": "default",
"resourceVersion": "2946929",
"selfLink": "/apis/apps/v1/namespaces/default/statefulsets/nginx-sts",
"uid": "59c516cb-9fe4-4d7f-b7f4-479928506423"
},
"spec": {
"podManagementPolicy": "OrderedReady",
"replicas": 2,
"revisionHistoryLimit": 10,
"selector": {
"matchLabels": {
"app": "nginx-sts"
}
},
"serviceName": "nginx-sts",
"template": {
"metadata": {
"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"
}
],
"dnsPolicy": "ClusterFirst",
"restartPolicy": "Always",
"schedulerName": "default-scheduler",
"securityContext": {},
"terminationGracePeriodSeconds": 30
}
},
"updateStrategy": {
"rollingUpdate": {
"partition": 0
},
"type": "RollingUpdate"
}
},
"status": {
"collisionCount": 0,
"currentReplicas": 2,
"currentRevision": "nginx-sts-688d57df8f",
"observedGeneration": 1,
"readyReplicas": 2,
"replicas": 2,
"updateRevision": "nginx-sts-688d57df8f",
"updatedReplicas": 2
}
}

View File

@ -0,0 +1,35 @@
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"name\":\"nginx\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"nodePort\":30805,\"port\":8080,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"nginx\"},\"type\":\"NodePort\"}}\n"
},
"creationTimestamp": "2020-01-16T04:18:04Z",
"name": "nginx",
"namespace": "default",
"resourceVersion": "3066081",
"selfLink": "/api/v1/namespaces/default/services/nginx",
"uid": "3dc94561-06ce-4e56-8002-7c4679203d5b"
},
"spec": {
"clusterIP": "10.96.10.89",
"externalTrafficPolicy": "Cluster",
"ports": [
{
"nodePort": 30805,
"port": 8080,
"protocol": "TCP",
"targetPort": 80
}
],
"selector": {
"app": "nginx"
},
"sessionAffinity": "None",
"type": "NodePort"
},
"status": {
"loadBalancer": {}
}
}

369
internal/xray/tree_node.go Normal file
View File

@ -0,0 +1,369 @@
package xray
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
"vbom.ml/util/sortorder"
)
// TreeRef namespaces tree context values.
type TreeRef string
const (
// KeyParent indicates a parent node context key.
KeyParent TreeRef = "parent"
// PathSeparator represents a node path separator.
PathSeparator = "::"
// StatusKey status map key.
StatusKey = "status"
// StateKey state map key.
StateKey = "state"
// OkStatus stands for all is cool.
OkStatus = "ok"
// ToastStatus stands for a resource is not up to snuff
// aka not running or imcomplete.
ToastStatus = "toast"
// CompletedStatus stands for a completed resource.
CompletedStatus = "completed"
// MissingRefStatus stands for a non existing resource reference.
MissingRefStatus = "noref"
)
type Childrens []*TreeNode
// Len returns the list size.
func (c Childrens) Len() int {
return len(c)
}
// Swap swaps list values.
func (c Childrens) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
// Less returns true if i < j.
func (c Childrens) Less(i, j int) bool {
id1, id2 := c[i].ID, c[j].ID
return sortorder.NaturalLess(id1, id2)
}
type TreeNode struct {
GVR, ID string
Children Childrens
Parent *TreeNode
Extras map[string]string
}
func NewTreeNode(gvr, id string) *TreeNode {
return &TreeNode{
GVR: gvr,
ID: id,
Extras: make(map[string]string),
}
}
func (t *TreeNode) Size() int {
return len(t.Children)
}
func count(t *TreeNode, counter int) int {
for _, c := range t.Children {
counter += count(c, counter)
}
return counter
}
func (t *TreeNode) Diff(d *TreeNode) bool {
if t == nil {
return d != nil
}
if t.Size() != d.Size() {
log.Debug().Msgf("SIZE-DIFF")
return true
}
if t.ID != d.ID || t.GVR != d.GVR || !reflect.DeepEqual(t.Extras, d.Extras) {
log.Debug().Msgf("ID DIFF")
return true
}
for i := 0; i < len(t.Children); i++ {
if t.Children[i].Diff(d.Children[i]) {
log.Debug().Msgf("CHILD-DIFF")
return true
}
}
return false
}
func (t *TreeNode) Sort() {
sortChildren(t)
}
func sortChildren(t *TreeNode) {
sort.Sort(t.Children)
for _, c := range t.Children {
sortChildren(c)
}
}
type NodeSpec struct {
GVR, Path string
}
func (t *TreeNode) Spec() NodeSpec {
parent := t
var gvr, path []string
for parent != nil {
gvr = append(gvr, parent.GVR)
path = append(path, parent.ID)
parent = parent.Parent
}
return NodeSpec{
GVR: strings.Join(gvr, PathSeparator),
Path: strings.Join(path, PathSeparator),
}
}
func (t *TreeNode) Flatten() []NodeSpec {
var refs []NodeSpec
for _, c := range t.Children {
if c.IsLeaf() {
refs = append(refs, c.Spec())
continue
}
refs = append(refs, c.Flatten()...)
}
return refs
}
func (t *TreeNode) Blank() bool {
return t.GVR == "" && t.ID == ""
}
func Hydrate(refs []NodeSpec) *TreeNode {
root := NewTreeNode("", "")
nav := root
for _, ref := range refs {
ids := strings.Split(ref.Path, PathSeparator)
gvrs := strings.Split(ref.GVR, PathSeparator)
for i := len(ids) - 1; i >= 0; i-- {
if nav.Blank() {
nav.GVR, nav.ID = gvrs[i], ids[i]
continue
}
c := NewTreeNode(gvrs[i], ids[i])
if n := nav.Find(gvrs[i], ids[i]); n == nil {
nav.Add(c)
nav = c
} else {
nav = n
}
}
nav = root
}
return root
}
func (t *TreeNode) Level() int {
var level int
p := t
for p != nil {
p = p.Parent
level++
}
return level - 1
}
func (t *TreeNode) MaxDepth(depth int) int {
max := depth
for _, c := range t.Children {
m := c.MaxDepth(depth + 1)
if m > max {
max = m
}
}
return max
}
func makeSpacer(d int) string {
return strings.Repeat(" ", d)
}
func (t *TreeNode) Root() *TreeNode {
for p := t; p != nil; p = p.Parent {
if p.Parent == nil {
return p
}
}
return nil
}
func (r *TreeNode) IsLeaf() bool {
return r.Empty()
}
func (r *TreeNode) IsRoot() bool {
return r.Parent == nil
}
func (r *TreeNode) ShallowClone() *TreeNode {
return &TreeNode{GVR: r.GVR, ID: r.ID, Extras: r.Extras}
}
func (r *TreeNode) Filter(q string, filter func(q, path string) bool) *TreeNode {
specs := r.Flatten()
matches := make([]NodeSpec, 0, len(specs))
for _, s := range specs {
if filter(q, s.Path) {
matches = append(matches, s)
}
}
if len(matches) == 0 {
return nil
}
return Hydrate(matches)
}
func (t *TreeNode) Find(gvr, id string) *TreeNode {
if t.GVR == gvr && t.ID == id {
return t
}
for _, c := range t.Children {
if v := c.Find(gvr, id); v != nil {
return v
}
}
return nil
}
func (t *TreeNode) Title() string {
const withNS = "[white::b]%s[-::d]"
title := fmt.Sprintf(withNS, t.colorize())
if t.Size() > 0 {
title += fmt.Sprintf("([white::d]%d[-::d])[-::-]", t.Size())
}
return title
}
func (t *TreeNode) Empty() bool {
return len(t.Children) == 0
}
func (t *TreeNode) Clear() {
t.Children = []*TreeNode{}
}
func (t *TreeNode) Dump() {
dump(t, 0)
}
func dump(n *TreeNode, level int) {
if n == nil {
log.Debug().Msgf("NO DATA!!")
return
}
log.Debug().Msgf("%s%s::%s\n", strings.Repeat(" ", level), n.GVR, n.ID)
for _, c := range n.Children {
dump(c, level+1)
}
}
func (t *TreeNode) DumpStdOut() {
dumpStdOut(t, 0)
}
func dumpStdOut(n *TreeNode, level int) {
if n == nil {
fmt.Println("NO DATA!!")
return
}
fmt.Printf("%s%s::%s\n", strings.Repeat(" ", level), n.GVR, n.ID)
for _, c := range n.Children {
dumpStdOut(c, level+1)
}
}
func (t *TreeNode) Add(c *TreeNode) {
c.Parent = t
t.Children = append(t.Children, c)
}
// Helpers...
func statusEmoji(s string) string {
switch s {
case "ok":
return "[green::b]✔︎"
case "done":
return "[gray::b]🏁"
case "bad":
return "[red::b]𐄂"
default:
return ""
}
}
// 😡👎💥🧨💣🎭 🟥🟩✅✔︎☑️✔️✓
func toEmoji(gvr string) string {
switch gvr {
case "v1/pods":
return "🚛"
case "apps/v1/deployments":
return "🪂"
case "apps/v1/statefulset":
return "🎎"
case "apps/v1/daemonsets":
return "😈"
case "containers":
return "🐳"
case "v1/serviceaccounts":
return "🛎"
case "v1/persistentvolumes":
return "📚"
case "v1/persistentvolumeclaims":
return "🎟"
case "v1/secrets":
return "🔒"
case "v1/configmaps":
return "🗄"
default:
return "📎"
}
}
func (t TreeNode) colorize() string {
const colorFmt = "%s %s [%s::b]%s[::]"
_, n := client.Namespaced(t.ID)
color, flag := "white", "[green::b]OK"
if v, ok := t.Extras[StatusKey]; ok {
switch v {
case ToastStatus:
color, flag = "orangered", "[red::b]TOAST"
case MissingRefStatus:
color, flag = "orange", "[orange::b]MISSING_REF"
}
}
return fmt.Sprintf(colorFmt, toEmoji(t.GVR), flag, color, n)
}

View File

@ -0,0 +1,427 @@
package xray_test
import (
"regexp"
"strings"
"testing"
"github.com/derailed/k9s/internal/xray"
"github.com/stretchr/testify/assert"
)
func TestTreeNodeFilter(t *testing.T) {
uu := map[string]struct {
q string
root, e *xray.TreeNode
}{
"filter_simple": {
root: root1(),
e: diff1(),
q: "c1",
},
"filter_complex": {
root: root2(),
e: diff2(),
q: "c2",
},
"filter_no_match": {
root: root2(),
e: nil,
q: "bozo",
},
"filter_all_match": {
root: root2(),
e: root2(),
q: "",
},
"filter_complex1": {
root: root3(),
e: diff3(),
q: "coredns",
},
}
rx := func(q, path string) bool {
rx := regexp.MustCompile(`(?i)` + q)
tokens := strings.Split(path, "::")
for _, t := range tokens {
if rx.MatchString(t) {
return true
}
}
return false
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
filtered := u.root.Filter(u.q, rx)
assert.Equal(t, u.e, filtered)
})
}
}
func TestTreeNodeHydrate(t *testing.T) {
uu := map[string]struct {
spec []xray.NodeSpec
e *xray.TreeNode
}{
"flat_simple": {
spec: []xray.NodeSpec{
{
GVR: "containers::v1/pods",
Path: "c1::default/p1",
},
{
GVR: "containers::v1/pods",
Path: "c2::default/p1",
},
},
e: root1(),
},
"flat_complex": {
spec: []xray.NodeSpec{
{
GVR: "v1/secrets::containers::v1/pods",
Path: "s1::c1::default/p1",
},
{
GVR: "v1/secrets::containers::v1/pods",
Path: "s2::c2::default/p1",
},
},
e: root2(),
},
"complex1": {
spec: []xray.NodeSpec{
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "default/default-token-rr22g::default/nginx-6b866d578b-c6tcn::default/nginx::-/default::deployments",
},
{
GVR: "v1/configmaps::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/coredns::kube-system/coredns-6955765f44-89q2p::kube-system/coredns::-/kube-system::deployments",
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/coredns-token-5cq9j::kube-system/coredns-6955765f44-89q2p::kube-system/coredns::-/kube-system::deployments",
},
{
GVR: "v1/configmaps::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/coredns::kube-system/coredns-6955765f44-r9j9t::kube-system/coredns::-/kube-system::deployments",
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/coredns-token-5cq9j::kube-system/coredns-6955765f44-r9j9t::kube-system/coredns::-/kube-system::deployments",
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/default-token-thzt8::kube-system/metrics-server-6754dbc9df-88bk4::kube-system/metrics-server::-/kube-system::deployments",
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kube-system/nginx-ingress-token-kff5q::kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55::kube-system/nginx-ingress-controller::-/kube-system::deployments",
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4::kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56::kubernetes-dashboard/dashboard-metrics-scraper::-/kubernetes-dashboard::deployments",
},
{
GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments",
Path: "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4::kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d::kubernetes-dashboard/kubernetes-dashboard::-/kubernetes-dashboard::deployments",
},
},
e: root3(),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
root := xray.Hydrate(u.spec)
assert.Equal(t, u.e.Flatten(), root.Flatten())
})
}
}
func TestTreeNodeFlatten(t *testing.T) {
uu := map[string]struct {
root *xray.TreeNode
e []xray.NodeSpec
}{
"flat_simple": {
root: root1(),
e: []xray.NodeSpec{
{
GVR: "containers::v1/pods",
Path: "c1::default/p1",
},
{
GVR: "containers::v1/pods",
Path: "c2::default/p1",
},
},
},
"flat_complex": {
root: root2(),
e: []xray.NodeSpec{
{
GVR: "v1/secrets::containers::v1/pods",
Path: "s1::c1::default/p1",
},
{
GVR: "v1/secrets::containers::v1/pods",
Path: "s2::c2::default/p1",
},
},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
flat := u.root.Flatten()
assert.Equal(t, u.e, flat)
})
}
}
func TestTreeNodeDiff(t *testing.T) {
uu := map[string]struct {
n1, n2 *xray.TreeNode
e bool
}{
"blank": {
n1: &xray.TreeNode{},
n2: &xray.TreeNode{},
},
"same": {
n1: xray.NewTreeNode("v1/pods", "default/p1"),
n2: xray.NewTreeNode("v1/pods", "default/p1"),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, u.n1.Diff(u.n2))
})
}
}
func TestTreeNodeClone(t *testing.T) {
n := xray.NewTreeNode("v1/pods", "default/p1")
c1 := xray.NewTreeNode("containers", "c1")
n.Add(c1)
c := n.ShallowClone()
assert.Equal(t, n.GVR, c.GVR)
}
func TestTreeNodeRoot(t *testing.T) {
n := xray.NewTreeNode("v1/pods", "default/p1")
c1 := xray.NewTreeNode("containers", "c1")
c2 := xray.NewTreeNode("containers", "c2")
n.Add(c1)
n.Add(c2)
assert.Equal(t, 2, n.Size())
assert.Equal(t, n, n.Root())
assert.True(t, n.IsRoot())
assert.False(t, n.IsLeaf())
assert.Equal(t, n, c1.Root())
assert.False(t, c1.IsRoot())
assert.Equal(t, n, c2.Root())
assert.True(t, c1.IsLeaf())
}
func TestTreeNodeLevel(t *testing.T) {
n := xray.NewTreeNode("v1/pods", "default/p1")
c1 := xray.NewTreeNode("containers", "c1")
c2 := xray.NewTreeNode("containers", "c2")
n.Add(c1)
n.Add(c2)
assert.Equal(t, 0, n.Level())
assert.Equal(t, 1, c1.Level())
assert.Equal(t, 1, c2.Level())
}
func TestTreeNodeMaxDepth(t *testing.T) {
n := xray.NewTreeNode("v1/pods", "default/p1")
c1 := xray.NewTreeNode("containers", "c1")
c2 := xray.NewTreeNode("containers", "c2")
n.Add(c1)
n.Add(c2)
assert.Equal(t, 1, n.MaxDepth(0))
}
// ----------------------------------------------------------------------------
// Helpers...
func root1() *xray.TreeNode {
n := xray.NewTreeNode("v1/pods", "default/p1")
c1 := xray.NewTreeNode("containers", "c1")
c2 := xray.NewTreeNode("containers", "c2")
n.Add(c1)
n.Add(c2)
return n
}
func diff1() *xray.TreeNode {
n := xray.NewTreeNode("v1/pods", "default/p1")
c1 := xray.NewTreeNode("containers", "c1")
n.Add(c1)
return n
}
func root2() *xray.TreeNode {
n := xray.NewTreeNode("v1/pods", "default/p1")
c1 := xray.NewTreeNode("containers", "c1")
c2 := xray.NewTreeNode("containers", "c2")
n.Add(c1)
n.Add(c2)
s1 := xray.NewTreeNode("v1/secrets", "s1")
c1.Add(s1)
s2 := xray.NewTreeNode("v1/secrets", "s2")
c2.Add(s2)
return n
}
func diff2() *xray.TreeNode {
n := xray.NewTreeNode("v1/pods", "default/p1")
c1 := xray.NewTreeNode("containers", "c2")
n.Add(c1)
s1 := xray.NewTreeNode("v1/secrets", "s2")
c1.Add(s1)
return n
}
func root3() *xray.TreeNode {
n := xray.NewTreeNode("apps/v1/deployments", "deployments")
ns1 := xray.NewTreeNode("v1/namespaces", "-/default")
n.Add(ns1)
{
d1 := xray.NewTreeNode("apps/v1/deployments", "default/nginx")
ns1.Add(d1)
{
p1 := xray.NewTreeNode("v1/pods", "default/nginx-6b866d578b-c6tcn")
d1.Add(p1)
{
s1 := xray.NewTreeNode("v1/secrets", "default/default-token-rr22g")
p1.Add(s1)
}
}
}
ns2 := xray.NewTreeNode("v1/namespaces", "-/kube-system")
n.Add(ns2)
{
d2 := xray.NewTreeNode("apps/v1/deployments", "kube-system/coredns")
ns2.Add(d2)
{
p2 := xray.NewTreeNode("v1/pods", "kube-system/coredns-6955765f44-89q2p")
d2.Add(p2)
{
c1 := xray.NewTreeNode("v1/configmaps", "kube-system/coredns")
p2.Add(c1)
s2 := xray.NewTreeNode("v1/secrets", "kube-system/coredns-token-5cq9j")
p2.Add(s2)
}
p3 := xray.NewTreeNode("v1/pods", "kube-system/coredns-6955765f44-r9j9t")
d2.Add(p3)
{
c2 := xray.NewTreeNode("v1/configmaps", "kube-system/coredns")
p3.Add(c2)
s3 := xray.NewTreeNode("v1/secrets", "kube-system/coredns-token-5cq9j")
p3.Add(s3)
}
}
d3 := xray.NewTreeNode("apps/v1/deployments", "kube-system/metrics-server")
ns2.Add(d3)
{
p3 := xray.NewTreeNode("v1/pods", "kube-system/metrics-server-6754dbc9df-88bk4")
d3.Add(p3)
{
s4 := xray.NewTreeNode("v1/secrets", "kube-system/default-token-thzt8")
p3.Add(s4)
}
}
d4 := xray.NewTreeNode("apps/v1/deployments", "kube-system/nginx-ingress-controller")
ns2.Add(d4)
{
p4 := xray.NewTreeNode("v1/pods", "kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55")
d4.Add(p4)
{
s5 := xray.NewTreeNode("v1/secrets", "kube-system/nginx-ingress-token-kff5q")
p4.Add(s5)
}
}
}
ns3 := xray.NewTreeNode("v1/namespaces", "-/kubernetes-dashboard")
n.Add(ns3)
{
d5 := xray.NewTreeNode("apps/v1/deployments", "kubernetes-dashboard/dashboard-metrics-scraper")
ns3.Add(d5)
{
p5 := xray.NewTreeNode("v1/pods", "kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56")
d5.Add(p5)
{
s6 := xray.NewTreeNode("v1/secrets", "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4")
p5.Add(s6)
}
}
d6 := xray.NewTreeNode("apps/v1/deployments", "kubernetes-dashboard/kubernetes-dashboard")
ns3.Add(d6)
{
p6 := xray.NewTreeNode("v1/pods", "kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d")
d6.Add(p6)
{
s6 := xray.NewTreeNode("v1/secrets", "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4")
p6.Add(s6)
}
}
}
return n
}
func diff3() *xray.TreeNode {
n := xray.NewTreeNode("apps/v1/deployments", "deployments")
ns2 := xray.NewTreeNode("v1/namespaces", "-/kube-system")
n.Add(ns2)
{
d2 := xray.NewTreeNode("apps/v1/deployments", "kube-system/coredns")
ns2.Add(d2)
{
p2 := xray.NewTreeNode("v1/pods", "kube-system/coredns-6955765f44-89q2p")
d2.Add(p2)
{
c1 := xray.NewTreeNode("v1/configmaps", "kube-system/coredns")
p2.Add(c1)
s2 := xray.NewTreeNode("v1/secrets", "kube-system/coredns-token-5cq9j")
p2.Add(s2)
}
p3 := xray.NewTreeNode("v1/pods", "kube-system/coredns-6955765f44-r9j9t")
d2.Add(p3)
{
c2 := xray.NewTreeNode("v1/configmaps", "kube-system/coredns")
p3.Add(c2)
s3 := xray.NewTreeNode("v1/secrets", "kube-system/coredns-token-5cq9j")
p3.Add(s3)
}
}
}
return n
}

73
skins/dracula.yml Normal file
View File

@ -0,0 +1,73 @@
foreground: &foreground "#f8f8f2"
background: &background "#282a36"
current_line: &current_line "#44475a"
selection: &selection "#44475a"
comment: &comment "#6272a4"
cyan: &cyan "#8be9fd"
green: &green "#50fa7b"
orange: &orange "#ffb86c"
pink: &pink "#ff79c6"
purple: &purple "#bd93f9"
red: &red "#ff5555"
yellow: &yellow "#f1fa8c"
k9s:
# General K9s styles
body:
fgColor: *foreground
bgColor: *background
logoColor: *purple
# ClusterInfoView styles.
info:
fgColor: *pink
sectionColor: *foreground
frame:
# Borders styles.
border:
fgColor: *selection
focusColor: *current_line
menu:
fgColor: *foreground
keyColor: *pink
# Used for favorite namespaces
numKeyColor: *pink
# CrumbView attributes for history navigation.
crumbs:
fgColor: *foreground
bgColor: *current_line
activeColor: *current_line
# Resource status and update styles
status:
newColor: *cyan
modifyColor: *purple
addColor: *green
errorColor: *red
highlightcolor: *orange
killColor: *comment
completedColor: *comment
# Border title styles.
title:
fgColor: *foreground
bgColor: *current_line
highlightColor: *orange
counterColor: *purple
filterColor: *pink
# TableView attributes.
table:
fgColor: *foreground
bgColor: *background
cursorColor: *current_line
# Header row styles.
header:
fgColor: *foreground
bgColor: *background
sorterColor: *cyan
views:
# YAML info styles.
yaml:
keyColor: *pink
colonColor: *purple
valueColor: *foreground
# Logs styles.
logs:
fgColor: *foreground
bgColor: *background