Rel v0.50.6 (#3338)

* update deps

* add epslice support

* update pulses

* fix #3334

* rel notes
mine
Fernand Galiana 2025-05-11 23:16:37 -06:00 committed by GitHub
parent b68b68e23f
commit 13cb55bb66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 335 additions and 76 deletions

3
.gitignore vendored
View File

@ -24,4 +24,5 @@ demos
kind
*.snap
/stresser
__debug_bin*
__debug_bin*
fg.yaml

View File

@ -11,6 +11,8 @@ run:
tests: true
linters:
disable:
- staticcheck
enable:
- sloglint
- bodyclose

View File

@ -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 ?=

View File

@ -0,0 +1,39 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# 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
---
<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)

21
go.mod
View File

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

42
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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", "<none>"}, 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])
}

108
internal/render/eps.go Normal file
View File

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

View File

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

View File

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

View File

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

51
internal/render/testdata/eps.json vendored Normal file
View File

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

View File

@ -45,7 +45,7 @@ const (
UnknownValue = "<unknown>"
// UnsetValue represent an unset value.
UnsetValue = ""
UnsetValue = "<unset>"
// ZeroValue represents a zero value.
ZeroValue = "0"

View File

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