diff --git a/.gitignore b/.gitignore
index 25be190a..abff12a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,5 @@ demos
kind
*.snap
/stresser
-__debug_bin*
\ No newline at end of file
+__debug_bin*
+fg.yaml
\ No newline at end of file
diff --git a/.golangci.yml b/.golangci.yml
index 72fdf6e6..da766b8b 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -11,6 +11,8 @@ run:
tests: true
linters:
+ disable:
+ - staticcheck
enable:
- sloglint
- bodyclose
diff --git a/Makefile b/Makefile
index bfb1ce21..d193f5bd 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
NAME := k9s
-VERSION ?= v0.50.5
+VERSION ?= v0.50.6
PACKAGE := github.com/derailed/$(NAME)
OUTPUT_BIN ?= execs/${NAME}
GO_FLAGS ?=
diff --git a/change_logs/release_v0.50.6.md b/change_logs/release_v0.50.6.md
new file mode 100644
index 00000000..a3fd8274
--- /dev/null
+++ b/change_logs/release_v0.50.6.md
@@ -0,0 +1,39 @@
+
+
+# Release v0.50.6
+
+## 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/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)
+
+## Maintenance Release!
+
+---
+
+## Resolved Issues
+
+* [#3334](https://github.com/derailed/k9s/issues/3334) Watcher failed for events.k8s.io/v1/events -- expecting a meta table but got *unstructured.Unstructure
+
+---
+
+## 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!!
+
+* [#3332](https://github.com/derailed/k9s/pull/3332) fix: pre-check for get permissions only on port-forward
+* [#3311](https://github.com/derailed/k9s/pull/3311) Fix concurrent read writes
+* [#3310](https://github.com/derailed/k9s/pull/3310) fix: use full path of date to avoid conflict
+
+---
+
© 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/go.mod b/go.mod
index 5dd573c4..f5991e6a 100644
--- a/go.mod
+++ b/go.mod
@@ -30,15 +30,14 @@ require (
golang.org/x/text v0.24.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.17.3
- k8s.io/api v0.32.3
- k8s.io/apiextensions-apiserver v0.32.3
+ k8s.io/api v0.33.0
+ k8s.io/apiextensions-apiserver v0.33.0
k8s.io/apimachinery v0.33.0
- k8s.io/cli-runtime v0.32.3
- k8s.io/client-go v0.32.3
+ k8s.io/cli-runtime v0.33.0
+ k8s.io/client-go v0.33.0
k8s.io/klog/v2 v2.130.1
- k8s.io/kubectl v0.32.3
- k8s.io/kubernetes v1.33.0
- k8s.io/metrics v0.32.3
+ k8s.io/kubectl v0.33.0
+ k8s.io/metrics v0.33.0
sigs.k8s.io/yaml v1.4.0
)
@@ -171,7 +170,6 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/gohugoio/hashstructure v0.5.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
- github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
@@ -330,7 +328,7 @@ require (
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect
- golang.org/x/oauth2 v0.27.0 // indirect
+ golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
@@ -348,8 +346,9 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gorm.io/gorm v1.25.12 // indirect
- k8s.io/apiserver v0.32.3 // indirect
- k8s.io/component-base v0.32.3 // indirect
+ k8s.io/apiserver v0.33.0 // indirect
+ k8s.io/component-base v0.33.0 // indirect
+ k8s.io/component-helpers v0.33.0 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
modernc.org/libc v1.62.1 // indirect
diff --git a/go.sum b/go.sum
index 62365371..58c37e67 100644
--- a/go.sum
+++ b/go.sum
@@ -1148,8 +1148,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
-github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
@@ -2006,8 +2004,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
-golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
-golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
+golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -2596,30 +2594,30 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
-k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
-k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
-k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY=
-k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss=
+k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
+k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
+k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
+k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
-k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8=
-k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc=
-k8s.io/cli-runtime v0.32.3 h1:khLF2ivU2T6Q77H97atx3REY9tXiA3OLOjWJxUrdvss=
-k8s.io/cli-runtime v0.32.3/go.mod h1:vZT6dZq7mZAca53rwUfdFSZjdtLyfF61mkf/8q+Xjak=
-k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
-k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
-k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k=
-k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI=
+k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc=
+k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8=
+k8s.io/cli-runtime v0.33.0 h1:Lbl/pq/1o8BaIuyn+aVLdEPHVN665tBAXUePs8wjX7c=
+k8s.io/cli-runtime v0.33.0/go.mod h1:QcA+r43HeUM9jXFJx7A+yiTPfCooau/iCcP1wQh4NFw=
+k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
+k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
+k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk=
+k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU=
+k8s.io/component-helpers v0.33.0 h1:0AdW0A0mIgljLgtG0hJDdJl52PPqTrtMgOgtm/9i/Ys=
+k8s.io/component-helpers v0.33.0/go.mod h1:9SRiXfLldPw9lEEuSsapMtvT8j/h1JyFFapbtybwKvU=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
-k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI=
-k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg=
-k8s.io/kubernetes v1.33.0 h1:BP5Y5yIzUZVeBuE/ESZvnw6TNxjXbLsCckIkljE+R0U=
-k8s.io/kubernetes v1.33.0/go.mod h1:2nWuPk0seE4+6sd0x60wQ6rYEXcV7SoeMbU0YbFm/5k=
-k8s.io/metrics v0.32.3 h1:2vsBvw0v8rIIlczZ/lZ8Kcqk9tR6Fks9h+dtFNbc2a4=
-k8s.io/metrics v0.32.3/go.mod h1:9R1Wk5cb+qJpCQon9h52mgkVCcFeYxcY+YkumfwHVCU=
+k8s.io/kubectl v0.33.0 h1:HiRb1yqibBSCqic4pRZP+viiOBAnIdwYDpzUFejs07g=
+k8s.io/kubectl v0.33.0/go.mod h1:gAlGBuS1Jq1fYZ9AjGWbI/5Vk3M/VW2DK4g10Fpyn/0=
+k8s.io/metrics v0.33.0 h1:sKe5sC9qb1RakMhs8LWYNuN2ne6OTCWexj8Jos3rO2Y=
+k8s.io/metrics v0.33.0/go.mod h1:XewckTFXmE2AJiP7PT3EXaY7hi7bler3t2ZLyOdQYzU=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
diff --git a/internal/client/gvrs.go b/internal/client/gvrs.go
index ada91c79..c4a3f912 100644
--- a/internal/client/gvrs.go
+++ b/internal/client/gvrs.go
@@ -20,6 +20,9 @@ var (
NodeGVR = NewGVR("v1/nodes")
SvcGVR = NewGVR("v1/services")
+ // Discovery...
+ EpsGVR = NewGVR("discovery.k8s.io/v1/endpointslices")
+
// Autoscaling...
HpaGVR = NewGVR("autoscaling/v1/horizontalpodautoscalers")
diff --git a/internal/model/pulse.go b/internal/model/pulse.go
index a36c6af6..1916937a 100644
--- a/internal/model/pulse.go
+++ b/internal/model/pulse.go
@@ -3,7 +3,6 @@ package model
import (
"context"
"fmt"
- "time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
@@ -11,14 +10,12 @@ import (
"github.com/derailed/k9s/internal/health"
)
-const defaultRefreshRate = 1 * time.Minute
-
// PulseListener represents a health model listener.
type PulseListener interface {
// PulseChanged notifies the model data changed.
PulseChanged(*health.Check)
- // TreeFailed notifies the health check failed.
+ // PulseFailed notifies the health check failed.
PulseFailed(error)
// MetricsChanged update metrics time series.
@@ -27,18 +24,16 @@ type PulseListener interface {
// Pulse tracks multiple resources health.
type Pulse struct {
- gvr *client.GVR
- namespace string
- listeners []PulseListener
- refreshRate time.Duration
- health *PulseHealth
+ gvr *client.GVR
+ namespace string
+ listeners []PulseListener
+ health *PulseHealth
}
// NewPulse returns a new pulse.
func NewPulse(gvr *client.GVR) *Pulse {
return &Pulse{
- gvr: gvr,
- refreshRate: defaultRefreshRate,
+ gvr: gvr,
}
}
diff --git a/internal/model/pulse_health.go b/internal/model/pulse_health.go
index b43251d3..b2341c81 100644
--- a/internal/model/pulse_health.go
+++ b/internal/model/pulse_health.go
@@ -10,6 +10,8 @@ import (
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/slogs"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
)
const pulseRate = 15 * time.Second
@@ -96,6 +98,7 @@ func (h *PulseHealth) Watch(ctx context.Context, ns string) HealthChan {
}
func (h *PulseHealth) checkPulse(ctx context.Context, ns string, c HealthChan) error {
+ slog.Debug("Checking pulses...")
for _, gvr := range PulseGVRs {
check, err := h.check(ctx, ns, gvr)
if err != nil {
@@ -110,8 +113,8 @@ func (h *PulseHealth) check(ctx context.Context, ns string, gvr *client.GVR) (He
meta, ok := Registry[gvr]
if !ok {
meta = ResourceMeta{
- DAO: &dao.Table{},
- Renderer: &render.Generic{},
+ DAO: new(dao.Table),
+ Renderer: new(render.Table),
}
}
if meta.DAO == nil {
@@ -124,11 +127,31 @@ func (h *PulseHealth) check(ctx context.Context, ns string, gvr *client.GVR) (He
return HealthPoint{}, err
}
c := HealthPoint{GVR: gvr, Total: len(oo)}
- for _, o := range oo {
- if err := meta.Renderer.Healthy(ctx, o); err != nil {
- c.Faults++
+ if isTable(oo) {
+ ta := oo[0].(*metav1.Table)
+ c.Total = len(ta.Rows)
+ for _, row := range ta.Rows {
+ if err := meta.Renderer.Healthy(ctx, row); err != nil {
+ c.Faults++
+ }
+ }
+ } else {
+ for _, o := range oo {
+ if err := meta.Renderer.Healthy(ctx, o); err != nil {
+ c.Faults++
+ }
}
}
+ slog.Debug("Checked", slogs.GVR, gvr, slogs.Config, c)
return c, nil
}
+
+func isTable(oo []runtime.Object) bool {
+ if len(oo) == 0 || len(oo) > 1 {
+ return false
+ }
+ _, ok := oo[0].(*metav1.Table)
+
+ return ok
+}
diff --git a/internal/model/registry.go b/internal/model/registry.go
index 193de099..0a393bea 100644
--- a/internal/model/registry.go
+++ b/internal/model/registry.go
@@ -84,6 +84,11 @@ var Registry = map[*client.GVR]ResourceMeta{
Renderer: new(render.Alias),
},
+ // Discovery...
+ client.EpsGVR: {
+ Renderer: new(render.EndpointSlice),
+ },
+
// Core...
client.EpGVR: {
Renderer: new(render.Endpoints),
@@ -124,6 +129,7 @@ var Registry = map[*client.GVR]ResourceMeta{
Renderer: new(render.PersistentVolumeClaim),
},
client.EvGVR: {
+ DAO: new(dao.Table),
Renderer: new(render.Event),
},
diff --git a/internal/model1/table_data.go b/internal/model1/table_data.go
index 8b6c516f..0ddaa8e1 100644
--- a/internal/model1/table_data.go
+++ b/internal/model1/table_data.go
@@ -272,7 +272,7 @@ func (t *TableData) Render(_ context.Context, r Renderer, oo []runtime.Object) e
t.Update(rows)
t.SetHeader(t.namespace, r.Header(t.namespace))
if t.HeaderCount() == 0 {
- return fmt.Errorf("fail to list resource %s", t.gvr)
+ return fmt.Errorf("no data found for resource %s", t.gvr)
}
return nil
diff --git a/internal/port/pfs.go b/internal/port/pfs.go
index 9dd9e760..533f3e46 100644
--- a/internal/port/pfs.go
+++ b/internal/port/pfs.go
@@ -40,7 +40,7 @@ func (aa PFAnns) ToTunnels(address string, _ ContainerPortSpecs, available PortC
return pts, err
}
if !available(pt) {
- return pts, fmt.Errorf("Port %s is not available on host", pt.LocalPort)
+ return pts, fmt.Errorf("port %s is not available on host", pt.LocalPort)
}
pts = append(pts, pt)
}
diff --git a/internal/render/ep_test.go b/internal/render/ep_test.go
index f9d618b3..e65667cf 100644
--- a/internal/render/ep_test.go
+++ b/internal/render/ep_test.go
@@ -17,6 +17,6 @@ func TestEndpointsRender(t *testing.T) {
r := model1.NewRow(4)
require.NoError(t, c.Render(load(t, "ep"), "", &r))
- assert.Equal(t, "default/dictionary1", r.ID)
- assert.Equal(t, model1.Fields{"default", "dictionary1", ""}, r.Fields[:3])
+ assert.Equal(t, "ns-1/blee", r.ID)
+ assert.Equal(t, model1.Fields{"ns-1", "blee", "10.0.0.67:8080"}, r.Fields[:3])
}
diff --git a/internal/render/eps.go b/internal/render/eps.go
new file mode 100644
index 00000000..4bdcc72f
--- /dev/null
+++ b/internal/render/eps.go
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package render
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
+ discoveryv1 "k8s.io/api/discovery/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime"
+)
+
+var defaultEPsHeader = model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "ADDRESSTYPE"},
+ model1.HeaderColumn{Name: "PORTS"},
+ model1.HeaderColumn{Name: "ENDPOINTS"},
+ model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
+}
+
+// EndpointSlice renders a K8s EndpointSlice to screen.
+type EndpointSlice struct {
+ Base
+}
+
+// Header returns a header row.
+func (e EndpointSlice) Header(_ string) model1.Header {
+ return e.doHeader(defaultEPsHeader)
+}
+
+// Render renders a K8s resource to screen.
+func (e EndpointSlice) Render(o any, ns string, row *model1.Row) error {
+ if err := e.defaultRow(o, ns, row); err != nil {
+ return err
+ }
+ if e.specs.isEmpty() {
+ return nil
+ }
+ cols, err := e.specs.realize(o.(*unstructured.Unstructured), defaultEPsHeader, row)
+ if err != nil {
+ return err
+ }
+ cols.hydrateRow(row)
+
+ return nil
+}
+
+func (e EndpointSlice) defaultRow(o any, ns string, r *model1.Row) error {
+ raw, ok := o.(*unstructured.Unstructured)
+ if !ok {
+ return fmt.Errorf("expected Unstructured, but got %T", o)
+ }
+ var eps discoveryv1.EndpointSlice
+ err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &eps)
+ if err != nil {
+ return err
+ }
+
+ r.ID = client.MetaFQN(&eps.ObjectMeta)
+ r.Fields = make(model1.Fields, 0, len(e.Header(ns)))
+ r.Fields = model1.Fields{
+ eps.Namespace,
+ eps.Name,
+ string(eps.AddressType),
+ toPorts(eps.Ports),
+ toEPss(eps.Endpoints),
+ ToAge(eps.GetCreationTimestamp()),
+ }
+
+ return nil
+}
+
+// ----------------------------------------------------------------------------
+// Helpers...
+
+func toEPss(ee []discoveryv1.Endpoint) string {
+ if len(ee) == 0 {
+ return UnsetValue
+ }
+
+ aa := make([]string, 0, len(ee))
+ for _, e := range ee {
+ aa = append(aa, e.Addresses...)
+ }
+
+ return strings.Join(aa, ",")
+}
+
+func toPorts(ee []discoveryv1.EndpointPort) string {
+ if len(ee) == 0 {
+ return UnsetValue
+ }
+
+ aa := make([]string, 0, len(ee))
+ for _, e := range ee {
+ if e.Port != nil {
+ aa = append(aa, strconv.Itoa(int(*e.Port)))
+ }
+ }
+
+ return strings.Join(aa, ",")
+}
diff --git a/internal/render/eps_test.go b/internal/render/eps_test.go
new file mode 100644
index 00000000..bb4828be
--- /dev/null
+++ b/internal/render/eps_test.go
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package render_test
+
+import (
+ "testing"
+
+ "github.com/derailed/k9s/internal/model1"
+ "github.com/derailed/k9s/internal/render"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestEndpointSliceRender(t *testing.T) {
+ c := render.EndpointSlice{}
+ r := model1.NewRow(4)
+
+ require.NoError(t, c.Render(load(t, "eps"), "", &r))
+ assert.Equal(t, "blee/fred", r.ID)
+ assert.Equal(t, model1.Fields{"blee", "fred", "IPv4", "4244", "172.20.0.2,172.20.0.3"}, r.Fields[:5])
+}
diff --git a/internal/render/ev.go b/internal/render/ev.go
index ec7791b0..e5e2f701 100644
--- a/internal/render/ev.go
+++ b/internal/render/ev.go
@@ -8,9 +8,7 @@ import (
"log/slog"
"github.com/derailed/k9s/internal/slogs"
- "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/apimachinery/pkg/runtime"
- api "k8s.io/kubernetes/pkg/apis/core"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Event renders a event resource to screen.
@@ -20,19 +18,14 @@ type Event struct {
// Healthy checks component health.
func (*Event) Healthy(_ context.Context, o any) error {
- u, ok := o.(*unstructured.Unstructured)
+ r, ok := o.(metav1.TableRow)
if !ok {
- slog.Error("Expected Unstructured", slogs.Type, fmt.Sprintf("%T", o))
+ slog.Error("Expected TableRow", slogs.Type, fmt.Sprintf("%T", o))
return nil
}
- var ev api.Event
- err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &ev)
- if err != nil {
- slog.Error("Failed to convert unstructured to Node", slogs.Error, err)
- return nil
- }
- if ev.Type != "Normal" {
- return fmt.Errorf("event is not normal: %s", ev.Type)
+ idx := 2
+ if idx < len(r.Cells) && r.Cells[idx] != "Normal" {
+ return fmt.Errorf("event is not normal: %s", r.Cells[idx])
}
return nil
diff --git a/internal/render/testdata/ep.json b/internal/render/testdata/ep.json
index 659472af..f0e8e24f 100644
--- a/internal/render/testdata/ep.json
+++ b/internal/render/testdata/ep.json
@@ -3,10 +3,29 @@
"kind": "Endpoints",
"metadata": {
"creationTimestamp": "2019-07-10T23:10:43Z",
- "name": "dictionary1",
- "namespace": "default",
- "resourceVersion": "36684456",
- "selfLink": "/api/v1/namespaces/default/endpoints/dictionary1",
- "uid": "f119c74f-a367-11e9-990f-42010a800218"
- }
+ "name": "blee",
+ "namespace": "ns-1"
+ },
+ "subsets": [
+ {
+ "addresses": [
+ {
+ "ip": "10.0.0.67",
+ "nodeName": "n-1",
+ "targetRef": {
+ "kind": "Pod",
+ "name": "blah",
+ "namespace": "blee"
+ }
+ }
+ ],
+ "ports": [
+ {
+ "name": "http",
+ "port": 8080,
+ "protocol": "TCP"
+ }
+ ]
+ }
+]
}
\ No newline at end of file
diff --git a/internal/render/testdata/eps.json b/internal/render/testdata/eps.json
new file mode 100644
index 00000000..8f4366e5
--- /dev/null
+++ b/internal/render/testdata/eps.json
@@ -0,0 +1,51 @@
+{
+ "apiVersion": "discovery.k8s.io/v1",
+ "kind": "EndpointSlice",
+ "metadata": {
+ "creationTimestamp": "2025-04-17T22:14:13Z",
+ "name": "fred",
+ "namespace": "blee"
+ },
+ "addressType": "IPv4",
+ "endpoints": [
+ {
+ "addresses": [
+ "172.20.0.2"
+ ],
+ "conditions": {
+ "ready": true,
+ "serving": true,
+ "terminating": false
+ },
+ "nodeName": "n-1",
+ "targetRef": {
+ "kind": "Pod",
+ "name": "zorg",
+ "namespace": "kube-system"
+ }
+ },
+ {
+ "addresses": [
+ "172.20.0.3"
+ ],
+ "conditions": {
+ "ready": true,
+ "serving": true,
+ "terminating": false
+ },
+ "nodeName": "n-1",
+ "targetRef": {
+ "kind": "Pod",
+ "name": "zorg",
+ "namespace": "kube-system"
+ }
+ }
+ ],
+ "ports": [
+ {
+ "name": "peer-service",
+ "port": 4244,
+ "protocol": "TCP"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/internal/render/types.go b/internal/render/types.go
index 3b1c37ae..9d23756c 100644
--- a/internal/render/types.go
+++ b/internal/render/types.go
@@ -45,7 +45,7 @@ const (
UnknownValue = ""
// UnsetValue represent an unset value.
- UnsetValue = ""
+ UnsetValue = ""
// ZeroValue represents a zero value.
ZeroValue = "0"
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 70ef57f0..35435935 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,6 +1,6 @@
name: k9s
base: core22
-version: 'v0.50.5'
+version: 'v0.50.6'
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.