checkpoint
parent
860728c083
commit
d45d3af116
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Reference in New Issue