checkpoint

mine
derailed 2020-01-18 18:45:17 -07:00
parent 860728c083
commit d45d3af116
29 changed files with 699 additions and 519 deletions

View File

@ -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

View File

@ -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.

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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 {

View File

@ -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...

View File

@ -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(

View File

@ -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.

View File

@ -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",
},
{

View File

@ -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)

View File

@ -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)

View File

@ -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])
})
}

View File

@ -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)
}

View File

@ -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())
})
}
}

View File

@ -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)
}

View File

@ -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())
})
}
}

View File

@ -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
}

View File

@ -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())
})
}
}

View File

@ -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
}

View File

@ -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())
})
}
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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())
})
}
}

View File

@ -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
}

View File

@ -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())
})
}
}

View File

@ -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),
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"
}
}

View File

@ -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
@ -72,10 +98,12 @@ func TestTreeNodeHydrate(t *testing.T) {
{
GVR: "containers::v1/pods",
Path: "c1::default/p1",
Status: threeOK,
},
{
GVR: "containers::v1/pods",
Path: "c2::default/p1",
Status: threeOK,
},
},
e: root1(),
@ -85,10 +113,12 @@ func TestTreeNodeHydrate(t *testing.T) {
{
GVR: "v1/secrets::containers::v1/pods",
Path: "s1::c1::default/p1",
Status: threeOK,
},
{
GVR: "v1/secrets::containers::v1/pods",
Path: "s2::c2::default/p1",
Status: threeOK,
},
},
e: root2(),
@ -98,38 +128,47 @@ func TestTreeNodeHydrate(t *testing.T) {
{
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",
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",
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",
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",
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",
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",
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",
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",
Status: fiveOK,
},
},
e: root3(),
@ -156,10 +195,12 @@ func TestTreeNodeFlatten(t *testing.T) {
{
GVR: "containers::v1/pods",
Path: "c1::default/p1",
Status: strings.Join([]string{"ok", "ok"}, xray.PathSeparator),
},
{
GVR: "containers::v1/pods",
Path: "c2::default/p1",
Status: strings.Join([]string{"ok", "ok"}, xray.PathSeparator),
},
},
},
@ -169,10 +210,12 @@ func TestTreeNodeFlatten(t *testing.T) {
{
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",
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())