From 4d37d2449c092a110943bffe5c2b9e9796acf4f9 Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 20 Jan 2020 10:16:00 -0700 Subject: [PATCH] remove xray icons. fix #498, #497 --- internal/client/client.go | 2 +- internal/client/gvr.go | 26 +++---- internal/client/gvr_test.go | 10 +-- internal/dao/describe.go | 2 +- internal/dao/generic.go | 3 +- internal/dao/rbac.go | 4 +- internal/dao/registry.go | 71 +++++++++-------- internal/dao/table.go | 10 +-- internal/model/registry.go | 3 +- internal/model/table.go | 4 +- internal/model/tree.go | 10 ++- internal/render/alias.go | 2 +- internal/ui/flash.go | 67 +++++++++------- internal/ui/flash_test.go | 23 +++--- internal/view/app.go | 3 +- internal/view/browser.go | 5 +- internal/view/command.go | 24 +++++- internal/view/node.go | 2 +- internal/view/xray.go | 15 ++-- internal/watch/factory.go | 13 +++- internal/xray/rs.go | 80 ++++++++++++++++++++ internal/xray/rs_test.go | 44 +++++++++++ internal/xray/svc.go | 2 +- internal/xray/test_assets/rs.json | 122 ++++++++++++++++++++++++++++++ internal/xray/tree_node.go | 83 ++++++++++---------- 25 files changed, 465 insertions(+), 165 deletions(-) create mode 100644 internal/xray/rs.go create mode 100644 internal/xray/rs_test.go create mode 100644 internal/xray/test_assets/rs.json diff --git a/internal/client/client.go b/internal/client/client.go index 1c2e5d88..706ff94e 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -64,7 +64,7 @@ func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { ns = "" } spec := NewGVR(gvr) - res := spec.AsGVR() + res := spec.GVR() return &authorizationv1.SelfSubjectAccessReview{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{ ResourceAttributes: &authorizationv1.ResourceAttributes{ diff --git a/internal/client/gvr.go b/internal/client/gvr.go index 85649f7a..a22f7aa1 100644 --- a/internal/client/gvr.go +++ b/internal/client/gvr.go @@ -72,7 +72,7 @@ func (g GVR) String() string { } // AsGV returns the group version scheme representation. -func (g GVR) AsGV() schema.GroupVersion { +func (g GVR) GV() schema.GroupVersion { return schema.GroupVersion{ Group: g.g, Version: g.v, @@ -80,39 +80,39 @@ func (g GVR) AsGV() schema.GroupVersion { } // AsGVR returns a a full schema representation. -func (g GVR) AsGVR() schema.GroupVersionResource { +func (g GVR) GVR() schema.GroupVersionResource { return schema.GroupVersionResource{ - Group: g.ToG(), - Version: g.ToV(), - Resource: g.ToR(), + Group: g.G(), + Version: g.V(), + Resource: g.R(), } } // AsGR returns a a full schema representation. -func (g GVR) AsGR() *schema.GroupResource { +func (g GVR) GR() *schema.GroupResource { return &schema.GroupResource{ - Group: g.ToG(), - Resource: g.ToR(), + Group: g.G(), + Resource: g.R(), } } // ToV returns the resource version. -func (g GVR) ToV() string { +func (g GVR) V() string { return g.v } // ToRAndG returns the resource and group. -func (g GVR) ToRAndG() (string, string) { +func (g GVR) RG() (string, string) { return g.r, g.g } // ToR returns the resource name. -func (g GVR) ToR() string { +func (g GVR) R() string { return g.r } // ToG returns the resource group name. -func (g GVR) ToG() string { +func (g GVR) G() string { return g.g } @@ -131,7 +131,7 @@ func (g GVRs) Swap(i, j int) { // Less returns true if i < j. func (g GVRs) Less(i, j int) bool { - g1, g2 := g[i].ToG(), g[j].ToG() + g1, g2 := g[i].G(), g[j].G() return sortorder.NaturalLess(g1, g2) } diff --git a/internal/client/gvr_test.go b/internal/client/gvr_test.go index a5815809..224ee282 100644 --- a/internal/client/gvr_test.go +++ b/internal/client/gvr_test.go @@ -59,7 +59,7 @@ func TestAsGVR(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).AsGVR()) + assert.Equal(t, u.e, client.NewGVR(u.gvr).GVR()) }) } } @@ -77,7 +77,7 @@ func TestAsGV(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).AsGV()) + assert.Equal(t, u.e, client.NewGVR(u.gvr).GV()) }) } } @@ -132,7 +132,7 @@ func TestToR(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).ToR()) + assert.Equal(t, u.e, client.NewGVR(u.gvr).R()) }) } } @@ -151,7 +151,7 @@ func TestToG(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).ToG()) + assert.Equal(t, u.e, client.NewGVR(u.gvr).G()) }) } } @@ -170,7 +170,7 @@ func TestToV(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).ToV()) + assert.Equal(t, u.e, client.NewGVR(u.gvr).V()) }) } } diff --git a/internal/dao/describe.go b/internal/dao/describe.go index 79bb4046..b7d9fbb7 100644 --- a/internal/dao/describe.go +++ b/internal/dao/describe.go @@ -17,7 +17,7 @@ func Describe(c client.Connection, gvr client.GVR, path string) (string, error) return "", err } - gvk, err := m.KindFor(gvr.AsGVR()) + gvk, err := m.KindFor(gvr.GVR()) if err != nil { log.Error().Err(err).Msgf("No GVK for resource %s", gvr) return "", err diff --git a/internal/dao/generic.go b/internal/dao/generic.go index d67ff92e..c3fa704c 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -26,7 +26,6 @@ type Generic struct { // List returns a collection of resources. // BOZO!! no auth check?? func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) { - log.Debug().Msgf("GENERIC-LIST %q:%q", ns, g.gvr) labelSel, ok := ctx.Value(internal.KeyLabels).(string) if !ok { log.Warn().Msgf("No label selector found in context. Listing all resources") @@ -116,5 +115,5 @@ func (g *Generic) Delete(path string, cascade, force bool) error { } func (g *Generic) dynClient() dynamic.NamespaceableResourceInterface { - return g.Client().DynDialOrDie().Resource(g.gvr.AsGVR()) + return g.Client().DynDialOrDie().Resource(g.gvr.GVR()) } diff --git a/internal/dao/rbac.go b/internal/dao/rbac.go index c0daf77f..238abd32 100644 --- a/internal/dao/rbac.go +++ b/internal/dao/rbac.go @@ -42,7 +42,7 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) { } res := client.NewGVR(gvr) - switch res.ToR() { + switch res.R() { case "clusterrolebindings": return r.loadClusterRoleBinding(path) case "rolebindings": @@ -52,7 +52,7 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) { case "roles": return r.loadRole(path) default: - return nil, fmt.Errorf("expecting clusterrole/role but found %s", res.ToR()) + return nil, fmt.Errorf("expecting clusterrole/role but found %s", res.R()) } } diff --git a/internal/dao/registry.go b/internal/dao/registry.go index 475073ee..52a84ed3 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -3,6 +3,7 @@ package dao import ( "fmt" "sort" + "strings" "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" @@ -113,47 +114,54 @@ func loadNonResource(m ResourceMetas) { func loadK9s(m ResourceMetas) { m[client.NewGVR("xrays")] = metav1.APIResource{ - Name: "xray", - Kind: "XRays", - Categories: []string{"k9s"}, + Name: "xray", + Kind: "XRays", + SingularName: "xray", + Categories: []string{"k9s"}, } m[client.NewGVR("aliases")] = metav1.APIResource{ - Name: "aliases", - Kind: "Aliases", - Categories: []string{"k9s"}, + Name: "aliases", + Kind: "Aliases", + SingularName: "alias", + Categories: []string{"k9s"}, } m[client.NewGVR("contexts")] = metav1.APIResource{ - Name: "contexts", - Kind: "Contexts", - ShortNames: []string{"ctx"}, - Categories: []string{"k9s"}, + Name: "contexts", + Kind: "Contexts", + SingularName: "context", + ShortNames: []string{"ctx"}, + Categories: []string{"k9s"}, } m[client.NewGVR("screendumps")] = metav1.APIResource{ - Name: "screendumps", - Kind: "ScreenDumps", - ShortNames: []string{"sd"}, - Verbs: []string{"delete"}, - Categories: []string{"k9s"}, + Name: "screendumps", + Kind: "ScreenDumps", + SingularName: "screendump", + ShortNames: []string{"sd"}, + Verbs: []string{"delete"}, + Categories: []string{"k9s"}, } m[client.NewGVR("benchmarks")] = metav1.APIResource{ - Name: "benchmarks", - Kind: "Benchmarks", - ShortNames: []string{"be"}, - Verbs: []string{"delete"}, - Categories: []string{"k9s"}, + Name: "benchmarks", + Kind: "Benchmarks", + SingularName: "benchmark", + ShortNames: []string{"be"}, + Verbs: []string{"delete"}, + Categories: []string{"k9s"}, } m[client.NewGVR("portforwards")] = metav1.APIResource{ - Name: "portforwards", - Namespaced: true, - Kind: "PortForwards", - ShortNames: []string{"pf"}, - Verbs: []string{"delete"}, - Categories: []string{"k9s"}, + Name: "portforwards", + Namespaced: true, + Kind: "PortForwards", + SingularName: "portforward", + ShortNames: []string{"pf"}, + Verbs: []string{"delete"}, + Categories: []string{"k9s"}, } m[client.NewGVR("containers")] = metav1.APIResource{ - Name: "containers", - Kind: "Containers", - Categories: []string{"k9s"}, + Name: "containers", + Kind: "Containers", + SingularName: "container", + Categories: []string{"k9s"}, } } @@ -199,7 +207,10 @@ func loadPreferred(f Factory, m ResourceMetas) error { for _, r := range rr { for _, res := range r.APIResources { gvr := client.FromGVAndR(r.GroupVersion, res.Name) - res.Group, res.Version = gvr.ToG(), gvr.ToV() + res.Group, res.Version = gvr.G(), gvr.V() + if res.SingularName == "" { + res.SingularName = strings.ToLower(res.Kind) + } m[gvr] = res } } diff --git a/internal/dao/table.go b/internal/dao/table.go index 11c4c6fb..3abdf94e 100644 --- a/internal/dao/table.go +++ b/internal/dao/table.go @@ -34,7 +34,7 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { SetHeader("Accept", a). Namespace(ns). Name(n). - Resource(t.gvr.ToR()). + Resource(t.gvr.R()). VersionedParams(&metav1beta1.TableOptions{}, codec). Do().Get() if err != nil { @@ -57,7 +57,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { o, err := c.Get(). SetHeader("Accept", a). Namespace(ns). - Resource(t.gvr.ToR()). + Resource(t.gvr.R()). VersionedParams(&metav1beta1.TableOptions{}, codec). Do().Get() if err != nil { @@ -74,10 +74,10 @@ const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" func (t *Table) getClient() (*rest.RESTClient, error) { crConfig := t.Client().RestConfigOrDie() - gv := t.gvr.AsGV() + gv := t.gvr.GV() crConfig.GroupVersion = &gv crConfig.APIPath = "/apis" - if t.gvr.ToG() == "" { + if t.gvr.G() == "" { crConfig.APIPath = "/api" } codec, _ := t.codec() @@ -92,7 +92,7 @@ func (t *Table) getClient() (*rest.RESTClient, error) { func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) { scheme := runtime.NewScheme() - gv := t.gvr.AsGV() + gv := t.gvr.GV() metav1.AddToGroupVersion(scheme, gv) scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) diff --git a/internal/model/registry.go b/internal/model/registry.go index 4f041f21..717f8bec 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -97,7 +97,8 @@ var Registry = map[string]ResourceMeta{ TreeRenderer: &xray.Deployment{}, }, "apps/v1/replicasets": { - Renderer: &render.ReplicaSet{}, + Renderer: &render.ReplicaSet{}, + TreeRenderer: &xray.ReplicaSet{}, }, "apps/v1/statefulsets": { DAO: &dao.StatefulSet{}, diff --git a/internal/model/table.go b/internal/model/table.go index 0f869f40..e8526670 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -15,7 +15,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -const iniRefreshRate = 300 * time.Millisecond +const initRefreshRate = 300 * time.Millisecond // TableListener represents a table model listener. type TableListener interface { @@ -176,7 +176,7 @@ func (t *Table) Peek() render.TableData { func (t *Table) updater(ctx context.Context) { defer log.Debug().Msgf("Model canceled -- %q", t.gvr) - rate := iniRefreshRate + rate := initRefreshRate for { select { case <-ctx.Done(): diff --git a/internal/model/tree.go b/internal/model/tree.go index 91254834..55dc4284 100644 --- a/internal/model/tree.go +++ b/internal/model/tree.go @@ -18,6 +18,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +const initTreeRefreshRate = 500 * time.Millisecond + // TreeListener represents a tree model listener. type TreeListener interface { // TreeChanged notifies the model data changed. @@ -159,7 +161,7 @@ func (t *Tree) ToYAML(ctx context.Context, gvr, path string) (string, error) { func (t *Tree) updater(ctx context.Context) { defer log.Debug().Msgf("Tree-model canceled -- %q", t.gvr) - rate := iniRefreshRate + rate := initTreeRefreshRate for { select { case <-ctx.Done(): @@ -204,7 +206,8 @@ func (t *Tree) reconcile(ctx context.Context) error { } ns := client.CleanseNamespace(t.namespace) - root := xray.NewTreeNode("root", client.NewGVR(t.gvr).ToR()) + res := client.NewGVR(t.gvr).R() + root := xray.NewTreeNode(res, res) ctx = context.WithValue(ctx, xray.KeyParent, root) if _, ok := meta.TreeRenderer.(*xray.Generic); ok { table, ok := oo[0].(*metav1beta1.Table) @@ -287,6 +290,9 @@ func rxFilter(q, path string) bool { } func treeHydrate(ctx context.Context, ns string, oo []runtime.Object, re TreeRenderer) error { + if re == nil { + return fmt.Errorf("no tree renderer defined for this resource") + } for _, o := range oo { if err := re.Render(ctx, ns, o); err != nil { return err diff --git a/internal/render/alias.go b/internal/render/alias.go index 54960111..ddbe0fa9 100644 --- a/internal/render/alias.go +++ b/internal/render/alias.go @@ -39,7 +39,7 @@ func (Alias) Render(o interface{}, ns string, r *Row) error { r.ID = a.GVR gvr := client.NewGVR(a.GVR) - res, grp := gvr.ToRAndG() + res, grp := gvr.RG() r.Fields = append(r.Fields, res, strings.Join(a.Aliases, ","), diff --git a/internal/ui/flash.go b/internal/ui/flash.go index 43f091d0..d8b08484 100644 --- a/internal/ui/flash.go +++ b/internal/ui/flash.go @@ -23,7 +23,7 @@ const ( // FlashFatal represents an fatal message. FlashFatal - flashDelay = 3 + flashDelay = 3 * time.Second emoDoh = "😗" emoRed = "😡" @@ -39,23 +39,32 @@ type ( Flash struct { *tview.TextView - cancel context.CancelFunc - app *App + cancel context.CancelFunc + app *App + flushNow bool } ) // NewFlash returns a new flash view. func NewFlash(app *App, m string) *Flash { - f := Flash{app: app, TextView: tview.NewTextView()} + f := Flash{ + app: app, + TextView: tview.NewTextView(), + } f.SetTextColor(tcell.ColorAqua) f.SetTextAlign(tview.AlignLeft) f.SetBorderPadding(0, 0, 1, 1) - f.SetText("") + f.SetText(m) f.app.Styles.AddListener(&f) return &f } +// TestMode for testing... +func (f *Flash) TestMode() { + f.flushNow = true +} + // StylesChanged notifies listener the skin changed. func (f *Flash) StylesChanged(s *config.Styles) { f.SetBackgroundColor(s.BgColor()) @@ -64,6 +73,7 @@ func (f *Flash) StylesChanged(s *config.Styles) { // Info displays an info flash message. func (f *Flash) Info(msg string) { + log.Info().Msg(msg) f.SetMessage(FlashInfo, msg) } @@ -74,6 +84,7 @@ func (f *Flash) Infof(fmat string, args ...interface{}) { // Warn displays a warning flash message. func (f *Flash) Warn(msg string) { + log.Warn().Msg(msg) f.SetMessage(FlashWarn, msg) } @@ -84,7 +95,7 @@ func (f *Flash) Warnf(fmat string, args ...interface{}) { // Err displays an error flash message. func (f *Flash) Err(err error) { - log.Error().Err(err).Msgf("%v", err) + log.Error().Msg(err.Error()) f.SetMessage(FlashErr, err.Error()) } @@ -106,35 +117,35 @@ func (f *Flash) SetMessage(level FlashLevel, msg ...string) { if f.cancel != nil { f.cancel() } - var ctx1, ctx2 context.Context - { - var timerCancel context.CancelFunc - ctx1, f.cancel = context.WithCancel(context.TODO()) - ctx2, timerCancel = context.WithTimeout(context.TODO(), flashDelay*time.Second) - go f.refresh(ctx1, ctx2, timerCancel) - } + _, _, width, _ := f.GetRect() if width <= 15 { width = 100 } m := strings.Join(msg, " ") - f.SetTextColor(flashColor(level)) - f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3)) + if f.flushNow { + f.SetTextColor(flashColor(level)) + f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3)) + } else { + f.app.QueueUpdateDraw(func() { + log.Debug().Msgf("FLASH %q", m) + f.SetTextColor(flashColor(level)) + f.SetText(render.Truncate(flashEmoji(level)+" "+m, width-3)) + }) + } + + var ctx context.Context + ctx, f.cancel = context.WithCancel(context.TODO()) + ctx, f.cancel = context.WithTimeout(ctx, flashDelay) + go f.refresh(ctx) } -func (f *Flash) refresh(ctx1, ctx2 context.Context, cancel context.CancelFunc) { - defer cancel() - for { - select { - case <-ctx1.Done(): - return - case <-ctx2.Done(): - f.app.QueueUpdateDraw(func() { - f.Clear() - }) - return - } - } +func (f *Flash) refresh(ctx context.Context) { + <-ctx.Done() + f.app.QueueUpdateDraw(func() { + log.Debug().Msgf("FLASH-CLEAR %q", f.GetText(true)) + f.Clear() + }) } func flashEmoji(l FlashLevel) string { diff --git a/internal/ui/flash_test.go b/internal/ui/flash_test.go index 7f3f022e..c158f166 100644 --- a/internal/ui/flash_test.go +++ b/internal/ui/flash_test.go @@ -9,32 +9,37 @@ import ( ) func TestFlashInfo(t *testing.T) { - f := ui.NewFlash(ui.NewApp(""), "YO!") - + f := newFlash() f.Info("Blee") - assert.Equal(t, "😎 Blee\n", f.GetText(false)) + assert.Equal(t, "😎 Blee\n", f.GetText(false)) f.Infof("Blee %s", "duh") assert.Equal(t, "😎 Blee duh\n", f.GetText(false)) } func TestFlashWarn(t *testing.T) { - f := ui.NewFlash(ui.NewApp(""), "YO!") - + f := newFlash() f.Warn("Blee") - assert.Equal(t, "😗 Blee\n", f.GetText(false)) + assert.Equal(t, "😗 Blee\n", f.GetText(false)) f.Warnf("Blee %s", "duh") assert.Equal(t, "😗 Blee duh\n", f.GetText(false)) } func TestFlashErr(t *testing.T) { - f := ui.NewFlash(ui.NewApp(""), "YO!") + f := newFlash() f.Err(errors.New("Blee")) assert.Equal(t, "😡 Blee\n", f.GetText(false)) - f.Errf("Blee %s", "duh") assert.Equal(t, "😡 Blee duh\n", f.GetText(false)) - +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func newFlash() *ui.Flash { + f := ui.NewFlash(ui.NewApp(""), "YO!") + f.TestMode() + return f } diff --git a/internal/view/app.go b/internal/view/app.go index 29ca0ed8..63c6d8e1 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -314,10 +314,11 @@ func (a *App) Status(l ui.FlashLevel, msg string) { a.Draw() } -// ClearStatus reset log back to normal. +// ClearStatus reset logo back to normal. func (a *App) ClearStatus(flash bool) { a.Logo().Reset() if flash { + log.Debug().Msgf("FLASH CLEARED!!") a.Flash().Clear() } a.Draw() diff --git a/internal/view/browser.go b/internal/view/browser.go index 9ead47b9..1ea8611f 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -92,7 +92,6 @@ func (b *Browser) SetInstance(path string) { func (b *Browser) Start() { b.Stop() - b.App().Status(ui.FlashInfo, "Loading...") b.Table.Start() ctx := b.defaultContext() ctx, b.cancelFn = context.WithCancel(ctx) @@ -141,7 +140,7 @@ func (b *Browser) TableDataChanged(data render.TableData) { b.app.QueueUpdateDraw(func() { b.refreshActions() b.Update(data) - b.App().ClearStatus(true) + b.App().ClearStatus(false) }) } @@ -244,7 +243,7 @@ func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { b.Stop() defer b.Start() { - msg := fmt.Sprintf("Delete %s %s?", b.gvr.ToR(), selections[0]) + msg := fmt.Sprintf("Delete %s %s?", b.gvr.R(), selections[0]) if len(selections) > 1 { msg = fmt.Sprintf("Delete %d marked %s?", len(selections), b.gvr) } diff --git a/internal/view/command.go b/internal/view/command.go index c09dd10f..9ef4662c 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -43,13 +43,32 @@ func (c *Command) Init() error { return nil } +func allowedXRay(gvr client.GVR) bool { + gg := []string{ + "v1/pods", + "v1/services", + "apps/v1/deployments", + "apps/v1/daemonsets", + "apps/v1/statefulsets", + "apps/v1/replicasets", + } + + for _, g := range gg { + if g == gvr.String() { + return true + } + } + + return false +} + func (c *Command) xrayCmd(cmd string) error { 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 { + if !ok || !allowedXRay(gvr) { return fmt.Errorf("Huh? `%s` Command not found", cmd) } return c.exec(cmd, "xrays", NewXray(gvr), true) @@ -172,8 +191,7 @@ func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) e if comp == nil { return fmt.Errorf("No component given for %s", gvr) } - - c.app.Flash().Infof("Running command %s", cmd) + c.app.Flash().Infof("Viewing %s...", client.NewGVR(gvr).R()) c.app.Config.SetActiveView(cmd) if err := c.app.Config.Save(); err != nil { log.Error().Err(err).Msg("Config save failed!") diff --git a/internal/view/node.go b/internal/view/node.go index 78d07152..96643d48 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -65,7 +65,7 @@ func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey { } sel := n.GetTable().GetSelectedItem() - gvr := client.NewGVR(n.GVR()).AsGVR() + gvr := client.NewGVR(n.GVR()).GVR() o, err := n.App().factory.Client().DynDialOrDie().Resource(gvr).Get(sel, metav1.GetOptions{}) if err != nil { n.App().Flash().Errf("Unable to get resource %q -- %s", n.GVR(), err) diff --git a/internal/view/xray.go b/internal/view/xray.go index 2c1e7acf..9db79abd 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -77,9 +77,9 @@ func (x *Xray) Init(ctx context.Context) error { 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(fmt.Sprintf(" %s-%s ", xrayTitle, strings.Title(x.gvr.ToR()))) + x.SetTitle(fmt.Sprintf(" %s-%s ", xrayTitle, strings.Title(x.gvr.R()))) x.SetGraphics(true) - x.SetGraphicsColor(tcell.ColorDimGray) + x.SetGraphicsColor(tcell.ColorFloralWhite) x.SetInputCapture(x.keyboard) x.model.SetRefreshRate(time.Duration(x.app.Config.K9s.GetRefreshRate()) * time.Second) @@ -370,7 +370,7 @@ func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey { ns, n := client.Namespaced(ref.Path) args := make([]string, 0, 10) args = append(args, "edit") - args = append(args, client.NewGVR(ref.GVR).ToR()) + args = append(args, client.NewGVR(ref.GVR).R()) args = append(args, "-n", ns) args = append(args, "--context", x.app.Config.K9s.CurrentContext) if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { @@ -450,7 +450,7 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if len(strings.Split(ref.Path, "/")) == 1 { return nil } - if err := x.app.viewResource(client.NewGVR(ref.GVR).ToR(), ref.Path, false); err != nil { + if err := x.app.viewResource(client.NewGVR(ref.GVR).R(), ref.Path, false); err != nil { x.app.Flash().Err(err) } @@ -632,11 +632,14 @@ func (x *Xray) App() *App { // UpdateTitle updates the view title. func (x *Xray) UpdateTitle() { - x.SetTitle(x.styleTitle()) + t := x.styleTitle() + x.app.QueueUpdateDraw(func() { + x.SetTitle(t) + }) } func (x *Xray) styleTitle() string { - base := fmt.Sprintf("%s-%s", xrayTitle, strings.Title(x.gvr.ToR())) + base := fmt.Sprintf("%s-%s", xrayTitle, strings.Title(x.gvr.R())) ns := x.model.GetNamespace() if client.IsAllNamespaces(ns) { ns = client.NamespaceAll diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 8ad48ebc..ce6d7586 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -38,6 +38,9 @@ func NewFactory(client client.Connection) *Factory { // Start initializes the informers until caller cancels the context. func (f *Factory) Start(ns string) { + f.mx.Lock() + defer f.mx.Unlock() + log.Debug().Msgf("Factory START with ns `%q", ns) f.stopChan = make(chan struct{}) for ns, fac := range f.factories { @@ -48,13 +51,13 @@ func (f *Factory) Start(ns string) { // Terminate terminates all watchers and forwards. func (f *Factory) Terminate() { + f.mx.Lock() + defer f.mx.Unlock() + if f.stopChan != nil { close(f.stopChan) f.stopChan = nil } - - f.mx.Lock() - defer f.mx.Unlock() for k := range f.factories { delete(f.factories, k) } @@ -179,6 +182,9 @@ func (f *Factory) ForResource(ns, gvr string) informers.GenericInformer { log.Error().Err(fmt.Errorf("MEOW! No informer for %q:%q", ns, gvr)) return inf } + + f.mx.RLock() + defer f.mx.RUnlock() fact.Start(f.stopChan) return inf @@ -193,7 +199,6 @@ func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory { if fac, ok := f.factories[ns]; ok { return fac } - log.Debug().Msgf("FACTORY_CREATE for ns %q", ns) f.factories[ns] = di.NewFilteredDynamicSharedInformerFactory( f.client.DynDialOrDie(), defaultResync, diff --git a/internal/xray/rs.go b/internal/xray/rs.go new file mode 100644 index 00000000..f920b3fe --- /dev/null +++ b/internal/xray/rs.go @@ -0,0 +1,80 @@ +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" +) + +// ReplicaSet represents an xray renderer. +type ReplicaSet struct{} + +// Render renders an xray node. +func (r *ReplicaSet) 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 rs appsv1.ReplicaSet + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rs) + 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)) + } + + root := NewTreeNode("apps/v1/replicasets", client.FQN(rs.Namespace, rs.Name)) + oo, err := locatePods(ctx, rs.Namespace, rs.Spec.Selector) + if err != nil { + return err + } + + ctx = context.WithValue(ctx, KeyParent, root) + var re Pod + for _, o := range oo { + p, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("expecting *Unstructured but got %T", o) + } + if err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil { + return err + } + } + + if root.IsLeaf() { + return nil + } + + gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, rs.Namespace) + nsn := parent.Find(gvr, nsID) + if nsn == nil { + nsn = NewTreeNode(gvr, nsID) + parent.Add(nsn) + } + nsn.Add(root) + + return r.validate(root, rs) +} + +func (*ReplicaSet) validate(root *TreeNode, rs appsv1.ReplicaSet) error { + root.Extras[StatusKey] = OkStatus + var r int32 + if rs.Spec.Replicas != nil { + r = int32(*rs.Spec.Replicas) + } + a := rs.Status.Replicas + if a != r { + root.Extras[StatusKey] = ToastStatus + } + root.Extras[InfoKey] = fmt.Sprintf("%d/%d", a, r) + + return nil +} diff --git a/internal/xray/rs_test.go b/internal/xray/rs_test.go new file mode 100644 index 00000000..fab86f43 --- /dev/null +++ b/internal/xray/rs_test.go @@ -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" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestReplicaSetRender(t *testing.T) { + uu := map[string]struct { + file string + level1, level2 int + status string + }{ + "plain": { + file: "rs", + level1: 1, + level2: 1, + status: xray.OkStatus, + }, + } + + var re xray.ReplicaSet + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + f := makeFactory() + f.rows = map[string][]runtime.Object{"v1/pods": {load(t, "po")}} + + o := load(t, u.file) + root := xray.NewTreeNode("replicasets", "replicasets") + ctx := context.WithValue(context.Background(), xray.KeyParent, root) + ctx = context.WithValue(ctx, internal.KeyFactory, f) + + assert.Nil(t, re.Render(ctx, "", o)) + assert.Equal(t, u.level1, root.CountChildren()) + assert.Equal(t, u.level2, root.Children[0].CountChildren()) + }) + } +} diff --git a/internal/xray/svc.go b/internal/xray/svc.go index a64f01a2..7084d7e8 100644 --- a/internal/xray/svc.go +++ b/internal/xray/svc.go @@ -37,7 +37,7 @@ func (s *Service) Render(ctx context.Context, ns string, o interface{}) error { return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent)) } - root := NewTreeNode("apps/v1/services", client.FQN(svc.Namespace, svc.Name)) + root := NewTreeNode("v1/services", client.FQN(svc.Namespace, svc.Name)) oo, err := s.locatePods(ctx, svc.Namespace, svc.Spec.Selector) if err != nil { return err diff --git a/internal/xray/test_assets/rs.json b/internal/xray/test_assets/rs.json new file mode 100644 index 00000000..0b5f56ce --- /dev/null +++ b/internal/xray/test_assets/rs.json @@ -0,0 +1,122 @@ +{ + "apiVersion": "apps/v1", + "kind": "ReplicaSet", + "metadata": { + "annotations": { + "deployment.kubernetes.io/desired-replicas": "1", + "deployment.kubernetes.io/max-replicas": "2", + "deployment.kubernetes.io/revision": "2" + }, + "creationTimestamp": "2020-01-20T01:34:11Z", + "generation": 1, + "labels": { + "app": "nginx-pv", + "pod-template-hash": "6476d7d5c8" + }, + "name": "nginx-pv-6476d7d5c8", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Deployment", + "name": "nginx-pv", + "uid": "68aa70ff-ff7c-4a67-8d4f-fc31ef27ec35" + } + ], + "resourceVersion": "3743997", + "selfLink": "/apis/apps/v1/namespaces/default/replicasets/nginx-pv-6476d7d5c8", + "uid": "547a036d-94d9-4818-bd9e-ec2939019471" + }, + "spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "app": "nginx-pv", + "pod-template-hash": "6476d7d5c8" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "nginx-pv", + "pod-template-hash": "6476d7d5c8" + } + }, + "spec": { + "automountServiceAccountToken": true, + "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", + "volumeMounts": [ + { + "mountPath": "/usr/share/nginx/html", + "name": "index" + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "zorg", + "serviceAccountName": "zorg", + "terminationGracePeriodSeconds": 30, + "volumes": [ + { + "name": "index", + "persistentVolumeClaim": { + "claimName": "web" + } + } + ] + } + } + }, + "status": { + "availableReplicas": 1, + "fullyLabeledReplicas": 1, + "observedGeneration": 1, + "readyReplicas": 1, + "replicas": 1 + } +} diff --git a/internal/xray/tree_node.go b/internal/xray/tree_node.go index 65b7a1a4..88021b8d 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" "vbom.ml/util/sortorder" ) @@ -295,15 +296,7 @@ func (t *TreeNode) Find(gvr, id string) *TreeNode { // Title computes the node title. func (t *TreeNode) Title() string { - const withNS = "[white::b]%s[-::d]" - - title := fmt.Sprintf(withNS, t.AsString()) - - if t.CountChildren() > 0 { - title += fmt.Sprintf("([white::d]%d[-::d])[-::-]", t.CountChildren()) - } - - return title + return t.toTitle() } // ---------------------------------------------------------------------------- @@ -341,53 +334,55 @@ func dumpStdOut(n *TreeNode, level int) { } } -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 category(gvr string) string { + meta, err := dao.MetaFor(client.NewGVR(gvr)) + if err != nil { + return "" } + + return meta.SingularName } -// AsString transforms a node as a string for viewing. -func (t TreeNode) AsString() string { - const colorFmt = "%s [gray::-][%s[gray::-]] [%s::b]%s[::]" +const ( + titleFmt = " [gray::-]%s/[white::b][%s::b]%s[::]" + topTitleFmt = " [white::b][%s::b]%s[::]" + toast = "TOAST" +) +func (t TreeNode) toTitle() (title string) { _, n := client.Namespaced(t.ID) - color, flag := "white", "[green::b]OK" + color, status := "white", "OK" if v, ok := t.Extras[StatusKey]; ok { switch v { case ToastStatus: - color, flag = "orangered", "[red::b]TOAST" + color, status = "orangered", toast case MissingRefStatus: - color, flag = "orange", "[orange::b]TOAST_REF" + color, status = "orange", toast+"_REF" } } - str := fmt.Sprintf(colorFmt, toEmoji(t.GVR), flag, color, n) - i, ok := t.Extras[InfoKey] - if !ok { - return str + defer func() { + if status != "OK" { + title += fmt.Sprintf(" [gray::-][[%s::b]%s[gray::-]]", color, status) + } + }() + + categ := category(t.GVR) + if categ == "" { + title = fmt.Sprintf(topTitleFmt, color, n) + } else { + title = fmt.Sprintf(titleFmt, categ, color, n) } - return fmt.Sprintf("%s [antiquewhite::][%s][::] ", str, i) + if !t.IsLeaf() { + title += fmt.Sprintf("[white::d](%d[-::d])[-::-]", t.CountChildren()) + } + + info, ok := t.Extras[InfoKey] + if !ok { + return + } + + title += fmt.Sprintf(" [antiquewhite::][%s][::]", info) + return }