Rel v0.40.7 (#3188)

* [FEAT] add readonly indicator

* [FIX] Hosed cust view loading

* rel notes
mine
Fernand Galiana 2025-03-10 12:29:36 -06:00 committed by GitHub
parent 1244cc518d
commit 08b8efa617
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 127 additions and 42 deletions

View File

@ -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}

View File

@ -700,6 +700,12 @@ views:
- 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

View File

@ -0,0 +1,45 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# 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
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -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)

View File

@ -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
}
}

View File

@ -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": {

View File

@ -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
}

View File

@ -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 = "↑"

View File

@ -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.

View File

@ -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) {

View File

@ -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{

View File

@ -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)
}

View File

@ -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{

View File

@ -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)
}

View File

@ -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))

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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,

View File

@ -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()

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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.