checkpoint
parent
530a23e28c
commit
860728c083
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = "↑"
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ type Tabular interface {
|
|||
Namespaceable
|
||||
Lister
|
||||
|
||||
SetInstance(string)
|
||||
|
||||
// Empty returns true if model has no data.
|
||||
Empty() bool
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ type ResourceViewer interface {
|
|||
|
||||
// SetBindKeys provision additional key bindings.
|
||||
SetBindKeysFn(BindKeysFunc)
|
||||
SetInstance(string)
|
||||
}
|
||||
|
||||
// LogViewer represents a log viewer.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"},
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
foreground: &foreground "#f8f8f2"
|
||||
background: &background "#282a36"
|
||||
current_line: ¤t_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
|
||||
Loading…
Reference in New Issue