From d45d3af116979106657a500f8e79e48a89b5e22e Mon Sep 17 00:00:00 2001 From: derailed Date: Sat, 18 Jan 2020 18:45:17 -0700 Subject: [PATCH] checkpoint --- .goreleaser.yml | 35 +--- README.md | 7 + internal/client/gvr.go | 3 + internal/model/log.go | 2 +- internal/model/log_test.go | 15 +- internal/model/table.go | 2 +- internal/model/tree.go | 56 ++++-- internal/view/actions.go | 5 +- internal/view/command.go | 22 -- internal/view/help.go | 4 +- internal/view/xray.go | 342 ++++++++++++++++++++++++++++---- internal/xray/container.go | 26 ++- internal/xray/container_test.go | 8 +- internal/xray/dp.go | 24 ++- internal/xray/dp_test.go | 10 +- internal/xray/ds.go | 37 ++-- internal/xray/ds_test.go | 9 +- internal/xray/generic.go | 34 +--- internal/xray/generic_test.go | 2 +- internal/xray/ns.go | 11 +- internal/xray/ns_test.go | 2 +- internal/xray/pod.go | 39 ++-- internal/xray/pod_test.go | 153 +------------- internal/xray/sts.go | 56 +++--- internal/xray/sts_test.go | 10 +- internal/xray/svc.go | 25 ++- internal/xray/svc_test.go | 10 +- internal/xray/tree_node.go | 156 ++++++++------- internal/xray/tree_node_test.go | 113 +++++++---- 29 files changed, 699 insertions(+), 519 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 7e9f2b13..ffb43d7c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -4,7 +4,7 @@ before: - go mod download - go generate ./... release: - prerelease: true + prerelease: false builds: - env: - CGO_ENABLED=0 @@ -53,38 +53,7 @@ brews: name: derailed email: fernand@imhotep.io folder: Formula - homepage: https://k9ss.io + homepage: https://k8sk9s.dev/ description: Kubernetes CLI To Manage Your Clusters In Style! test: | system "k9s version" - -# Snapcraft -# snapcraft: -# name: k9s -# summary: K9s is a CLI to view and manage your Kubernetes clusters. -# description: | -# K9s is a CLI to view and manage your Kubernetes clusters. -# By leveraging a terminal UI, you can easily traverse Kubernetes resources -# and view the state of you clusters in a single powerful session. -# name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" -# publish: true -# replacements: -# amd64: 64-bit -# 386: 32-bit -# darwin: macOS -# linux: Tux -# bit: Arm -# bitv6: Arm6 -# bitv7: Arm7 -# # grade: devel -# # confinement: devmode -# grade: stable -# confinement: strict -# apps: -# k9s: -# plugs: ["home", "network", "kube-config"] -# plugs: -# kube-config: -# interface: personal-files -# read: -# - $HOME/.kube \ No newline at end of file diff --git a/README.md b/README.md index ed67f4bd..27119886 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ for changes and offers subsequent commands to interact with observed Kubernetes --- +## Slack Channel + +Wanna discuss K9s features with your fellow `K9sers` or simply show your support for this tool? +Please Dial [K9s Slack](https://k9sers.slack.com/) + +--- + ## Installation K9s is available on Linux, OSX and Windows platforms. diff --git a/internal/client/gvr.go b/internal/client/gvr.go index df07c6bd..85649f7a 100644 --- a/internal/client/gvr.go +++ b/internal/client/gvr.go @@ -140,6 +140,9 @@ func (g GVRs) Less(i, j int) bool { // Can determines the available actions for a given resource. func Can(verbs []string, v string) bool { + if verbs == nil { + return false + } if len(verbs) == 0 { return true } diff --git a/internal/model/log.go b/internal/model/log.go index a52d5abe..f7354aaf 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -208,7 +208,6 @@ func (l *Log) updateLogs(ctx context.Context, c <-chan string) { // AddListener adds a new model listener. func (l *Log) AddListener(listener LogsListener) { l.listeners = append(l.listeners, listener) - l.fireLogChanged(l.lines) } // RemoveListener delete a listener from the lisl. @@ -265,6 +264,7 @@ func (l *Log) fireLogError(err error) { } func (l *Log) fireLogChanged(lines []string) { + log.Debug().Msgf("FIRE LOGS CHANGED %v", lines) for _, lis := range l.listeners { lis.LogChanged(lines) } diff --git a/internal/model/log_test.go b/internal/model/log_test.go index 410cd4b6..bb3fd6cc 100644 --- a/internal/model/log_test.go +++ b/internal/model/log_test.go @@ -31,7 +31,7 @@ func TestLogFullBuffer(t *testing.T) { } m.Notify(false) - assert.Equal(t, 2, v.dataCalled) + assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, data[4:], v.data) @@ -74,13 +74,13 @@ func TestLogFilter(t *testing.T) { } m.Notify(true) - assert.Equal(t, 3, v.dataCalled) + assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, u.e, len(v.data)) m.ClearFilter() - assert.Equal(t, 4, v.dataCalled) + assert.Equal(t, 3, v.dataCalled) assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, size, len(v.data)) @@ -103,7 +103,7 @@ func TestLogStartStop(t *testing.T) { m.Notify(true) m.Stop() - assert.Equal(t, 2, v.dataCalled) + assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, 2, len(v.data)) @@ -125,7 +125,7 @@ func TestLogClear(t *testing.T) { m.Notify(true) m.Clear() - assert.Equal(t, 2, v.dataCalled) + assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, 0, len(v.data)) @@ -141,7 +141,7 @@ func TestLogBasic(t *testing.T) { data := []string{"line1", "line2"} m.Set(data) - assert.Equal(t, 2, v.dataCalled) + assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 0, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, data, v.data) @@ -153,6 +153,7 @@ func TestLogAppend(t *testing.T) { v := newTestView() m.AddListener(v) + m.Set([]string{"blah blah"}) assert.Equal(t, []string{"blah blah"}, v.data) data := []string{"line1", "line2"} @@ -182,7 +183,7 @@ func TestLogTimedout(t *testing.T) { m.Append(d) } m.Notify(true) - assert.Equal(t, 3, v.dataCalled) + assert.Equal(t, 2, v.dataCalled) assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, []string{"line1"}, v.data) diff --git a/internal/model/table.go b/internal/model/table.go index 96081082..b39fe0c4 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -86,7 +86,7 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { return meta.DAO.Get(ctx, path) } -// Delete removes a resource. +// Delete deletes a resource. func (t *Table) Delete(ctx context.Context, path string, cascade, force bool) error { meta, err := t.getMeta(ctx) if err != nil { diff --git a/internal/model/tree.go b/internal/model/tree.go index b3453c93..033ea477 100644 --- a/internal/model/tree.go +++ b/internal/model/tree.go @@ -116,7 +116,7 @@ func (t *Tree) InNamespace(ns string) bool { // Empty return true if no model data. func (t *Tree) Empty() bool { - return t.root.Empty() + return t.root.IsLeaf() } // Peek returns model data. @@ -124,6 +124,36 @@ func (t *Tree) Peek() *xray.TreeNode { return t.root } +// Describe describes a given resource. +func (t *Tree) Describe(ctx context.Context, gvr, path string) (string, error) { + meta, err := t.getMeta(ctx, gvr) + if err != nil { + return "", err + } + + desc, ok := meta.DAO.(dao.Describer) + if !ok { + return "", fmt.Errorf("no describer for %q", meta.DAO.GVR()) + } + + return desc.Describe(path) +} + +// ToYAML returns a resource yaml. +func (t *Tree) ToYAML(ctx context.Context, gvr, path string) (string, error) { + meta, err := t.getMeta(ctx, gvr) + if err != nil { + return "", err + } + + desc, ok := meta.DAO.(dao.Describer) + if !ok { + return "", fmt.Errorf("no describer for %q", meta.DAO.GVR()) + } + + return desc.ToYAML(path) +} + func (t *Tree) updater(ctx context.Context) { defer log.Debug().Msgf("Model canceled -- %q", t.gvr) @@ -181,7 +211,7 @@ func (t *Tree) reconcile(ctx context.Context) error { log.Debug().Msgf(" TREE returned %d rows", len(oo)) ns := client.CleanseNamespace(t.namespace) - root := xray.NewTreeNode(t.gvr, client.NewGVR(t.gvr).ToR()) + root := xray.NewTreeNode("root", 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) @@ -212,17 +242,6 @@ func (t *Tree) reconcile(ctx context.Context) error { 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 { @@ -251,6 +270,17 @@ func (t *Tree) fireTreeLoadFailed(err error) { } } +func (t *Tree) getMeta(ctx context.Context, gvr string) (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(gvr)) + + return meta, nil +} + // ---------------------------------------------------------------------------- // Helpers... diff --git a/internal/view/actions.go b/internal/view/actions.go index 2c578b7d..c371f79f 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -52,18 +52,19 @@ func inScope(scopes, aliases []string) bool { func hotKeyActions(r Runner, aa ui.KeyActions) { hh := config.NewHotKeys() if err := hh.Load(); err != nil { + log.Error().Err(err).Msgf("Loading HOTKEYS") return } for k, hk := range hh.HotKey { key, err := asKey(hk.ShortCut) if err != nil { - log.Error().Err(err).Msg("Unable to map hotkey shortcut to a key") + log.Error().Err(err).Msg("HOT-KEY Unable to map hotkey shortcut to a key") continue } _, ok := aa[key] if ok { - log.Error().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") + log.Error().Err(fmt.Errorf("HOT-KEY Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut") continue } aa[key] = ui.NewSharedKeyAction( diff --git a/internal/view/command.go b/internal/view/command.go index e3033583..49d24c66 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -44,16 +44,6 @@ func (c *Command) Init() error { } 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") @@ -63,18 +53,6 @@ func (c *Command) xrayCmd(cmd string) error { 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. diff --git a/internal/view/help.go b/internal/view/help.go index 33aad1ec..59629ab7 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -65,7 +65,6 @@ func (v *Help) bindKeys() { } func (v *Help) computeMaxes(hh model.MenuHints) { - v.maxKey, v.maxDesc = 0, 0 for _, h := range hh { if len(h.Mnemonic) > v.maxKey { v.maxKey = len(h.Mnemonic) @@ -80,6 +79,7 @@ func (v *Help) computeMaxes(hh model.MenuHints) { func (v *Help) build() { v.Clear() + v.maxRows = len(v.showGeneral()) ff := []HelpFunc{v.app.Content.Top().Hints, v.showGeneral, v.showNav, v.showHelp} var col int for i, section := range []string{"RESOURCE", "GENERAL", "NAVIGATION", "HELP"} { @@ -197,7 +197,7 @@ func (v *Help) showGeneral() model.MenuHints { Description: "Clear command", }, { - Mnemonic: "h", + Mnemonic: "Ctrl-h", Description: "Toggle Header", }, { diff --git a/internal/view/xray.go b/internal/view/xray.go index d308b19a..5bb19a22 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -2,6 +2,7 @@ package view import ( "context" + "errors" "fmt" "regexp" "strings" @@ -10,13 +11,16 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/k9s/internal/xray" "github.com/derailed/tview" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" "github.com/sahilm/fuzzy" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const xrayTitle = "Xray" @@ -33,6 +37,9 @@ type Xray struct { cancelFn context.CancelFunc cmdBuff *ui.CmdBuff expandNodes bool + meta metav1.APIResource + count int + envFn EnvFunc } var _ ResourceViewer = (*Xray)(nil) @@ -40,6 +47,7 @@ var _ ResourceViewer = (*Xray)(nil) // NewXray returns a new view. func NewXray(gvr client.GVR) ResourceViewer { a := Xray{ + gvr: gvr, TreeView: tview.NewTreeView(), model: model.NewTree(gvr.String()), expandNodes: true, @@ -53,6 +61,11 @@ func NewXray(gvr client.GVR) ResourceViewer { // Init initializes the view func (x *Xray) Init(ctx context.Context) error { var err error + x.meta, err = dao.MetaFor(x.gvr) + if err != nil { + return err + } + if x.app, err = extractApp(ctx); err != nil { return err } @@ -64,7 +77,7 @@ 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(" Xray ") + x.SetTitle(fmt.Sprintf(" %s-%s ", xrayTitle, strings.Title(x.gvr.ToR()))) x.SetGraphics(true) x.SetGraphicsColor(tcell.ColorDimGray) x.SetInputCapture(x.keyboard) @@ -80,7 +93,9 @@ func (x *Xray) Init(ctx context.Context) error { return } x.selectedNode = ref.Path + x.refreshActions() }) + x.refreshActions() return nil } @@ -101,9 +116,7 @@ func (x *Xray) Hints() model.MenuHints { 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.KeyX: ui.NewKeyAction("Expand/Collapse All", x.toggleCollapseCmd, 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), @@ -134,6 +147,244 @@ func (x *Xray) keyboard(evt *tcell.EventKey) *tcell.EventKey { return evt } +func (x *Xray) refreshActions() { + aa := make(ui.KeyActions) + + defer func() { + pluginActions(x, aa) + hotKeyActions(x, aa) + + x.actions.Add(aa) + x.app.Menu().HydrateMenu(x.Hints()) + }() + + x.actions.Clear() + x.bindKeys() + + ref := x.selectedSpec() + if ref == nil { + return + } + + var err error + x.meta, err = dao.MetaFor(client.NewGVR(ref.GVR)) + if err != nil { + log.Warn().Msgf("NO meta for %q -- %s", ref.GVR, err) + return + } + + if client.Can(x.meta.Verbs, "edit") { + aa[ui.KeyE] = ui.NewKeyAction("Edit", x.editCmd, true) + } + if client.Can(x.meta.Verbs, "delete") { + aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", x.deleteCmd, true) + } + if client.Can(x.meta.Verbs, "view") { + aa[tcell.KeyEnter] = ui.NewKeyAction("Goto", x.gotoCmd, true) + } + if !dao.IsK9sMeta(x.meta) { + aa[ui.KeyY] = ui.NewKeyAction("YAML", x.viewCmd, true) + aa[ui.KeyD] = ui.NewKeyAction("Describe", x.describeCmd, true) + } + + if ref.GVR == "containers" { + aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true) + aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true) + aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true) + } + + x.actions.Add(aa) +} + +func (x *Xray) GetSelectedItem() string { + ref := x.selectedSpec() + if ref == nil { + return "" + } + return ref.Path +} + +// EnvFn returns an plugin env function if available. +func (x *Xray) EnvFn() EnvFunc { + return x.envFn +} + +// Aliases returns all available aliases. +func (x *Xray) Aliases() []string { + return append(x.meta.ShortNames, x.meta.SingularName, x.meta.Name) +} + +func (x *Xray) selectedSpec() *xray.NodeSpec { + node := x.GetCurrentNode() + if node == nil { + return nil + } + + ref, ok := node.GetReference().(xray.NodeSpec) + if !ok { + log.Error().Msgf("Expecting a NodeSpec!") + return nil + } + + return &ref +} + +func (x *Xray) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + ref := x.selectedSpec() + if ref == nil { + return nil + } + + if ref.Parent != nil { + x.showLogs(ref.Parent, ref, prev) + } else { + log.Error().Msgf("No parent found for container %q", ref.Path) + } + + return nil + } +} + +func (x *Xray) showLogs(pod, co *xray.NodeSpec, prev bool) { + log.Debug().Msgf("SHOWING LOGS path %q", co.Path) + // Need to load and wait for pods + ns, _ := client.Namespaced(pod.Path) + _, err := x.app.factory.CanForResource(ns, "v1/pods", client.MonitorAccess) + if err != nil { + x.app.Flash().Err(err) + return + } + + if err := x.app.inject(NewLog(client.NewGVR(co.GVR), pod.Path, co.Path, prev)); err != nil { + x.app.Flash().Err(err) + } +} + +func (x *Xray) shellCmd(evt *tcell.EventKey) *tcell.EventKey { + ref := x.selectedSpec() + if ref == nil { + return nil + } + + log.Debug().Msgf("STATUS %q", ref.Status) + if ref.Status != "" { + x.app.Flash().Errf("%s is not in a running state", ref.Path) + return nil + } + + if ref.Parent != nil { + x.shellIn(ref.Parent.Path, ref.Path) + } else { + log.Error().Msgf("No parent found on container node %q", ref.Path) + } + + return nil +} + +func (x *Xray) shellIn(path, co string) { + x.Stop() + shellIn(x.app, path, co) + x.Start() +} + +func (x *Xray) viewCmd(evt *tcell.EventKey) *tcell.EventKey { + ref := x.selectedSpec() + if ref == nil { + return evt + } + + ctx := x.defaultContext() + raw, err := x.model.ToYAML(ctx, ref.GVR, ref.Path) + if err != nil { + x.App().Flash().Errf("unable to get resource %q -- %s", ref.GVR, err) + return nil + } + + details := NewDetails(x.app, "YAML", ref.Path).Update(raw) + if err := x.app.inject(details); err != nil { + x.app.Flash().Err(err) + } + + return nil + +} + +func (x *Xray) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { + ref := x.selectedSpec() + if ref == nil { + return evt + } + + x.Stop() + defer x.Start() + { + gvr := client.NewGVR(ref.GVR) + meta, err := dao.MetaFor(gvr) + if err != nil { + log.Warn().Msgf("NO meta for %q -- %s", ref.GVR, err) + return nil + } + x.resourceDelete(gvr, ref, fmt.Sprintf("Delete %s %s?", meta.SingularName, ref.Path)) + } + + return nil + +} + +func (x *Xray) describeCmd(evt *tcell.EventKey) *tcell.EventKey { + ref := x.selectedSpec() + if ref == nil { + return evt + } + + x.describe(ref.GVR, ref.Path) + + return nil +} + +func (x *Xray) describe(gvr, path string) { + ctx := context.Background() + ctx = context.WithValue(ctx, internal.KeyFactory, x.app.factory) + + yaml, err := x.model.Describe(ctx, gvr, path) + if err != nil { + x.app.Flash().Errf("Describe command failed: %s", err) + return + } + + details := NewDetails(x.app, "Describe", path).Update(yaml) + if err := x.app.inject(details); err != nil { + x.app.Flash().Err(err) + } +} + +func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey { + ref := x.selectedSpec() + if ref == nil { + return evt + } + + x.Stop() + defer x.Start() + { + 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, "-n", ns) + args = append(args, "--context", x.app.Config.K9s.CurrentContext) + if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { + args = append(args, "--kubeconfig", *cfg) + } + if !runK(true, x.app, append(args, n)...) { + x.app.Flash().Err(errors.New("Edit exec failed")) + } + } + + return evt +} + func (x *Xray) noopCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } @@ -168,19 +419,6 @@ func (x *Xray) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { 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() @@ -199,8 +437,9 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if x.cmdBuff.IsActive() { if ui.IsLabelSelector(x.cmdBuff.String()) { x.Start() - return nil } + x.cmdBuff.SetActive(false) + return nil } n := x.GetCurrentNode() if n == nil { @@ -287,7 +526,11 @@ func (x *Xray) update(node *xray.TreeNode) { x.app.QueueUpdateDraw(func() { x.SetRoot(root) root.Walk(func(node, parent *tview.TreeNode) bool { - ref := node.GetReference().(xray.NodeSpec) + ref, ok := node.GetReference().(xray.NodeSpec) + if !ok { + log.Error().Msgf("Expeting a NodeSpec but got %T", node.GetReference()) + return false + } // BOZO!! Figure this out expand/collapse but the root if parent != nil { node.SetExpanded(x.expandNodes) @@ -295,11 +538,6 @@ func (x *Xray) update(node *xray.TreeNode) { 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) @@ -312,6 +550,7 @@ func (x *Xray) update(node *xray.TreeNode) { // XrayDataChanged notifies the model data changed. func (x *Xray) TreeChanged(node *xray.TreeNode) { log.Debug().Msgf("Tree Changed %d", len(node.Children)) + x.count = node.Count(x.gvr.String()) x.update(x.filter(node)) x.UpdateTitle() } @@ -358,7 +597,7 @@ func (x *Xray) Start() { log.Debug().Msgf("XRAY STARTING! -- %q", x.selectedNode) x.cmdBuff.AddListener(x.app.Cmd()) x.cmdBuff.AddListener(x) - x.app.SetFocus(x) + // x.app.SetFocus(x) ctx := x.defaultContext() ctx, x.cancelFn = context.WithCancel(ctx) @@ -405,12 +644,7 @@ func (x *Xray) UpdateTitle() { } func (x *Xray) styleTitle() string { - rc := x.GetRowCount() - if rc > 0 { - rc-- - } - - base := strings.Title(xrayTitle) + base := fmt.Sprintf("%s-%s", xrayTitle, strings.Title(x.gvr.ToR())) ns := x.model.GetNamespace() if client.IsAllNamespaces(ns) { ns = client.NamespaceAll @@ -419,9 +653,9 @@ func (x *Xray) styleTitle() string { buff := x.cmdBuff.String() var title string if ns == client.ClusterScope { - title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, rc), x.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, x.count), x.app.Styles.Frame()) } else { - title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, rc), x.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, x.count), x.app.Styles.Frame()) } if buff == "" { return title @@ -434,6 +668,30 @@ func (x *Xray) styleTitle() string { return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), x.app.Styles.Frame()) } +func (x *Xray) resourceDelete(gvr client.GVR, ref *xray.NodeSpec, msg string) { + dialog.ShowDelete(x.app.Content.Pages, msg, func(cascade, force bool) { + x.app.Flash().Infof("Delete resource %s %s", ref.GVR, ref.Path) + accessor, err := dao.AccessorFor(x.app.factory, gvr) + if err != nil { + log.Error().Err(err).Msgf("No accessor") + return + } + + nuker, ok := accessor.(dao.Nuker) + if !ok { + x.app.Flash().Errf("Invalid nuker %T", accessor) + return + } + if err := nuker.Delete(ref.Path, true, true); err != nil { + x.app.Flash().Errf("Delete failed with `%s", err) + } else { + x.app.Flash().Infof("%s `%s deleted successfully", x.GVR(), ref.Path) + x.app.factory.DeleteForwarder(ref.Path) + } + x.Refresh() + }, func() {}) +} + // ---------------------------------------------------------------------------- // Helpers... @@ -448,23 +706,19 @@ func mapKey(evt *tcell.EventKey) tcell.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 + return len(mm) > 0 } 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 } @@ -472,7 +726,15 @@ func makeTreeNode(node *xray.TreeNode, expanded bool, styles *config.Styles) *tv n := tview.NewTreeNode("No data...") if node != nil { n.SetText(node.Title()) - n.SetReference(xray.NodeSpec{GVR: node.GVR, Path: node.ID}) + spec := xray.NodeSpec{} + if p := node.Parent; p != nil { + spec.GVR, spec.Path = p.GVR, p.ID + } + n.SetReference(xray.NodeSpec{ + GVR: node.GVR, + Path: node.ID, + Parent: &spec, + }) } n.SetSelectable(true) n.SetExpanded(expanded) diff --git a/internal/xray/container.go b/internal/xray/container.go index bcdeef99..75b59f39 100644 --- a/internal/xray/container.go +++ b/internal/xray/container.go @@ -27,15 +27,16 @@ func (c *Container) Render(ctx context.Context, ns string, o interface{}) error } root := NewTreeNode("containers", client.FQN(ns, co.Container.Name)) - parent := ctx.Value(KeyParent).(*TreeNode) + parent, ok := 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() { + if !root.IsLeaf() { parent.Add(root) } + return nil } @@ -51,11 +52,11 @@ func (c *Container) envRefs(f dao.Factory, parent *TreeNode, ns string, co *v1.C 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) + 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) + addRef(f, parent, gvr, id, e.SecretRef.Optional) } } } @@ -65,7 +66,7 @@ func (c *Container) secretRefs(f dao.Factory, parent *TreeNode, ns string, ref * return } gvr, id := "v1/secrets", client.FQN(ns, ref.LocalObjectReference.Name) - c.addRef(f, parent, id, gvr, ref.Optional) + addRef(f, parent, id, gvr, ref.Optional) } func (c *Container) configMapRefs(f dao.Factory, parent *TreeNode, ns string, ref *v1.ConfigMapKeySelector) { @@ -73,10 +74,13 @@ func (c *Container) configMapRefs(f dao.Factory, parent *TreeNode, ns string, re return } gvr, id := "v1/configmaps", client.FQN(ns, ref.LocalObjectReference.Name) - c.addRef(f, parent, gvr, id, ref.Optional) + addRef(f, parent, gvr, id, ref.Optional) } -func (c *Container) addRef(f dao.Factory, parent *TreeNode, gvr, id string, optional *bool) { +// ---------------------------------------------------------------------------- +// Helpers... + +func 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) @@ -84,13 +88,7 @@ func (c *Container) addRef(f dao.Factory, parent *TreeNode, gvr, id string, opti } } -// Helpers... - -func validate(f dao.Factory, n *TreeNode, optional *bool) { - if optional == nil || *optional { - n.Extras[StatusKey] = OkStatus - return - } +func validate(f dao.Factory, n *TreeNode, _ *bool) { 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) diff --git a/internal/xray/container_test.go b/internal/xray/container_test.go index 3688df03..05649427 100644 --- a/internal/xray/container_test.go +++ b/internal/xray/container_test.go @@ -54,7 +54,7 @@ func TestCORefs(t *testing.T) { co: render.ContainerRes{Container: makeCMContainer("c1", true)}, level1: 1, level2: 1, - e: xray.OkStatus, + e: xray.MissingRefStatus, }, "cm_doubleRef": { co: render.ContainerRes{Container: makeDoubleCMKeysContainer("c1", false)}, @@ -72,7 +72,7 @@ func TestCORefs(t *testing.T) { co: render.ContainerRes{Container: makeSecContainer("c1", true)}, level1: 1, level2: 1, - e: xray.OkStatus, + e: xray.MissingRefStatus, }, "envFrom_optional": { co: render.ContainerRes{Container: makeCMEnvFromContainer("c1", false)}, @@ -91,8 +91,8 @@ func TestCORefs(t *testing.T) { 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.level1, root.CountChildren()) + assert.Equal(t, u.level2, root.Children[0].CountChildren()) assert.Equal(t, u.e, root.Children[0].Children[0].Extras[xray.StatusKey]) }) } diff --git a/internal/xray/dp.go b/internal/xray/dp.go index 530a0ee3..4df4a210 100644 --- a/internal/xray/dp.go +++ b/internal/xray/dp.go @@ -34,15 +34,7 @@ func (d *Deployment) Render(ctx context.Context, ns string, o interface{}) error 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 @@ -50,12 +42,26 @@ func (d *Deployment) Render(ctx context.Context, ns string, o interface{}) error ctx = context.WithValue(ctx, KeyParent, root) var re Pod for _, o := range oo { - p := o.(*unstructured.Unstructured) + 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, dp.Namespace) + nsn := parent.Find(gvr, nsID) + if nsn == nil { + nsn = NewTreeNode(gvr, nsID) + parent.Add(nsn) + } + nsn.Add(root) + return d.validate(root, dp) } diff --git a/internal/xray/dp_test.go b/internal/xray/dp_test.go index fc31f920..06509a0b 100644 --- a/internal/xray/dp_test.go +++ b/internal/xray/dp_test.go @@ -7,6 +7,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime" ) func TestDeployRender(t *testing.T) { @@ -25,16 +26,19 @@ func TestDeployRender(t *testing.T) { var re xray.Deployment for k := range uu { + f := makeFactory() + f.rows = []runtime.Object{load(t, "po")} + 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()) + ctx = context.WithValue(ctx, internal.KeyFactory, f) assert.Nil(t, re.Render(ctx, "", o)) - assert.Equal(t, u.level1, root.Size()) - assert.Equal(t, u.level2, root.Children[0].Size()) + assert.Equal(t, u.level1, root.CountChildren()) + assert.Equal(t, u.level2, root.Children[0].CountChildren()) }) } } diff --git a/internal/xray/ds.go b/internal/xray/ds.go index 5f42baf3..e3ffb45d 100644 --- a/internal/xray/ds.go +++ b/internal/xray/ds.go @@ -29,29 +29,34 @@ func (d *DaemonSet) Render(ctx context.Context, ns string, o interface{}) error return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent)) } - nsID, gvr := client.FQN(client.ClusterScope, ds.Namespace), "v1/namespaces" + root := NewTreeNode("apps/v1/daemonsets", client.FQN(ds.Namespace, ds.Name)) + 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, 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, ds.Namespace) 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) } diff --git a/internal/xray/ds_test.go b/internal/xray/ds_test.go index 877f3dd0..f4ee8a97 100644 --- a/internal/xray/ds_test.go +++ b/internal/xray/ds_test.go @@ -7,6 +7,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime" ) func TestDaemonSetRender(t *testing.T) { @@ -25,16 +26,18 @@ func TestDaemonSetRender(t *testing.T) { var re xray.DaemonSet for k := range uu { + f := makeFactory() + f.rows = []runtime.Object{load(t, "po")} 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()) + ctx = context.WithValue(ctx, internal.KeyFactory, f) assert.Nil(t, re.Render(ctx, "", o)) - assert.Equal(t, u.level1, root.Size()) - assert.Equal(t, u.level2, root.Children[0].Size()) + assert.Equal(t, u.level1, root.CountChildren()) + assert.Equal(t, u.level2, root.Children[0].CountChildren()) }) } } diff --git a/internal/xray/generic.go b/internal/xray/generic.go index 35bde59e..8287ce7d 100644 --- a/internal/xray/generic.go +++ b/internal/xray/generic.go @@ -2,8 +2,6 @@ package xray import ( "context" - "encoding/json" - "errors" "fmt" "github.com/derailed/k9s/internal/client" @@ -33,35 +31,11 @@ func (g *Generic) Render(ctx context.Context, ns string, o interface{}) error { } root := NewTreeNode("generic", client.FQN(ns, n)) - parent := ctx.Value(KeyParent).(*TreeNode) + parent, ok := ctx.Value(KeyParent).(*TreeNode) + if !ok { + return fmt.Errorf("expecting TreeNode but got %T", ctx.Value(KeyParent)) + } 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 -} diff --git a/internal/xray/generic_test.go b/internal/xray/generic_test.go index a37330ec..24175b9a 100644 --- a/internal/xray/generic_test.go +++ b/internal/xray/generic_test.go @@ -30,7 +30,7 @@ func TestGenericRender(t *testing.T) { ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory()) assert.Nil(t, re.Render(ctx, "", makeTable())) - assert.Equal(t, u.level1, root.Size()) + assert.Equal(t, u.level1, root.CountChildren()) }) } } diff --git a/internal/xray/ns.go b/internal/xray/ns.go index 3a76d94f..a8fc2e3d 100644 --- a/internal/xray/ns.go +++ b/internal/xray/ns.go @@ -12,7 +12,7 @@ import ( type Namespace struct{} -func (p *Namespace) Render(ctx context.Context, ns string, o interface{}) error { +func (n *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) @@ -31,5 +31,14 @@ func (p *Namespace) Render(ctx context.Context, ns string, o interface{}) error } parent.Add(root) + return n.validate(root, nss) +} + +func (*Namespace) validate(root *TreeNode, ns v1.Namespace) error { + root.Extras[StatusKey] = OkStatus + if ns.Status.Phase == v1.NamespaceTerminating { + root.Extras[StatusKey] = ToastStatus + } + return nil } diff --git a/internal/xray/ns_test.go b/internal/xray/ns_test.go index 216eb1a1..c8fac72a 100644 --- a/internal/xray/ns_test.go +++ b/internal/xray/ns_test.go @@ -32,7 +32,7 @@ func TestNamespaceRender(t *testing.T) { 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.level1, root.CountChildren()) }) } } diff --git a/internal/xray/pod.go b/internal/xray/pod.go index 2e9dcb69..b5c3532e 100644 --- a/internal/xray/pod.go +++ b/internal/xray/pod.go @@ -5,7 +5,9 @@ import ( "fmt" "strconv" + "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/runtime" @@ -30,6 +32,11 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { return err } + f, ok := ctx.Value(internal.KeyFactory).(dao.Factory) + if !ok { + return fmt.Errorf("no factory found in context") + } + phase := p.phase(&po) ss := po.Status.ContainerStatuses cr, _, _ := p.statuses(ss) @@ -41,16 +48,16 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { 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)) + node := NewTreeNode("v1/pods", client.FQN(po.Namespace, po.Name)) + node.Extras[StatusKey] = status + node.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) + parent.Add(node) - ctx = context.WithValue(ctx, KeyParent, root) + ctx = context.WithValue(ctx, KeyParent, node) 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 { @@ -62,22 +69,28 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { return err } } - p.podVolumeRefs(root, po.Namespace, po.Spec.Volumes) + p.podVolumeRefs(f, node, po.Namespace, po.Spec.Volumes) return nil } -func (*Pod) podVolumeRefs(parent *TreeNode, ns string, vv []v1.Volume) { +func (*Pod) podVolumeRefs(f dao.Factory, 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))) + sec := v.VolumeSource.Secret + if sec != nil { + addRef(f, parent, "v1/secrets", client.FQN(ns, sec.SecretName), nil) continue } - cmv := v.VolumeSource.ConfigMap - if cmv != nil { - parent.Add(NewTreeNode("v1/configmaps", client.FQN(ns, cmv.LocalObjectReference.Name))) + cm := v.VolumeSource.ConfigMap + if cm != nil { + addRef(f, parent, "v1/configmaps", client.FQN(ns, cm.LocalObjectReference.Name), nil) + continue + } + + pvc := v.VolumeSource.PersistentVolumeClaim + if pvc != nil { + addRef(f, parent, "v1/persistentvolumeclaims", client.FQN(ns, pvc.ClaimName), nil) } } } diff --git a/internal/xray/pod_test.go b/internal/xray/pod_test.go index dba40d9b..f1442292 100644 --- a/internal/xray/pod_test.go +++ b/internal/xray/pod_test.go @@ -8,8 +8,6 @@ import ( "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) { @@ -21,7 +19,7 @@ func TestPodRender(t *testing.T) { "plain": { file: "po", level1: 1, - level2: 1, + level2: 2, status: xray.OkStatus, }, "withInit": { @@ -42,153 +40,8 @@ func TestPodRender(t *testing.T) { 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()) + assert.Equal(t, u.level1, root.CountChildren()) + assert.Equal(t, u.level2, root.Children[0].CountChildren()) }) } } - -// ---------------------------------------------------------------------------- -// 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 -} diff --git a/internal/xray/sts.go b/internal/xray/sts.go index 41c4db95..c0dd40ee 100644 --- a/internal/xray/sts.go +++ b/internal/xray/sts.go @@ -4,26 +4,20 @@ 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 { +func (s *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 { @@ -35,31 +29,8 @@ func (p *StatefulSet) Render(ctx context.Context, ns string, o interface{}) erro 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()) + root := NewTreeNode("apps/v1/statefulsets", client.FQN(sts.Namespace, sts.Name)) + oo, err := locatePods(ctx, sts.Namespace, sts.Spec.Selector) if err != nil { return err } @@ -67,12 +38,31 @@ func (p *StatefulSet) Render(ctx context.Context, ns string, o interface{}) erro ctx = context.WithValue(ctx, KeyParent, root) var re Pod for _, o := range oo { - p := o.(*unstructured.Unstructured) + 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, sts.Namespace) + nsn := parent.Find(gvr, nsID) + if nsn == nil { + nsn = NewTreeNode(gvr, nsID) + parent.Add(nsn) + } + nsn.Add(root) + + return s.validate(root, sts) +} + +func (*StatefulSet) validate(root *TreeNode, sts appsv1.StatefulSet) error { root.Extras[StatusKey] = OkStatus var r int32 if sts.Spec.Replicas != nil { diff --git a/internal/xray/sts_test.go b/internal/xray/sts_test.go index 2685913f..a3fc2252 100644 --- a/internal/xray/sts_test.go +++ b/internal/xray/sts_test.go @@ -7,6 +7,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime" ) func TestStatefulSetRender(t *testing.T) { @@ -27,14 +28,17 @@ func TestStatefulSetRender(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { + f := makeFactory() + f.rows = []runtime.Object{load(t, "po")} + 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()) + ctx = context.WithValue(ctx, internal.KeyFactory, f) assert.Nil(t, re.Render(ctx, "", o)) - assert.Equal(t, u.level1, root.Size()) - assert.Equal(t, u.level2, root.Children[0].Size()) + 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 02324271..11cfe209 100644 --- a/internal/xray/svc.go +++ b/internal/xray/svc.go @@ -35,30 +35,35 @@ func (s *Service) Render(ctx context.Context, ns string, o interface{}) error { 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) + 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 } } root.Extras[StatusKey] = OkStatus + if root.IsLeaf() { + return nil + } + gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, svc.Namespace) + nsn := parent.Find(gvr, nsID) + if nsn == nil { + nsn = NewTreeNode(gvr, nsID) + parent.Add(nsn) + } + nsn.Add(root) + return nil } diff --git a/internal/xray/svc_test.go b/internal/xray/svc_test.go index dec8de1e..45d251d3 100644 --- a/internal/xray/svc_test.go +++ b/internal/xray/svc_test.go @@ -7,6 +7,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/xray" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/runtime" ) func TestServiceRender(t *testing.T) { @@ -25,16 +26,19 @@ func TestServiceRender(t *testing.T) { var re xray.Service for k := range uu { + f := makeFactory() + f.rows = []runtime.Object{load(t, "po")} + 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()) + ctx = context.WithValue(ctx, internal.KeyFactory, f) assert.Nil(t, re.Render(ctx, "", o)) - assert.Equal(t, u.level1, root.Size()) - assert.Equal(t, u.level2, root.Children[0].Size()) + assert.Equal(t, u.level1, root.CountChildren()) + assert.Equal(t, u.level2, root.Children[0].CountChildren()) }) } } diff --git a/internal/xray/tree_node.go b/internal/xray/tree_node.go index 8612b26d..1b66aa37 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -11,14 +11,11 @@ import ( "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 represents a node path separatot. PathSeparator = "::" // StatusKey status map key. @@ -41,6 +38,22 @@ const ( MissingRefStatus = "noref" ) +// ---------------------------------------------------------------------------- + +// TreeRef namespaces tree context values. +type TreeRef string + +// ---------------------------------------------------------------------------- + +// NodeSpec represents a node resource specification. +type NodeSpec struct { + GVR, Path, Status string + Parent *NodeSpec +} + +// ---------------------------------------------------------------------------- + +// Childrens represents a collection of children nodes. type Childrens []*TreeNode // Len returns the list size. @@ -60,6 +73,9 @@ func (c Childrens) Less(i, j int) bool { return sortorder.NaturalLess(id1, id2) } +// ---------------------------------------------------------------------------- + +// TreeNode represents a resource tree node. type TreeNode struct { GVR, ID string Children Childrens @@ -67,31 +83,39 @@ type TreeNode struct { Extras map[string]string } +// NewTreeNode returns a new instance. func NewTreeNode(gvr, id string) *TreeNode { return &TreeNode{ GVR: gvr, ID: id, - Extras: make(map[string]string), + Extras: map[string]string{StatusKey: OkStatus}, } } -func (t *TreeNode) Size() int { +// CountChildren returns the children count. +func (t *TreeNode) CountChildren() int { return len(t.Children) } -func count(t *TreeNode, counter int) int { +// Count all the nodes from this node +func (t *TreeNode) Count(gvr string) int { + counter := 0 + if t.GVR == gvr || gvr == "" { + counter++ + } for _, c := range t.Children { - counter += count(c, counter) + counter += c.Count(gvr) } return counter } +// Diff computes a tree diff. func (t *TreeNode) Diff(d *TreeNode) bool { if t == nil { return d != nil } - if t.Size() != d.Size() { + if t.CountChildren() != d.CountChildren() { log.Debug().Msgf("SIZE-DIFF") return true } @@ -109,36 +133,33 @@ func (t *TreeNode) Diff(d *TreeNode) bool { return false } +// Sort sorts the tree nodes. func (t *TreeNode) Sort() { - sortChildren(t) -} - -func sortChildren(t *TreeNode) { sort.Sort(t.Children) for _, c := range t.Children { - sortChildren(c) + c.Sort() } } -type NodeSpec struct { - GVR, Path string -} - +// Spec returns this node specification. func (t *TreeNode) Spec() NodeSpec { parent := t - var gvr, path []string + var gvr, path, status []string for parent != nil { gvr = append(gvr, parent.GVR) path = append(path, parent.ID) + status = append(status, parent.Extras[StatusKey]) parent = parent.Parent } return NodeSpec{ - GVR: strings.Join(gvr, PathSeparator), - Path: strings.Join(path, PathSeparator), + GVR: strings.Join(gvr, PathSeparator), + Path: strings.Join(path, PathSeparator), + Status: strings.Join(status, PathSeparator), } } +// Flatten returns a collection of node specs. func (t *TreeNode) Flatten() []NodeSpec { var refs []NodeSpec for _, c := range t.Children { @@ -151,23 +172,27 @@ func (t *TreeNode) Flatten() []NodeSpec { return refs } +// Blank returns true if this node is unset. func (t *TreeNode) Blank() bool { return t.GVR == "" && t.ID == "" } +// Hydrate hydrates a full tree bases on a collection of specifications. 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-- { + paths := strings.Split(ref.Path, PathSeparator) + statuses := strings.Split(ref.Status, PathSeparator) + for i := len(paths) - 1; i >= 0; i-- { if nav.Blank() { - nav.GVR, nav.ID = gvrs[i], ids[i] + nav.GVR, nav.ID, nav.Extras[StatusKey] = gvrs[i], paths[i], statuses[i] continue } - c := NewTreeNode(gvrs[i], ids[i]) - if n := nav.Find(gvrs[i], ids[i]); n == nil { + c := NewTreeNode(gvrs[i], paths[i]) + c.Extras[StatusKey] = statuses[i] + if n := nav.Find(gvrs[i], paths[i]); n == nil { nav.Add(c) nav = c } else { @@ -180,6 +205,7 @@ func Hydrate(refs []NodeSpec) *TreeNode { return root } +// Level computes the current node level. func (t *TreeNode) Level() int { var level int p := t @@ -190,6 +216,7 @@ func (t *TreeNode) Level() int { return level - 1 } +// MaxDepth computes the max tree depth. func (t *TreeNode) MaxDepth(depth int) int { max := depth for _, c := range t.Children { @@ -201,10 +228,7 @@ func (t *TreeNode) MaxDepth(depth int) int { return max } -func makeSpacer(d int) string { - return strings.Repeat(" ", d) -} - +// Root returns the current tree root node. func (t *TreeNode) Root() *TreeNode { for p := t; p != nil; p = p.Parent { if p.Parent == nil { @@ -214,23 +238,27 @@ func (t *TreeNode) Root() *TreeNode { return nil } -func (r *TreeNode) IsLeaf() bool { - return r.Empty() +// IsLeaf returns true if node has no children. +func (t *TreeNode) IsLeaf() bool { + return t.CountChildren() == 0 } -func (r *TreeNode) IsRoot() bool { - return r.Parent == nil +// IsRoot returns true if node is top node. +func (t *TreeNode) IsRoot() bool { + return t.Parent == nil } -func (r *TreeNode) ShallowClone() *TreeNode { - return &TreeNode{GVR: r.GVR, ID: r.ID, Extras: r.Extras} +// ShallowClone performs a shallow node clone. +func (t *TreeNode) ShallowClone() *TreeNode { + return &TreeNode{GVR: t.GVR, ID: t.ID, Extras: t.Extras} } -func (r *TreeNode) Filter(q string, filter func(q, path string) bool) *TreeNode { - specs := r.Flatten() +// Filter filters the node based on query. +func (t *TreeNode) Filter(q string, filter func(q, path string) bool) *TreeNode { + specs := t.Flatten() matches := make([]NodeSpec, 0, len(specs)) for _, s := range specs { - if filter(q, s.Path) { + if filter(q, s.Path+s.Status) { matches = append(matches, s) } } @@ -241,6 +269,18 @@ func (r *TreeNode) Filter(q string, filter func(q, path string) bool) *TreeNode return Hydrate(matches) } +// Add adds a new child node. +func (t *TreeNode) Add(c *TreeNode) { + c.Parent = t + t.Children = append(t.Children, c) +} + +// Clear delete all descendant nodes. +func (t *TreeNode) Clear() { + t.Children = []*TreeNode{} +} + +// Find locates a node given a gvr/id spec. func (t *TreeNode) Find(gvr, id string) *TreeNode { if t.GVR == gvr && t.ID == id { return t @@ -253,26 +293,23 @@ func (t *TreeNode) Find(gvr, id string) *TreeNode { return nil } +// Title computes the node title. 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()) + if t.CountChildren() > 0 { + title += fmt.Sprintf("([white::d]%d[-::d])[-::-]", t.CountChildren()) } return title } -func (t *TreeNode) Empty() bool { - return len(t.Children) == 0 -} - -func (t *TreeNode) Clear() { - t.Children = []*TreeNode{} -} +// ---------------------------------------------------------------------------- +// Helpers... +// Dump for debug... func (t *TreeNode) Dump() { dump(t, 0) } @@ -288,6 +325,7 @@ func dump(n *TreeNode, level int) { } } +// Dump to stdout for debug. func (t *TreeNode) DumpStdOut() { dumpStdOut(t, 0) } @@ -303,26 +341,6 @@ func dumpStdOut(n *TreeNode, level int) { } } -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 { @@ -361,7 +379,7 @@ func (t TreeNode) colorize() string { case ToastStatus: color, flag = "orangered", "[red::b]TOAST" case MissingRefStatus: - color, flag = "orange", "[orange::b]MISSING_REF" + color, flag = "orange", "[orange::b]TOAST_REF" } } diff --git a/internal/xray/tree_node_test.go b/internal/xray/tree_node_test.go index c81dab6e..652bc7fd 100644 --- a/internal/xray/tree_node_test.go +++ b/internal/xray/tree_node_test.go @@ -9,6 +9,29 @@ import ( "github.com/stretchr/testify/assert" ) +func TestTreeNodeCount(t *testing.T) { + uu := map[string]struct { + root *xray.TreeNode + e int + }{ + "simple": { + root: root1(), + e: 3, + }, + "complex": { + root: root3(), + e: 26, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.root.Count("")) + }) + } +} + func TestTreeNodeFilter(t *testing.T) { uu := map[string]struct { q string @@ -63,6 +86,9 @@ func TestTreeNodeFilter(t *testing.T) { } func TestTreeNodeHydrate(t *testing.T) { + threeOK := strings.Join([]string{"ok", "ok", "ok"}, xray.PathSeparator) + fiveOK := strings.Join([]string{"ok", "ok", "ok", "ok", "ok"}, xray.PathSeparator) + uu := map[string]struct { spec []xray.NodeSpec e *xray.TreeNode @@ -70,12 +96,14 @@ func TestTreeNodeHydrate(t *testing.T) { "flat_simple": { spec: []xray.NodeSpec{ { - GVR: "containers::v1/pods", - Path: "c1::default/p1", + GVR: "containers::v1/pods", + Path: "c1::default/p1", + Status: threeOK, }, { - GVR: "containers::v1/pods", - Path: "c2::default/p1", + GVR: "containers::v1/pods", + Path: "c2::default/p1", + Status: threeOK, }, }, e: root1(), @@ -83,12 +111,14 @@ func TestTreeNodeHydrate(t *testing.T) { "flat_complex": { spec: []xray.NodeSpec{ { - GVR: "v1/secrets::containers::v1/pods", - Path: "s1::c1::default/p1", + GVR: "v1/secrets::containers::v1/pods", + Path: "s1::c1::default/p1", + Status: threeOK, }, { - GVR: "v1/secrets::containers::v1/pods", - Path: "s2::c2::default/p1", + GVR: "v1/secrets::containers::v1/pods", + Path: "s2::c2::default/p1", + Status: threeOK, }, }, e: root2(), @@ -96,40 +126,49 @@ func TestTreeNodeHydrate(t *testing.T) { "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/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments", + Path: "default/default-token-rr22g::default/nginx-6b866d578b-c6tcn::default/nginx::-/default::deployments", + Status: fiveOK, }, { - 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/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", + Status: fiveOK, }, { - 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/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", + Status: fiveOK, }, { - 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/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", + Status: fiveOK, }, { - 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/coredns-token-5cq9j::kube-system/coredns-6955765f44-r9j9t::kube-system/coredns::-/kube-system::deployments", + Status: fiveOK, }, { - 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/default-token-thzt8::kube-system/metrics-server-6754dbc9df-88bk4::kube-system/metrics-server::-/kube-system::deployments", + Status: fiveOK, }, { - 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: "kube-system/nginx-ingress-token-kff5q::kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55::kube-system/nginx-ingress-controller::-/kube-system::deployments", + Status: fiveOK, }, { - 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/dashboard-metrics-scraper-7b64584c5c-c7b56::kubernetes-dashboard/dashboard-metrics-scraper::-/kubernetes-dashboard::deployments", + Status: fiveOK, }, { - 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", + 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", + Status: fiveOK, }, }, e: root3(), @@ -154,12 +193,14 @@ func TestTreeNodeFlatten(t *testing.T) { root: root1(), e: []xray.NodeSpec{ { - GVR: "containers::v1/pods", - Path: "c1::default/p1", + GVR: "containers::v1/pods", + Path: "c1::default/p1", + Status: strings.Join([]string{"ok", "ok"}, xray.PathSeparator), }, { - GVR: "containers::v1/pods", - Path: "c2::default/p1", + GVR: "containers::v1/pods", + Path: "c2::default/p1", + Status: strings.Join([]string{"ok", "ok"}, xray.PathSeparator), }, }, }, @@ -167,12 +208,14 @@ func TestTreeNodeFlatten(t *testing.T) { root: root2(), e: []xray.NodeSpec{ { - GVR: "v1/secrets::containers::v1/pods", - Path: "s1::c1::default/p1", + GVR: "v1/secrets::containers::v1/pods", + Path: "s1::c1::default/p1", + Status: strings.Join([]string{"ok", "ok", "ok"}, xray.PathSeparator), }, { - GVR: "v1/secrets::containers::v1/pods", - Path: "s2::c2::default/p1", + GVR: "v1/secrets::containers::v1/pods", + Path: "s2::c2::default/p1", + Status: strings.Join([]string{"ok", "ok", "ok"}, xray.PathSeparator), }, }, }, @@ -226,7 +269,7 @@ func TestTreeNodeRoot(t *testing.T) { n.Add(c1) n.Add(c2) - assert.Equal(t, 2, n.Size()) + assert.Equal(t, 2, n.CountChildren()) assert.Equal(t, n, n.Root()) assert.True(t, n.IsRoot()) assert.False(t, n.IsLeaf())