diff --git a/README.md b/README.md index 27119886..47fa043a 100644 --- a/README.md +++ b/README.md @@ -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 ;) + + + 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` diff --git a/assets/skins/dracula.png b/assets/skins/dracula.png index b396eb2a..4b2e8f02 100644 Binary files a/assets/skins/dracula.png and b/assets/skins/dracula.png differ diff --git a/change_logs/release_0.13.0.md b/change_logs/release_v0.13.0.md similarity index 90% rename from change_logs/release_0.13.0.md rename to change_logs/release_v0.13.0.md index 207d161a..bb94762c 100644 --- a/change_logs/release_0.13.0.md +++ b/change_logs/release_v0.13.0.md @@ -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 -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. diff --git a/internal/client/config.go b/internal/client/config.go index 32fe8f23..30c0ae8d 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -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 { diff --git a/internal/view/help.go b/internal/view/help.go index 59629ab7..90ead948 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -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 diff --git a/internal/xray/dp.go b/internal/xray/dp.go index 4df4a210..524421f8 100644 --- a/internal/xray/dp.go +++ b/internal/xray/dp.go @@ -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 } diff --git a/internal/xray/ds.go b/internal/xray/ds.go index e3ffb45d..2bd1a534 100644 --- a/internal/xray/ds.go +++ b/internal/xray/ds.go @@ -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 } diff --git a/internal/xray/pod.go b/internal/xray/pod.go index fa22421d..0f8fd666 100644 --- a/internal/xray/pod.go +++ b/internal/xray/pod.go @@ -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) } diff --git a/internal/xray/sa.go b/internal/xray/sa.go index 374818c9..60f9bd34 100644 --- a/internal/xray/sa.go +++ b/internal/xray/sa.go @@ -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 } diff --git a/internal/xray/sts.go b/internal/xray/sts.go index c0dd40ee..4d7a9e6b 100644 --- a/internal/xray/sts.go +++ b/internal/xray/sts.go @@ -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 } diff --git a/internal/xray/tree_node.go b/internal/xray/tree_node.go index 35276b0b..59ac999c 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -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) }