diff --git a/Makefile b/Makefile
index 99c87e6f..fdbdb0fe 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.50.1
+VERSION ?= v0.50.2
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}
diff --git a/change_logs/release_v0.50.2.md b/change_logs/release_v0.50.2.md
new file mode 100644
index 00000000..87b6befd
--- /dev/null
+++ b/change_logs/release_v0.50.2.md
@@ -0,0 +1,40 @@
+
+
+# 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??)
+
+---
+
© 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/client/helpers.go b/internal/client/helpers.go
index 116e6145..8f677bbe 100644
--- a/internal/client/helpers.go
+++ b/internal/client/helpers.go
@@ -22,6 +22,14 @@ func IsClusterWide(ns string) bool {
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.
func CleanseNamespace(ns string) string {
if IsAllNamespace(ns) {
diff --git a/internal/config/alias.go b/internal/config/alias.go
index 2f506ff6..275effd1 100644
--- a/internal/config/alias.go
+++ b/internal/config/alias.go
@@ -84,16 +84,14 @@ func (a *Aliases) Resolve(command string) (*client.GVR, string, bool) {
if !ok {
return nil, "", false
}
- if agvr.IsCommand() {
- p := cmd.NewInterpreter(agvr.String())
- gvr, ok := a.Get(p.Cmd())
- if !ok {
- return nil, "", false
- }
- return gvr, p.Args(), true
+
+ p := cmd.NewInterpreter(agvr.String())
+ gvr, ok := a.Get(p.Cmd())
+ if !ok {
+ return agvr, "", true
}
- return agvr, "", true
+ return gvr, p.Args(), true
}
// Get retrieves an alias.
diff --git a/internal/dao/alias_test.go b/internal/dao/alias_test.go
index 7cd41f9a..6738f5df 100644
--- a/internal/dao/alias_test.go
+++ b/internal/dao/alias_test.go
@@ -18,10 +18,13 @@ import (
func TestAsGVR(t *testing.T) {
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.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 {
cmd string
@@ -29,40 +32,59 @@ func TestAsGVR(t *testing.T) {
gvr *client.GVR
exp string
}{
- "ok": {
+ "gvr": {
+ cmd: "v1/pods",
+ ok: true,
+ gvr: client.PodGVR,
+ },
+
+ "r": {
cmd: "pods",
ok: true,
gvr: client.PodGVR,
},
- "ok-short": {
+ "alias1": {
cmd: "po",
ok: true,
gvr: client.PodGVR,
},
+ "alias-2": {
+ cmd: "pipo",
+ ok: true,
+ gvr: client.PodGVR,
+ },
+
"missing": {
cmd: "zorg",
},
- "alias": {
+ "no-args": {
cmd: "wkl",
ok: true,
gvr: client.WkGVR,
},
- "ns-alias": {
+ "ns-arg": {
cmd: "pp",
ok: true,
gvr: client.PodGVR,
exp: "default",
},
+ "ns-inception": {
+ cmd: "ppo",
+ ok: true,
+ gvr: client.PodGVR,
+ exp: "default",
+ },
+
"full-alias": {
cmd: "ppc",
ok: true,
gvr: client.PodGVR,
- exp: "default @fred",
+ exp: "default app=fred @fred",
},
}
diff --git a/internal/dao/helpers.go b/internal/dao/helpers.go
index 1bf6531b..5e280107 100644
--- a/internal/dao/helpers.go
+++ b/internal/dao/helpers.go
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"log/slog"
+ "maps"
"math"
"github.com/derailed/k9s/internal/client"
@@ -85,15 +86,22 @@ func ToYAML(o runtime.Object, showManaged bool) (string, error) {
if o == nil {
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 (
buff bytes.Buffer
p printers.YAMLPrinter
)
if !showManaged {
- o = o.DeepCopyObject()
- uo := o.(*unstructured.Unstructured).Object
- if meta, ok := uo["metadata"].(map[string]any); ok {
+ mm = maps.Clone(mm)
+ if meta, ok := mm["metadata"].(map[string]any); ok {
delete(meta, "managedFields")
}
}
diff --git a/internal/dao/registry.go b/internal/dao/registry.go
index bd9e5edf..3471240d 100644
--- a/internal/dao/registry.go
+++ b/internal/dao/registry.go
@@ -331,6 +331,9 @@ func loadPreferred(f Factory, m ResourceMetas) error {
if !isStandardGroup(r.GroupVersion) {
res.Categories = append(res.Categories, crdCat)
}
+ if isScalable(gvr) {
+ res.Categories = append(res.Categories, scaleCat)
+ }
m[gvr] = &res
}
}
@@ -342,6 +345,12 @@ func isStandardGroup(gv string) bool {
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(
client.NewGVR("v1/events"),
client.NewGVR("extensions/v1beta1/ingresses"),
diff --git a/internal/dao/table.go b/internal/dao/table.go
index 0952dc89..7b0240fe 100644
--- a/internal/dao/table.go
+++ b/internal/dao/table.go
@@ -6,12 +6,9 @@ package dao
import (
"context"
"fmt"
- "log/slog"
- "time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
- "github.com/derailed/k9s/internal/slogs"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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 {
return nil, err
}
- ti := time.Now()
o, err := c.Get().
SetHeader("Accept", header).
Param("includeObject", includeObject).
@@ -86,7 +82,6 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
if err != nil {
return nil, err
}
- slog.Debug("Q Time", slogs.Elapsed, time.Since(ti))
namespaced := true
if res, e := MetaAccess.MetaFor(t.gvr); e == nil && !res.Namespaced {
diff --git a/internal/model/registry.go b/internal/model/registry.go
index d9bb7682..f62a6c3b 100644
--- a/internal/model/registry.go
+++ b/internal/model/registry.go
@@ -43,7 +43,7 @@ var Registry = map[string]ResourceMeta{
Renderer: new(render.Container),
TreeRenderer: new(xray.Container),
},
- client.ScGVR.String(): {
+ client.ScnGVR.String(): {
DAO: new(dao.ImageScan),
Renderer: new(render.ImageScan),
},
diff --git a/internal/model/table.go b/internal/model/table.go
index 06a9827a..5283c401 100644
--- a/internal/model/table.go
+++ b/internal/model/table.go
@@ -26,6 +26,9 @@ const initRefreshRate = 300 * time.Millisecond
// TableListener represents a table model listener.
type TableListener interface {
+ // TableNoData notifies listener no data was found.
+ TableNoData(*model1.TableData)
+
// TableDataChanged notifies the model data changed.
TableDataChanged(*model1.TableData)
@@ -232,7 +235,12 @@ func (t *Table) refresh(ctx context.Context) error {
if err := t.reconcile(ctx); err != nil {
return err
}
- t.fireTableChanged(t.Peek())
+ data := t.Peek()
+ if data.RowCount() == 0 {
+ t.fireNoData(data)
+ } else {
+ t.fireTableChanged(data)
+ }
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) {
var ll []TableListener
t.mx.RLock()
diff --git a/internal/model/table_test.go b/internal/model/table_test.go
index a877c5b8..a883af5e 100644
--- a/internal/model/table_test.go
+++ b/internal/model/table_test.go
@@ -76,6 +76,8 @@ type tableListener struct {
count, errs int
}
+func (*tableListener) TableNoData(*model1.TableData) {}
+
func (l *tableListener) TableDataChanged(*model1.TableData) {
l.count++
}
diff --git a/internal/view/browser.go b/internal/view/browser.go
index 92dd4009..9f8ba17a 100644
--- a/internal/view/browser.go
+++ b/internal/view/browser.go
@@ -11,6 +11,7 @@ import (
"strconv"
"strings"
"sync"
+ "sync/atomic"
"time"
"github.com/derailed/k9s/internal"
@@ -39,6 +40,7 @@ type Browser struct {
cancelFn context.CancelFunc
mx sync.RWMutex
updating bool
+ firstView atomic.Int32
}
// NewBrowser returns a new browser.
@@ -279,6 +281,36 @@ func (b *Browser) Aliases() sets.Set[string] {
// ----------------------------------------------------------------------------
// 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.
func (b *Browser) TableDataChanged(data *model1.TableData) {
var cancel context.CancelFunc
@@ -297,6 +329,9 @@ func (b *Browser) TableDataChanged(data *model1.TableData) {
}
b.setUpdating(true)
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.UpdateUI(cdata, data)
})
@@ -488,7 +523,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
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.UpdateTitle()
b.SelectRow(1, 0, true)
@@ -528,7 +563,7 @@ func (b *Browser) defaultContext() context.Context {
}
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
}
aa := ui.NewKeyActionsFromMap(ui.KeyMap{
diff --git a/internal/view/command.go b/internal/view/command.go
index 2e746477..9b3cacf0 100644
--- a/internal/view/command.go
+++ b/internal/view/command.go
@@ -344,7 +344,6 @@ func (c *Command) exec(p *cmd.Interpreter, gvr *client.GVR, comp model.Component
}
comp.SetCommand(p)
- c.app.Flash().Infof("Viewing %s...", gvr)
if clearStack {
cmd := contextRX.ReplaceAllString(p.GetLine(), "")
c.app.Config.SetActiveView(cmd)
diff --git a/internal/view/registrar.go b/internal/view/registrar.go
index 655a4db6..b540183b 100644
--- a/internal/view/registrar.go
+++ b/internal/view/registrar.go
@@ -69,7 +69,7 @@ func miscViewers(vv MetaViewers) {
vv[client.CoGVR] = MetaViewer{
viewerFn: NewContainer,
}
- vv[client.ScGVR] = MetaViewer{
+ vv[client.ScnGVR] = MetaViewer{
viewerFn: NewImageScan,
}
vv[client.PfGVR] = MetaViewer{
diff --git a/internal/view/vul_extender.go b/internal/view/vul_extender.go
index fa6f3ca8..6c8c7eb8 100644
--- a/internal/view/vul_extender.go
+++ b/internal/view/vul_extender.go
@@ -35,7 +35,7 @@ func (v *VulnerabilityExtender) bindKeys(aa *ui.KeyActions) {
}
func (v *VulnerabilityExtender) showVulCmd(*tcell.EventKey) *tcell.EventKey {
- isv := NewImageScan(client.ScGVR)
+ isv := NewImageScan(client.ScnGVR)
isv.SetContextFn(v.selContext)
if err := v.App().inject(isv, false); err != nil {
v.App().Flash().Err(err)
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 2dff457e..551c1973 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,6 +1,6 @@
name: k9s
base: core22
-version: 'v0.50.1'
+version: 'v0.50.2'
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.