Rel v0.50.2 (#3269)

* fix#3266 dp alias fails

* fix#3267 add no data flash

* fix#3264 storage-class broke describe/yaml

* fix#3260 po yaml crash

* rel notes
mine
Fernand Galiana 2025-04-10 09:29:21 -06:00 committed by GitHub
parent 419a0ce6dc
commit bc22b87053
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 167 additions and 32 deletions

View File

@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
else else
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
endif endif
VERSION ?= v0.50.1 VERSION ?= v0.50.2
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,40 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# Release v0.50.2
## 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)
## 5-0, 5-0 HotFix!
It looks like we've broken a few (more) things in the clean up process 😳
This is what you get for trying to refresh a ~10 year old code base 🙀
Apologizes for the `disruption in the farce`. Hopefully much happier on v0.50.2...
Are we there yet? Crossing fingers AND toes...
☠️ Careful on this upgrade! 🏴‍☠️
We've gone thru lots of code revamp/refactor in the v0.50.0, so mileage may vary...
---
## Resolved Issues
* [#3267](https://github.com/derailed/k9s/issues/3267) Show some output or message when no resources are found
* [#3266](https://github.com/derailed/k9s/issues/3266) Command alias :dp fails with "no resource meta defined for deployments" error
* [#3264](https://github.com/derailed/k9s/issues/3264) can't execute get(y) or describe(d) in StorageClass view
* [#3260](https://github.com/derailed/k9s/issues/3260) yaml view of pod will crash the app (Boom!! cannot deep copy int. (Maybe??)
---
<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

@ -22,6 +22,14 @@ func IsClusterWide(ns string) bool {
return ns == NamespaceAll || ns == BlankNamespace || ns == ClusterScope return ns == NamespaceAll || ns == BlankNamespace || ns == ClusterScope
} }
func PrintNamespace(ns string) string {
if IsAllNamespaces(ns) {
return "all"
}
return ns
}
// CleanseNamespace ensures all ns maps to blank. // CleanseNamespace ensures all ns maps to blank.
func CleanseNamespace(ns string) string { func CleanseNamespace(ns string) string {
if IsAllNamespace(ns) { if IsAllNamespace(ns) {

View File

@ -84,16 +84,14 @@ func (a *Aliases) Resolve(command string) (*client.GVR, string, bool) {
if !ok { if !ok {
return nil, "", false return nil, "", false
} }
if agvr.IsCommand() {
p := cmd.NewInterpreter(agvr.String()) p := cmd.NewInterpreter(agvr.String())
gvr, ok := a.Get(p.Cmd()) gvr, ok := a.Get(p.Cmd())
if !ok { if !ok {
return nil, "", false return agvr, "", true
}
return gvr, p.Args(), true
} }
return agvr, "", true return gvr, p.Args(), true
} }
// Get retrieves an alias. // Get retrieves an alias.

View File

@ -18,10 +18,13 @@ import (
func TestAsGVR(t *testing.T) { func TestAsGVR(t *testing.T) {
a := dao.NewAlias(makeFactory()) a := dao.NewAlias(makeFactory())
a.Define(client.PodGVR, "po", "pod", "pods") a.Define(client.PodGVR, "po", "pipo", "pod")
a.Define(client.PodGVR, client.PodGVR.String())
a.Define(client.PodGVR, client.PodGVR.AsResourceName())
a.Define(client.WkGVR, client.WkGVR.String(), "workload", "wkl") a.Define(client.WkGVR, client.WkGVR.String(), "workload", "wkl")
a.Define(client.NewGVR("pod default"), "pp") a.Define(client.NewGVR("pod default"), "pp")
a.Define(client.NewGVR("pod default @fred"), "ppc") a.Define(client.NewGVR("pipo default"), "ppo")
a.Define(client.NewGVR("pod default app=fred @fred"), "ppc")
uu := map[string]struct { uu := map[string]struct {
cmd string cmd string
@ -29,40 +32,59 @@ func TestAsGVR(t *testing.T) {
gvr *client.GVR gvr *client.GVR
exp string exp string
}{ }{
"ok": { "gvr": {
cmd: "v1/pods",
ok: true,
gvr: client.PodGVR,
},
"r": {
cmd: "pods", cmd: "pods",
ok: true, ok: true,
gvr: client.PodGVR, gvr: client.PodGVR,
}, },
"ok-short": { "alias1": {
cmd: "po", cmd: "po",
ok: true, ok: true,
gvr: client.PodGVR, gvr: client.PodGVR,
}, },
"alias-2": {
cmd: "pipo",
ok: true,
gvr: client.PodGVR,
},
"missing": { "missing": {
cmd: "zorg", cmd: "zorg",
}, },
"alias": { "no-args": {
cmd: "wkl", cmd: "wkl",
ok: true, ok: true,
gvr: client.WkGVR, gvr: client.WkGVR,
}, },
"ns-alias": { "ns-arg": {
cmd: "pp", cmd: "pp",
ok: true, ok: true,
gvr: client.PodGVR, gvr: client.PodGVR,
exp: "default", exp: "default",
}, },
"ns-inception": {
cmd: "ppo",
ok: true,
gvr: client.PodGVR,
exp: "default",
},
"full-alias": { "full-alias": {
cmd: "ppc", cmd: "ppc",
ok: true, ok: true,
gvr: client.PodGVR, gvr: client.PodGVR,
exp: "default @fred", exp: "default app=fred @fred",
}, },
} }

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"maps"
"math" "math"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
@ -85,15 +86,22 @@ func ToYAML(o runtime.Object, showManaged bool) (string, error) {
if o == nil { if o == nil {
return "", errors.New("no object to yamlize") return "", errors.New("no object to yamlize")
} }
u, ok := o.(*unstructured.Unstructured)
if !ok {
return "", fmt.Errorf("expecting unstructured but got %T", o)
}
if u.Object == nil {
return "", fmt.Errorf("expecting unstructured object but got nil")
}
mm := u.Object
var ( var (
buff bytes.Buffer buff bytes.Buffer
p printers.YAMLPrinter p printers.YAMLPrinter
) )
if !showManaged { if !showManaged {
o = o.DeepCopyObject() mm = maps.Clone(mm)
uo := o.(*unstructured.Unstructured).Object if meta, ok := mm["metadata"].(map[string]any); ok {
if meta, ok := uo["metadata"].(map[string]any); ok {
delete(meta, "managedFields") delete(meta, "managedFields")
} }
} }

View File

@ -331,6 +331,9 @@ func loadPreferred(f Factory, m ResourceMetas) error {
if !isStandardGroup(r.GroupVersion) { if !isStandardGroup(r.GroupVersion) {
res.Categories = append(res.Categories, crdCat) res.Categories = append(res.Categories, crdCat)
} }
if isScalable(gvr) {
res.Categories = append(res.Categories, scaleCat)
}
m[gvr] = &res m[gvr] = &res
} }
} }
@ -342,6 +345,12 @@ func isStandardGroup(gv string) bool {
return stdGroups.Has(gv) || strings.Contains(gv, ".k8s.io") return stdGroups.Has(gv) || strings.Contains(gv, ".k8s.io")
} }
func isScalable(gvr *client.GVR) bool {
ss := sets.New(client.DpGVR, client.StsGVR)
return ss.Has(gvr)
}
var deprecatedGVRs = sets.New( var deprecatedGVRs = sets.New(
client.NewGVR("v1/events"), client.NewGVR("v1/events"),
client.NewGVR("extensions/v1beta1/ingresses"), client.NewGVR("extensions/v1beta1/ingresses"),

View File

@ -6,12 +6,9 @@ package dao
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"time"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/slogs"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -72,7 +69,6 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
ti := time.Now()
o, err := c.Get(). o, err := c.Get().
SetHeader("Accept", header). SetHeader("Accept", header).
Param("includeObject", includeObject). Param("includeObject", includeObject).
@ -86,7 +82,6 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
slog.Debug("Q Time", slogs.Elapsed, time.Since(ti))
namespaced := true namespaced := true
if res, e := MetaAccess.MetaFor(t.gvr); e == nil && !res.Namespaced { if res, e := MetaAccess.MetaFor(t.gvr); e == nil && !res.Namespaced {

View File

@ -43,7 +43,7 @@ var Registry = map[string]ResourceMeta{
Renderer: new(render.Container), Renderer: new(render.Container),
TreeRenderer: new(xray.Container), TreeRenderer: new(xray.Container),
}, },
client.ScGVR.String(): { client.ScnGVR.String(): {
DAO: new(dao.ImageScan), DAO: new(dao.ImageScan),
Renderer: new(render.ImageScan), Renderer: new(render.ImageScan),
}, },

View File

@ -26,6 +26,9 @@ const initRefreshRate = 300 * time.Millisecond
// TableListener represents a table model listener. // TableListener represents a table model listener.
type TableListener interface { type TableListener interface {
// TableNoData notifies listener no data was found.
TableNoData(*model1.TableData)
// TableDataChanged notifies the model data changed. // TableDataChanged notifies the model data changed.
TableDataChanged(*model1.TableData) TableDataChanged(*model1.TableData)
@ -232,7 +235,12 @@ func (t *Table) refresh(ctx context.Context) error {
if err := t.reconcile(ctx); err != nil { if err := t.reconcile(ctx); err != nil {
return err return err
} }
t.fireTableChanged(t.Peek()) data := t.Peek()
if data.RowCount() == 0 {
t.fireNoData(data)
} else {
t.fireTableChanged(data)
}
return nil return nil
} }
@ -292,6 +300,17 @@ func (t *Table) fireTableChanged(data *model1.TableData) {
} }
} }
func (t *Table) fireNoData(data *model1.TableData) {
var ll []TableListener
t.mx.RLock()
ll = t.listeners
t.mx.RUnlock()
for _, l := range ll {
l.TableNoData(data)
}
}
func (t *Table) fireTableLoadFailed(err error) { func (t *Table) fireTableLoadFailed(err error) {
var ll []TableListener var ll []TableListener
t.mx.RLock() t.mx.RLock()

View File

@ -76,6 +76,8 @@ type tableListener struct {
count, errs int count, errs int
} }
func (*tableListener) TableNoData(*model1.TableData) {}
func (l *tableListener) TableDataChanged(*model1.TableData) { func (l *tableListener) TableDataChanged(*model1.TableData) {
l.count++ l.count++
} }

View File

@ -11,6 +11,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
@ -39,6 +40,7 @@ type Browser struct {
cancelFn context.CancelFunc cancelFn context.CancelFunc
mx sync.RWMutex mx sync.RWMutex
updating bool updating bool
firstView atomic.Int32
} }
// NewBrowser returns a new browser. // NewBrowser returns a new browser.
@ -279,6 +281,36 @@ func (b *Browser) Aliases() sets.Set[string] {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Model Protocol... // Model Protocol...
// TableNoData notifies view no data is available.
func (b *Browser) TableNoData(data *model1.TableData) {
var cancel context.CancelFunc
b.mx.RLock()
cancel = b.cancelFn
b.mx.RUnlock()
if !b.app.ConOK() || cancel == nil || !b.app.IsRunning() {
return
}
if b.firstView.Load() == 0 {
b.firstView.Add(1)
return
}
cdata := b.Update(data, b.app.Conn().HasMetrics())
b.app.QueueUpdateDraw(func() {
if b.getUpdating() {
return
}
b.setUpdating(true)
defer b.setUpdating(false)
if b.GetColumnCount() == 0 {
b.app.Flash().Warnf("No resources found for %s in namespace %s", b.GVR(), client.PrintNamespace(b.GetNamespace()))
}
b.refreshActions()
b.UpdateUI(cdata, data)
})
}
// TableDataChanged notifies view new data is available. // TableDataChanged notifies view new data is available.
func (b *Browser) TableDataChanged(data *model1.TableData) { func (b *Browser) TableDataChanged(data *model1.TableData) {
var cancel context.CancelFunc var cancel context.CancelFunc
@ -297,6 +329,9 @@ func (b *Browser) TableDataChanged(data *model1.TableData) {
} }
b.setUpdating(true) b.setUpdating(true)
defer b.setUpdating(false) defer b.setUpdating(false)
if b.GetColumnCount() == 0 {
b.app.Flash().Infof("Viewing %s in namespace %s", b.GVR(), client.PrintNamespace(b.GetNamespace()))
}
b.refreshActions() b.refreshActions()
b.UpdateUI(cdata, data) b.UpdateUI(cdata, data)
}) })
@ -488,7 +523,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil return nil
} }
b.setNamespace(ns) b.setNamespace(ns)
b.app.Flash().Infof("Viewing namespace `%s`...", ns) b.app.Flash().Infof("Viewing %s in namespace `%s`...", b.GVR(), client.PrintNamespace(ns))
b.refresh() b.refresh()
b.UpdateTitle() b.UpdateTitle()
b.SelectRow(1, 0, true) b.SelectRow(1, 0, true)
@ -528,7 +563,7 @@ func (b *Browser) defaultContext() context.Context {
} }
func (b *Browser) refreshActions() { func (b *Browser) refreshActions() {
if b.App().Content.Top() != nil && b.App().Content.Top().Name() != b.Name() { if top := b.App().Content.Top(); top != nil && top.Name() != b.Name() {
return return
} }
aa := ui.NewKeyActionsFromMap(ui.KeyMap{ aa := ui.NewKeyActionsFromMap(ui.KeyMap{

View File

@ -344,7 +344,6 @@ func (c *Command) exec(p *cmd.Interpreter, gvr *client.GVR, comp model.Component
} }
comp.SetCommand(p) comp.SetCommand(p)
c.app.Flash().Infof("Viewing %s...", gvr)
if clearStack { if clearStack {
cmd := contextRX.ReplaceAllString(p.GetLine(), "") cmd := contextRX.ReplaceAllString(p.GetLine(), "")
c.app.Config.SetActiveView(cmd) c.app.Config.SetActiveView(cmd)

View File

@ -69,7 +69,7 @@ func miscViewers(vv MetaViewers) {
vv[client.CoGVR] = MetaViewer{ vv[client.CoGVR] = MetaViewer{
viewerFn: NewContainer, viewerFn: NewContainer,
} }
vv[client.ScGVR] = MetaViewer{ vv[client.ScnGVR] = MetaViewer{
viewerFn: NewImageScan, viewerFn: NewImageScan,
} }
vv[client.PfGVR] = MetaViewer{ vv[client.PfGVR] = MetaViewer{

View File

@ -35,7 +35,7 @@ func (v *VulnerabilityExtender) bindKeys(aa *ui.KeyActions) {
} }
func (v *VulnerabilityExtender) showVulCmd(*tcell.EventKey) *tcell.EventKey { func (v *VulnerabilityExtender) showVulCmd(*tcell.EventKey) *tcell.EventKey {
isv := NewImageScan(client.ScGVR) isv := NewImageScan(client.ScnGVR)
isv.SetContextFn(v.selContext) isv.SetContextFn(v.selContext)
if err := v.App().inject(isv, false); err != nil { if err := v.App().inject(isv, false); err != nil {
v.App().Flash().Err(err) v.App().Flash().Err(err)

View File

@ -1,6 +1,6 @@
name: k9s name: k9s
base: core22 base: core22
version: 'v0.50.1' version: 'v0.50.2'
summary: K9s is a CLI to view and manage your Kubernetes clusters. summary: K9s is a CLI to view and manage your Kubernetes clusters.
description: | 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. 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.