checkpoint

mine
derailed 2020-01-19 22:16:55 -07:00
parent 4d557fb813
commit 990d58e585
11 changed files with 114 additions and 81 deletions

View File

@ -95,6 +95,7 @@ K9s is available on Linux, OSX and Windows platforms.
## Demo Video
* [K9s v0.13.0](https://www.youtube.com/watch?v=qaeR2iK7U0o&t=15s)
* [K9s v0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I)
* [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8)
* [K9s v0 Demo](https://youtu.be/k7zseUhaXeU)
@ -434,6 +435,10 @@ roleRef:
## Skins
Example: Dracula Skin ;)
<img src="assets/skins/dracula.png">
You can style K9s based on your own sense of look and style. Skins are YAML files, that enable a user to change the K9s presentation layer. K9s skins are loaded from `$HOME/.k9s/skin.yml`. If a skin file is detected then the skin would be loaded if not the current stock skin remains in effect.
You can also change K9s skins based on the cluster you are connecting too. In this case, you can specify the skin file name as `$HOME/.k9s/mycluster_skin.yml`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 KiB

After

Width:  |  Height:  |  Size: 560 KiB

View File

@ -10,17 +10,16 @@ Also if you dig this tool, please make some noise on social! [@kitesurfer](https
---
### GH Sponsor
I know a lot of you have voiced in the past for other ways to contribute to this project ie liquids budget or prozac supplies whichever best applies here... So I've enabled github sponsors and the button should now be available on this repo.
### GitHub Sponsors
I'd like to personally thank the following folks for their support and efforts with this project as I know some of you have been around since it's inception almost a year ago!
* [Norbert Csibra](https://github.com/ncsibra)
* [Andrew Roth](https://github.com/RothAndrew)
* [James Smith](https://github.com/sedders123)
* [Daniel Koopmans](https://github.com/fsdaniel)
Big thanks in full effect to you all, I am so humbled and honored by your gesture!
Big thanks in full effect to you all, I am so humbled and honored by your kind actions!
### Dracula Skin
@ -32,7 +31,7 @@ Since we're in the thank you phase, might as well lasso in `Josh Symmonds` for c
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_xray.png"/>
Since we've launched K9s, we've longed for a view that would display the relationships among resources. For instance, pods may reference configmaps/secrets directly via volumes or indirectly with containers referencing configmaps/secrets via say env vars. Having the ability to know which pods/deployments use a given configmap may involve some serious `kubectl` wizardry. K9s now has xray vision which allows one to view and traverse these relationships/associations.
Since we've launched K9s, we've longed for a view that would display the relationships among resources. For instance, pods may reference configmaps/secrets directly via volumes or indirectly with containers referencing configmaps/secrets via say env vars. Having the ability to know which pods/deployments use a given configmap may involve some serious `kubectl` wizardry. K9s now has xray vision which allows one to view and traverse these relationships/associations as well as check for referential integrity.
For this, we are introducing a new command aka `xray`. Xray initally supports the following resources (more to come later...)
@ -45,7 +44,7 @@ To enable cluster xray vision for deployments simply type `:xray deploy`. You ca
Xray not only will tell you when a resource is considered `TOAST` ie the resource is in a bad state, but also will tell you if a dependency is actually broken via `TOAST_REF` status. For example a pod referencing a configmap that has been deleted from the cluster.
Xray view also supports for filtering the resources by leveraging regex, labels or fuzzy filters. This affords for getting more of an application view across several resources.
Xray view also supports for filtering the resources by leveraging regex, labels or fuzzy filters. This affords for getting more of an application `cross-cut` among several resources.
As it stands Xray will check for following resource dependencies:
@ -56,7 +55,7 @@ As it stands Xray will check for following resource dependencies:
* serviceaccounts
* persistentvolumeclaims
Keep in mind these can be expensive traversals and the view is eventually consistent as dependent resources would be lazy loaded.
Keep in mind these can be expensive traversals and the view is eventually consistent as dependent resources will be lazy loaded.
We hope you'll find this feature useful? Keep in mind this is an initial drop and more will be coming in this area in subsequent releases. As always, your comments/suggestions are encouraged and welcomed.

View File

@ -17,7 +17,7 @@ import (
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
const dialTimeout = 1 * time.Second
const dialTimeout = 5 * time.Second
// Config tracks a kubernetes configuration.
type Config struct {

View File

@ -40,63 +40,64 @@ func NewHelp() *Help {
}
// Init initializes the component.
func (v *Help) Init(ctx context.Context) error {
if err := v.Table.Init(ctx); err != nil {
func (h *Help) Init(ctx context.Context) error {
if err := h.Table.Init(ctx); err != nil {
return nil
}
v.SetSelectable(false, false)
v.resetTitle()
v.SetBorder(true)
v.SetBorderPadding(0, 0, 1, 1)
v.bindKeys()
v.build()
v.SetBackgroundColor(v.App().Styles.BgColor())
h.SetSelectable(false, false)
h.resetTitle()
h.SetBorder(true)
h.SetBorderPadding(0, 0, 1, 1)
h.bindKeys()
h.build()
h.SetBackgroundColor(h.App().Styles.BgColor())
return nil
}
func (v *Help) bindKeys() {
v.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS)
v.Actions().Set(ui.KeyActions{
tcell.KeyEsc: ui.NewKeyAction("Back", v.app.PrevCmd, false),
ui.KeyHelp: ui.NewKeyAction("Back", v.app.PrevCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Back", v.app.PrevCmd, false),
func (h *Help) bindKeys() {
h.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS)
h.Actions().Set(ui.KeyActions{
tcell.KeyEsc: ui.NewKeyAction("Back", h.app.PrevCmd, false),
ui.KeyHelp: ui.NewKeyAction("Back", h.app.PrevCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Back", h.app.PrevCmd, false),
})
}
func (v *Help) computeMaxes(hh model.MenuHints) {
for _, h := range hh {
if len(h.Mnemonic) > v.maxKey {
v.maxKey = len(h.Mnemonic)
func (h *Help) computeMaxes(hh model.MenuHints) {
h.maxKey, h.maxDesc = 0, 0
for _, hint := range hh {
if len(hint.Mnemonic) > h.maxKey {
h.maxKey = len(hint.Mnemonic)
}
if len(h.Description) > v.maxDesc {
v.maxDesc = len(h.Description)
if len(hint.Description) > h.maxDesc {
h.maxDesc = len(hint.Description)
}
}
v.maxKey += 2
h.maxKey += 2
}
func (v *Help) build() {
v.Clear()
func (h *Help) build() {
h.Clear()
v.maxRows = len(v.showGeneral())
ff := []HelpFunc{v.app.Content.Top().Hints, v.showGeneral, v.showNav, v.showHelp}
h.maxRows = len(h.showGeneral())
ff := []HelpFunc{h.app.Content.Top().Hints, h.showGeneral, h.showNav, h.showHelp}
var col int
for i, section := range []string{"RESOURCE", "GENERAL", "NAVIGATION", "HELP"} {
hh := ff[i]()
sort.Sort(hh)
v.computeMaxes(hh)
v.addSection(col, section, hh)
h.computeMaxes(hh)
h.addSection(col, section, hh)
col += 2
}
if h, err := v.showHotKeys(); err == nil {
v.computeMaxes(h)
v.addSection(col, "HOTKEYS", h)
if hh, err := h.showHotKeys(); err == nil {
h.computeMaxes(hh)
h.addSection(col, "HOTKEYS", hh)
}
}
func (v *Help) showHelp() model.MenuHints {
func (h *Help) showHelp() model.MenuHints {
return model.MenuHints{
{
Mnemonic: "?",
@ -109,7 +110,7 @@ func (v *Help) showHelp() model.MenuHints {
}
}
func (v *Help) showNav() model.MenuHints {
func (h *Help) showNav() model.MenuHints {
return model.MenuHints{
{
Mnemonic: "g",
@ -145,7 +146,7 @@ func (v *Help) showNav() model.MenuHints {
}
}
func (v *Help) showHotKeys() (model.MenuHints, error) {
func (h *Help) showHotKeys() (model.MenuHints, error) {
hh := config.NewHotKeys()
if err := hh.Load(); err != nil {
return nil, fmt.Errorf("no hotkey configuration found")
@ -166,7 +167,7 @@ func (v *Help) showHotKeys() (model.MenuHints, error) {
return mm, nil
}
func (v *Help) showGeneral() model.MenuHints {
func (h *Help) showGeneral() model.MenuHints {
return model.MenuHints{
{
Mnemonic: ":cmd",
@ -219,20 +220,20 @@ func (v *Help) showGeneral() model.MenuHints {
}
}
func (v *Help) resetTitle() {
v.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle))
func (h *Help) resetTitle() {
h.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle))
}
func (v *Help) addSpacer(c int) {
cell := tview.NewTableCell(render.Pad("", v.maxKey))
cell.SetBackgroundColor(v.App().Styles.BgColor())
func (h *Help) addSpacer(c int) {
cell := tview.NewTableCell(render.Pad("", h.maxKey))
cell.SetBackgroundColor(h.App().Styles.BgColor())
cell.SetExpansion(1)
v.SetCell(0, c, cell)
h.SetCell(0, c, cell)
}
func (v *Help) addSection(c int, title string, hh model.MenuHints) {
if len(hh) > v.maxRows {
v.maxRows = len(hh)
func (h *Help) addSection(c int, title string, hh model.MenuHints) {
if len(hh) > h.maxRows {
h.maxRows = len(hh)
}
row := 0
cell := tview.NewTableCell(title)
@ -240,40 +241,43 @@ func (v *Help) addSection(c int, title string, hh model.MenuHints) {
cell.SetAttributes(tcell.AttrBold)
cell.SetExpansion(1)
cell.SetAlign(tview.AlignLeft)
v.SetCell(row, c, cell)
v.addSpacer(c + 1)
h.SetCell(row, c, cell)
h.addSpacer(c + 1)
row++
for _, h := range hh {
for _, hint := range hh {
col := c
cell := tview.NewTableCell(render.Pad(toMnemonic(h.Mnemonic), v.maxKey))
if _, err := strconv.Atoi(h.Mnemonic); err != nil {
cell := tview.NewTableCell(render.Pad(toMnemonic(hint.Mnemonic), h.maxKey))
if _, err := strconv.Atoi(hint.Mnemonic); err != nil {
cell.SetTextColor(tcell.ColorDodgerBlue)
} else {
cell.SetTextColor(tcell.ColorFuchsia)
}
cell.SetAttributes(tcell.AttrBold)
v.SetCell(row, col, cell)
h.SetCell(row, col, cell)
col++
cell = tview.NewTableCell(render.Pad(h.Description, v.maxDesc))
cell = tview.NewTableCell(render.Pad(hint.Description, h.maxDesc))
cell.SetTextColor(tcell.ColorWhite)
v.SetCell(row, col, cell)
h.SetCell(row, col, cell)
row++
}
if len(hh) < v.maxRows {
for i := v.maxRows - len(hh); i > 0; i-- {
if len(hh) < h.maxRows {
for i := h.maxRows - len(hh); i > 0; i-- {
col := c
cell := tview.NewTableCell(render.Pad("", v.maxKey))
v.SetCell(row, col, cell)
cell := tview.NewTableCell(render.Pad("", h.maxKey))
h.SetCell(row, col, cell)
col++
cell = tview.NewTableCell(render.Pad("", v.maxDesc))
v.SetCell(row, col, cell)
cell = tview.NewTableCell(render.Pad("", h.maxDesc))
h.SetCell(row, col, cell)
row++
}
}
}
// ----------------------------------------------------------------------------
// Helpers...
func toMnemonic(s string) string {
if len(s) == 0 {
return s

View File

@ -72,9 +72,11 @@ func (*Deployment) validate(root *TreeNode, dp appsv1.Deployment) error {
r = int32(*dp.Spec.Replicas)
}
a := dp.Status.AvailableReplicas
if a != r {
if a != r || dp.Status.UnavailableReplicas != 0 {
root.Extras[StatusKey] = ToastStatus
}
root.Extras[InfoKey] = fmt.Sprintf("%d/%d/%d", a, r, dp.Status.UnavailableReplicas)
return nil
}

View File

@ -67,6 +67,7 @@ func (*DaemonSet) validate(root *TreeNode, ds appsv1.DaemonSet) error {
if d != a {
root.Extras[StatusKey] = ToastStatus
}
root.Extras[InfoKey] = fmt.Sprintf("%d/%d", a, d)
return nil
}

View File

@ -45,7 +45,7 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error {
return err
}
p.podVolumeRefs(f, node, po.Namespace, po.Spec.Volumes)
if err := p.serviceAccountRef(f, ctx, node, po.Namespace, po.Spec.ServiceAccountName); err != nil {
if err := p.serviceAccountRef(f, ctx, node, po.Namespace, po.Spec); err != nil {
return err
}
@ -65,7 +65,7 @@ func (p *Pod) validate(node *TreeNode, po v1.Pod) error {
}
node.Extras[StatusKey] = status
node.Extras[StateKey] = strconv.Itoa(cr) + "/" + strconv.Itoa(len(ss))
node.Extras[InfoKey] = strconv.Itoa(cr) + "/" + strconv.Itoa(len(ss))
return nil
}
@ -87,12 +87,12 @@ func (*Pod) containerRefs(ctx context.Context, parent *TreeNode, ns string, spec
return nil
}
func (*Pod) serviceAccountRef(f dao.Factory, ctx context.Context, parent *TreeNode, ns, sa string) error {
if sa == "" {
func (*Pod) serviceAccountRef(f dao.Factory, ctx context.Context, parent *TreeNode, ns string, spec v1.PodSpec) error {
if spec.ServiceAccountName == "" {
return nil
}
id := client.FQN(ns, sa)
id := client.FQN(ns, spec.ServiceAccountName)
o, err := f.Get("v1/serviceaccounts", id, false, labels.Everything())
if err != nil {
return err
@ -104,7 +104,7 @@ func (*Pod) serviceAccountRef(f dao.Factory, ctx context.Context, parent *TreeNo
var saRE ServiceAccount
ctx = context.WithValue(ctx, KeyParent, parent)
ctx = context.WithValue(ctx, KeySAAutomount, spec.AutomountServiceAccountToken)
return saRE.Render(ctx, ns, o)
}

View File

@ -30,9 +30,8 @@ func (s *ServiceAccount) Render(ctx context.Context, ns string, o interface{}) e
if !ok {
return fmt.Errorf("no factory found in context")
}
node := NewTreeNode("v1/serviceaccounts", client.FQN(sa.Namespace, sa.Name))
node.Extras[StatusKey] = OkStatus
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
@ -46,5 +45,18 @@ func (s *ServiceAccount) Render(ctx context.Context, ns string, o interface{}) e
addRef(f, node, "v1/secrets", client.FQN(sa.Namespace, sec.Name), nil)
}
auto, _ := ctx.Value(KeySAAutomount).(*bool)
return s.validate(node, sa, auto)
}
func (*ServiceAccount) validate(node *TreeNode, sa v1.ServiceAccount, auto *bool) error {
node.Extras[StatusKey] = OkStatus
if sa.AutomountServiceAccountToken != nil {
node.Extras[InfoKey] = fmt.Sprintf("automount=%t", *sa.AutomountServiceAccountToken)
}
if auto != nil {
node.Extras[InfoKey] = fmt.Sprintf("automount=%t", *auto)
}
return nil
}

View File

@ -68,10 +68,11 @@ func (*StatefulSet) validate(root *TreeNode, sts appsv1.StatefulSet) error {
if sts.Spec.Replicas != nil {
r = int32(*sts.Spec.Replicas)
}
a := sts.Status.Replicas
a := sts.Status.CurrentReplicas
if a != r {
root.Extras[StatusKey] = ToastStatus
}
root.Extras[InfoKey] = fmt.Sprintf("%d/%d", a, r)
return nil
}

View File

@ -15,14 +15,17 @@ const (
// KeyParent indicates a parent node context key.
KeyParent TreeRef = "parent"
// KeySAAutomount indicates whether an automount sa token is active or not.
KeySAAutomount TreeRef = "automount"
// PathSeparator represents a node path separatot.
PathSeparator = "::"
// StatusKey status map key.
StatusKey = "status"
// StateKey state map key.
StateKey = "state"
// InfoKey state map key.
InfoKey = "info"
// OkStatus stands for all is cool.
OkStatus = "ok"
@ -294,7 +297,7 @@ func (t *TreeNode) Find(gvr, id string) *TreeNode {
func (t *TreeNode) Title() string {
const withNS = "[white::b]%s[-::d]"
title := fmt.Sprintf(withNS, t.colorize())
title := fmt.Sprintf(withNS, t.AsString())
if t.CountChildren() > 0 {
title += fmt.Sprintf("([white::d]%d[-::d])[-::-]", t.CountChildren())
@ -366,7 +369,7 @@ func toEmoji(gvr string) string {
}
}
func (t TreeNode) colorize() string {
func (t TreeNode) AsString() string {
const colorFmt = "%s [gray::-][%s[gray::-]] [%s::b]%s[::]"
_, n := client.Namespaced(t.ID)
@ -379,6 +382,12 @@ func (t TreeNode) colorize() string {
color, flag = "orange", "[orange::b]TOAST_REF"
}
}
str := fmt.Sprintf(colorFmt, toEmoji(t.GVR), flag, color, n)
return fmt.Sprintf(colorFmt, toEmoji(t.GVR), flag, color, n)
i, ok := t.Extras[InfoKey]
if !ok {
return str
}
return fmt.Sprintf("%s [antiquewhite::][%s][::] ", str, i)
}