diff --git a/Makefile b/Makefile index d9b6f5ea..57b7ee6f 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H: else DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") endif -VERSION ?= v0.40.6 +VERSION ?= v0.40.7 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/README.md b/README.md index f653fc24..8fada8e9 100644 --- a/README.md +++ b/README.md @@ -695,11 +695,17 @@ views: - MEM/RL|S # => 🌚 Overrides std resource default wide attribute via `S` for `Show` - '%MEM/R|' # => NOTE! column names with non alpha names need to be quoted as columns must be strings! - v1/pods@fred: # => 🌚 New v0.40.6! Customize columns for a given resource and namespace! + v1/pods@fred: # => 🌚 New v0.40.6! Customize columns for a given resource and namespace! columns: - AGE - NAMESPACE|WR + v1/pods@kube*: # => 🌚 New v0.40.6! You can also specify a namespace using a regular expression. + columns: + - AGE + - NAMESPACE|WR + + v1/services: columns: - AGE diff --git a/change_logs/release_v0.40.7.md b/change_logs/release_v0.40.7.md new file mode 100644 index 00000000..bb1cc274 --- /dev/null +++ b/change_logs/release_v0.40.7.md @@ -0,0 +1,45 @@ + + +# Release v0.40.7 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for K9s! +I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev +and see if we're happier with some of the fixes! +If you've filed an issue please help me verify and close. + +Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! +Also big thanks to all that have allocated their own time to help others on both slack and on this repo!! + +As you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, +please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +🙀 Hoy! Hosed custom view loading in v0.40.6... + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A) +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#3186](https://github.com/derailed/k9s/pull/3186) fix: allow absolute paths for the 'dir' command + + © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index a0bfa8b1..e4ced887 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -34,6 +34,11 @@ func NewConfig(ks data.KubeSettings) *Config { } } +// IsReadOnly returns true if K9s is running in read-only mode. +func (c *Config) IsReadOnly() bool { + return c.K9s.IsReadOnly() +} + // ActiveClusterName returns the corresponding cluster name. func (c *Config) ActiveClusterName(contextName string) (string, error) { ct, err := c.settings.GetContext(contextName) diff --git a/internal/config/views.go b/internal/config/views.go index 11b3ceae..03c87298 100644 --- a/internal/config/views.go +++ b/internal/config/views.go @@ -142,28 +142,33 @@ func (v *CustomView) fireConfigChanged() { func (v *CustomView) getVS(gvr, ns string) *ViewSetting { k := gvr - if ns != "" { - k += "@" + ns - } - - for key := range maps.Keys(v.Views) { + kk := slices.Collect(maps.Keys(v.Views)) + slices.SortFunc(kk, func(s1, s2 string) int { + return strings.Compare(s1, s2) + }) + slices.Reverse(kk) + for _, key := range kk { if !strings.HasPrefix(key, gvr) { continue } switch { - case key == k: - vs := v.Views[key] - return &vs case strings.Contains(key, "@"): tt := strings.Split(key, "@") if len(tt) != 2 { break } - if rx, err := regexp.Compile(tt[1]); err == nil && rx.MatchString(k) { + nsk := gvr + if ns != "" { + nsk += "@" + ns + } + if rx, err := regexp.Compile(tt[1]); err == nil && rx.MatchString(nsk) { vs := v.Views[key] return &vs } + case key == k: + vs := v.Views[key] + return &vs } } diff --git a/internal/config/views_int_test.go b/internal/config/views_int_test.go index 63e532ce..e113fe71 100644 --- a/internal/config/views_int_test.go +++ b/internal/config/views_int_test.go @@ -43,6 +43,9 @@ func TestCustomView_getVS(t *testing.T) { "toast-no-ns": { gvr: "v1/pods", ns: "zorg", + e: &ViewSetting{ + Columns: []string{"NAMESPACE", "NAME", "AGE", "IP"}, + }, }, "toast-no-res": { diff --git a/internal/ui/table.go b/internal/ui/table.go index de4c5151..ad2c488e 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -12,7 +12,6 @@ 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/model1" "github.com/derailed/k9s/internal/render" @@ -54,6 +53,7 @@ type Table struct { hasMetrics bool ctx context.Context mx sync.RWMutex + readOnly bool } // NewTable returns a new table view. @@ -72,6 +72,13 @@ func NewTable(gvr client.GVR) *Table { } } +func (t *Table) SetReadOnly(ro bool) { + t.mx.Lock() + defer t.mx.Unlock() + + t.readOnly = ro +} + func (t *Table) setSortCol(sc model1.SortColumn) { t.mx.Lock() defer t.mx.Unlock() @@ -297,17 +304,12 @@ func (t *Table) UpdateUI(cdata, data *model1.TableData) { fg := t.styles.Table().Header.FgColor.Color() bg := t.styles.Table().Header.BgColor.Color() - var isNamespaced bool - if m, err := dao.MetaAccess.MetaFor(t.GVR()); err == nil { - isNamespaced = m.Namespaced - } - var col int for _, h := range cdata.Header() { if h.Hide || (!t.wide && h.Wide) { continue } - if h.Name == "NAMESPACE" && (!t.GetModel().ClusterWide() || !isNamespaced) { + if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() { continue } if h.MX && !t.hasMetrics { @@ -333,7 +335,7 @@ func (t *Table) UpdateUI(cdata, data *model1.TableData) { slog.Error("Unable to find original row event", slogs.RowID, re.Row.ID) return true } - t.buildRow(row+1, re, ore, cdata.Header(), pads, isNamespaced) + t.buildRow(row+1, re, ore, cdata.Header(), pads) return true }) @@ -342,7 +344,7 @@ func (t *Table) UpdateUI(cdata, data *model1.TableData) { t.UpdateTitle() } -func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads MaxyPad, isNamespaced bool) { +func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads MaxyPad) { color := model1.DefaultColorer if t.colorerFn != nil { color = t.colorerFn @@ -364,7 +366,7 @@ func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads M continue } - if h[c].Name == "NAMESPACE" && (!t.GetModel().ClusterWide() || !isNamespaced) { + if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() { continue } if h[c].MX && !t.hasMetrics { @@ -532,11 +534,12 @@ func (t *Table) styleTitle() string { if t.Extras != "" { ns = t.Extras } + var title string if ns == client.ClusterScope { - title = SkinTitle(fmt.Sprintf(TitleFmt, t.gvr, render.AsThousands(rc)), t.styles.Frame()) + title = SkinTitle(fmt.Sprintf(TitleFmt, ROIndicator(t.readOnly), t.gvr, render.AsThousands(rc)), t.styles.Frame()) } else { - title = SkinTitle(fmt.Sprintf(NSTitleFmt, t.gvr, ns, render.AsThousands(rc)), t.styles.Frame()) + title = SkinTitle(fmt.Sprintf(NSTitleFmt, ROIndicator(t.readOnly), t.gvr, ns, render.AsThousands(rc)), t.styles.Frame()) } buff := t.cmdBuff.GetText() @@ -552,3 +555,12 @@ func (t *Table) styleTitle() string { return title + SkinTitle(fmt.Sprintf(SearchFmt, buff), t.styles.Frame()) } + +// ROIndicator returns an icon showing whether the session is in readonly mode or not. +func ROIndicator(ro bool) string { + if ro { + return LockedIC + } + + return UnlockedIC +} diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index 30c4359f..535afa0b 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -23,10 +23,10 @@ const ( SearchFmt = "<[filter:bg:r]/%s[fg:bg:-]> " // NSTitleFmt represents a namespaced view title. - NSTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] " + NSTitleFmt = " %s [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] " // TitleFmt represents a standard view title. - TitleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] " + TitleFmt = " %s [fg:bg:b]%s[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] " descIndicator = "↓" ascIndicator = "↑" diff --git a/internal/ui/types.go b/internal/ui/types.go index 4f4222f1..5f8c2b97 100644 --- a/internal/ui/types.go +++ b/internal/ui/types.go @@ -15,6 +15,14 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +const ( + // UnlockedIC represents an unlocked icon. + UnlockedIC = "🔓" + + // LockedIC represents a locked icon. + LockedIC = "🔒" +) + // Namespaceable represents a namespaceable model. type Namespaceable interface { // ClusterWide returns true if the model represents resource in all namespaces. diff --git a/internal/view/actions.go b/internal/view/actions.go index 0b3cf767..335b2965 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -132,7 +132,7 @@ func pluginActions(r Runner, aa *ui.KeyActions) error { var ( errs error aliases = r.Aliases() - ro = r.App().Config.K9s.IsReadOnly() + ro = r.App().Config.IsReadOnly() ) for k, plugin := range pp.Plugins { if !inScope(plugin.Scopes, aliases) { diff --git a/internal/view/browser.go b/internal/view/browser.go index e691b8d0..3cf8533a 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -84,6 +84,7 @@ func (b *Browser) Init(ctx context.Context) error { if b.App().IsRunning() { b.app.CmdBuff().Reset() } + b.Table.SetReadOnly(b.app.Config.IsReadOnly()) b.bindKeys(b.Actions()) for _, f := range b.bindKeysFn { @@ -537,7 +538,7 @@ func (b *Browser) refreshActions() { if b.app.ConOK() { b.namespaceActions(aa) - if !b.app.Config.K9s.IsReadOnly() { + if !b.app.Config.IsReadOnly() { if client.Can(b.meta.Verbs, "edit") { aa.Add(ui.KeyE, ui.NewKeyActionWithOpts("Edit", b.editCmd, ui.ActionOpts{ diff --git a/internal/view/container.go b/internal/view/container.go index babcb2b8..f6b7ad0e 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -83,7 +83,7 @@ func (c *Container) bindDangerousKeys(aa *ui.KeyActions) { func (c *Container) bindKeys(aa *ui.KeyActions) { aa.Delete(tcell.KeyCtrlSpace, ui.KeySpace) - if !c.App().Config.K9s.IsReadOnly() { + if !c.App().Config.IsReadOnly() { c.bindDangerousKeys(aa) } diff --git a/internal/view/dir.go b/internal/view/dir.go index b45bfb26..825d79ec 100644 --- a/internal/view/dir.go +++ b/internal/view/dir.go @@ -81,7 +81,7 @@ func (d *Dir) bindKeys(aa *ui.KeyActions) { // !!BOZO!! Lame! aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ) - if !d.App().Config.K9s.IsReadOnly() { + if !d.App().Config.IsReadOnly() { d.bindDangerousKeys(aa) } aa.Bulk(ui.KeyMap{ diff --git a/internal/view/helm_history.go b/internal/view/helm_history.go index a2b5e5a9..4bbd7c31 100644 --- a/internal/view/helm_history.go +++ b/internal/view/helm_history.go @@ -54,7 +54,7 @@ func (h *History) HistoryContext(ctx context.Context) context.Context { } func (h *History) bindKeys(aa *ui.KeyActions) { - if !h.App().Config.K9s.IsReadOnly() { + if !h.App().Config.IsReadOnly() { h.bindDangerousKeys(aa) } diff --git a/internal/view/image_extender.go b/internal/view/image_extender.go index 96d364c9..c3675acc 100644 --- a/internal/view/image_extender.go +++ b/internal/view/image_extender.go @@ -58,7 +58,7 @@ func NewImageExtender(r ResourceViewer) ResourceViewer { } func (s *ImageExtender) bindKeys(aa *ui.KeyActions) { - if s.App().Config.K9s.IsReadOnly() { + if s.App().Config.IsReadOnly() { return } aa.Add(ui.KeyI, ui.NewKeyAction("Set Image", s.setImageCmd, false)) diff --git a/internal/view/live_view.go b/internal/view/live_view.go index e63d88e0..94b29578 100644 --- a/internal/view/live_view.go +++ b/internal/view/live_view.go @@ -153,7 +153,7 @@ func (v *LiveView) bindKeys() { tcell.KeyDelete: ui.NewSharedKeyAction("Erase", v.eraseCmd, false), }) - if !v.app.Config.K9s.IsReadOnly() { + if !v.app.Config.IsReadOnly() { v.actions.Add(ui.KeyE, ui.NewKeyAction("Edit", v.editCmd, true)) } if v.title == yamlAction { diff --git a/internal/view/node.go b/internal/view/node.go index 165c7f17..8d024630 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -78,7 +78,7 @@ func (n *Node) bindDangerousKeys(aa *ui.KeyActions) { } func (n *Node) bindKeys(aa *ui.KeyActions) { - if !n.App().Config.K9s.IsReadOnly() { + if !n.App().Config.IsReadOnly() { n.bindDangerousKeys(aa) } diff --git a/internal/view/pod.go b/internal/view/pod.go index df219d8f..555e8839 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -124,7 +124,7 @@ func (p *Pod) bindDangerousKeys(aa *ui.KeyActions) { } func (p *Pod) bindKeys(aa *ui.KeyActions) { - if !p.App().Config.K9s.IsReadOnly() { + if !p.App().Config.IsReadOnly() { p.bindDangerousKeys(aa) } diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go index 668e9bad..b18550c5 100644 --- a/internal/view/restart_extender.go +++ b/internal/view/restart_extender.go @@ -30,7 +30,7 @@ func NewRestartExtender(v ResourceViewer) ResourceViewer { // BindKeys creates additional menu actions. func (r *RestartExtender) bindKeys(aa *ui.KeyActions) { - if r.App().Config.K9s.IsReadOnly() { + if r.App().Config.IsReadOnly() { return } aa.Add(ui.KeyR, ui.NewKeyActionWithOpts("Restart", r.restartCmd, diff --git a/internal/view/sanitizer.go b/internal/view/sanitizer.go index 4497e8d5..2c7ef263 100644 --- a/internal/view/sanitizer.go +++ b/internal/view/sanitizer.go @@ -419,9 +419,9 @@ func (s *Sanitizer) styleTitle() string { var title string if ns == client.ClusterScope { - title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(s.Count))), s.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, ui.ROIndicator(s.app.Config.IsReadOnly()), base, render.AsThousands(int64(s.Count))), s.app.Styles.Frame()) } else { - title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(s.Count))), s.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, ui.ROIndicator(s.app.Config.IsReadOnly()), base, ns, render.AsThousands(int64(s.Count))), s.app.Styles.Frame()) } buff := s.CmdBuff().GetText() diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go index 86ae6f06..8e503e5a 100644 --- a/internal/view/scale_extender.go +++ b/internal/view/scale_extender.go @@ -32,7 +32,7 @@ func NewScaleExtender(r ResourceViewer) ResourceViewer { } func (s *ScaleExtender) bindKeys(aa *ui.KeyActions) { - if s.App().Config.K9s.IsReadOnly() { + if s.App().Config.IsReadOnly() { return } diff --git a/internal/view/workload.go b/internal/view/workload.go index fea30e19..cf0beedd 100644 --- a/internal/view/workload.go +++ b/internal/view/workload.go @@ -53,7 +53,7 @@ func (w *Workload) bindDangerousKeys(aa *ui.KeyActions) { } func (w *Workload) bindKeys(aa *ui.KeyActions) { - if !w.App().Config.K9s.IsReadOnly() { + if !w.App().Config.IsReadOnly() { w.bindDangerousKeys(aa) } diff --git a/internal/view/xray.go b/internal/view/xray.go index c797e071..aa96b8af 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -671,9 +671,9 @@ func (x *Xray) styleTitle() string { var title string if ns == client.ClusterScope { - title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(x.Count))), x.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, ui.ROIndicator(x.app.Config.IsReadOnly()), base, render.AsThousands(int64(x.Count))), x.app.Styles.Frame()) } else { - title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(x.Count))), x.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, ui.ROIndicator(x.app.Config.IsReadOnly()), base, ns, render.AsThousands(int64(x.Count))), x.app.Styles.Frame()) } buff := x.CmdBuff().GetText() diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index c662218b..9f7e5cfb 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: k9s base: core22 -version: 'v0.40.6' +version: 'v0.40.7' 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 your clusters in a single powerful session.