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.