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 ## 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.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I)
* [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8) * [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8)
* [K9s v0 Demo](https://youtu.be/k7zseUhaXeU) * [K9s v0 Demo](https://youtu.be/k7zseUhaXeU)
@ -434,6 +435,10 @@ roleRef:
## Skins ## 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 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` 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 ### GitHub Sponsors
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.
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! 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) * [Norbert Csibra](https://github.com/ncsibra)
* [Andrew Roth](https://github.com/RothAndrew) * [Andrew Roth](https://github.com/RothAndrew)
* [James Smith](https://github.com/sedders123) * [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 ### 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"/> <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...) 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 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: 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 * serviceaccounts
* persistentvolumeclaims * 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. 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" clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
) )
const dialTimeout = 1 * time.Second const dialTimeout = 5 * time.Second
// Config tracks a kubernetes configuration. // Config tracks a kubernetes configuration.
type Config struct { type Config struct {

View File

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

View File

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

View File

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

View File

@ -45,7 +45,7 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error {
return err return err
} }
p.podVolumeRefs(f, node, po.Namespace, po.Spec.Volumes) 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 return err
} }
@ -65,7 +65,7 @@ func (p *Pod) validate(node *TreeNode, po v1.Pod) error {
} }
node.Extras[StatusKey] = status 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 return nil
} }
@ -87,12 +87,12 @@ func (*Pod) containerRefs(ctx context.Context, parent *TreeNode, ns string, spec
return nil return nil
} }
func (*Pod) serviceAccountRef(f dao.Factory, ctx context.Context, parent *TreeNode, ns, sa string) error { func (*Pod) serviceAccountRef(f dao.Factory, ctx context.Context, parent *TreeNode, ns string, spec v1.PodSpec) error {
if sa == "" { if spec.ServiceAccountName == "" {
return nil return nil
} }
id := client.FQN(ns, sa) id := client.FQN(ns, spec.ServiceAccountName)
o, err := f.Get("v1/serviceaccounts", id, false, labels.Everything()) o, err := f.Get("v1/serviceaccounts", id, false, labels.Everything())
if err != nil { if err != nil {
return err return err
@ -104,7 +104,7 @@ func (*Pod) serviceAccountRef(f dao.Factory, ctx context.Context, parent *TreeNo
var saRE ServiceAccount var saRE ServiceAccount
ctx = context.WithValue(ctx, KeyParent, parent) ctx = context.WithValue(ctx, KeyParent, parent)
ctx = context.WithValue(ctx, KeySAAutomount, spec.AutomountServiceAccountToken)
return saRE.Render(ctx, ns, o) 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 { if !ok {
return fmt.Errorf("no factory found in context") return fmt.Errorf("no factory found in context")
} }
node := NewTreeNode("v1/serviceaccounts", client.FQN(sa.Namespace, sa.Name)) node := NewTreeNode("v1/serviceaccounts", client.FQN(sa.Namespace, sa.Name))
node.Extras[StatusKey] = OkStatus
parent, ok := ctx.Value(KeyParent).(*TreeNode) parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok { if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent)) 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) 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 return nil
} }

View File

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

View File

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