Rel v0.40.4 (#3130)

* fix#3119 - non alpha cust col

* Address issues:

o fix#3118 - wrong res shown on alias
o fix#3120 - shuffled cols after sort

* fix#3122 - view event not sorted

* rel v0.40.4
mine
Fernand Galiana 2025-02-17 23:27:56 -07:00 committed by GitHub
parent f8f112933d
commit e44c223e91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 160 additions and 106 deletions

View File

@ -1,7 +1,7 @@
# -----------------------------------------------------------------------------
# The base image for building the k9s binary
FROM golang:1.23.6-alpine3.20 AS build
FROM golang:1.24.0-alpine3.21 AS build
WORKDIR /k9s
COPY go.mod go.sum main.go Makefile ./
@ -13,8 +13,8 @@ RUN apk --no-cache add --update make libx11-dev git gcc libc-dev curl \
# -----------------------------------------------------------------------------
# Build the final Docker image
FROM alpine:3.21.2
ARG KUBECTL_VERSION="v1.31.2"
FROM alpine:3.21.3
ARG KUBECTL_VERSION="v1.32.2"
COPY --from=build /k9s/execs/k9s /bin/k9s
RUN apk --no-cache add --update ca-certificates \

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.3
VERSION ?= v0.40.4
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -44,6 +44,8 @@ Your donations will go a long way in keeping our servers lights on and beers in
## Demo Videos/Recordings
* [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)
* [K9s v0.29.0](https://youtu.be/oiU3wmoAkBo)
@ -640,6 +642,11 @@ The new column syntax is as follows:
Where `:json_parse_expression` represents an expression to pull a specific snippet out of the resource manifest.
Similar to `kubectl -o custom-columns` command. This expression is optional.
> IMPORTANT! Columns must be valid YAML strings. Thus if your column definition contains non-alpha chars
> they must figure with either single/double quotes or escaped via `\`
> NOTE! Be sure to watch k9s logs as any issues with the custom views specification are only surfaced in the logs.
Additionally, you can specify column attributes to further tailor the column rendering.
To use this you will need to add a `|` indicator followed by your rendering bits.
You can have one or more of the following attributes:

View File

@ -0,0 +1,56 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# Release v0.40.4
## 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!
😳 Aye! Continued Buzz kill on the 0.40.0 aftermath 🙀 👻
Likely additional `disturbance in the farce` might be observed.
Thank you all for giving this drop a rinse and reporting back!! 😍
---
## 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
* [#3122](https://github.com/derailed/k9s/issues/3122) Viewing events is no longer sorted by LAST SEEN
* [#3120](https://github.com/derailed/k9s/issues/3120) Custom View Column Mismatch in K9s: Shuffled Values in Pods View
* [#3119](https://github.com/derailed/k9s/issues/3119) Custom Views Fail to Load with % in Column Names
* [#3118](https://github.com/derailed/k9s/issues/3118) selecting an alias, the wrong resources are being shown
---
## 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!!
* [#3123](https://github.com/derailed/k9s/pull/3123) update regex to allow '%' and '/' in column names
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -68,6 +68,10 @@ func (g GVR) FQN(n string) string {
// AsResourceName returns a resource . separated descriptor in the shape of kind.version.group.
func (g GVR) AsResourceName() string {
if g.g == "" {
return g.r
}
return g.r + "." + g.v + "." + g.g
}

View File

@ -108,9 +108,9 @@ func TestGVRAsResourceName(t *testing.T) {
e string
}{
"full": {"apps/v1/deployments", "deployments.v1.apps"},
"core": {"v1/pods", "pods.v1."},
"k9s": {"users", "users.."},
"empty": {"", ".."},
"core": {"v1/pods", "pods"},
"k9s": {"users", "users"},
"empty": {"", ""},
}
for k := range uu {

View File

@ -108,10 +108,9 @@ func (a *Aliases) Define(gvr string, aliases ...string) {
}
for _, alias := range aliases {
if _, ok := a.Alias[alias]; ok {
continue
if _, ok := a.Alias[alias]; !ok {
a.Alias[alias] = gvr
}
a.Alias[alias] = gvr
}
}

View File

@ -116,9 +116,14 @@ func (a *Alias) load(path string) error {
continue
}
a.Define(gvrStr, strings.ToLower(meta.Kind), meta.Name)
if meta.SingularName != "" {
a.Define(gvrStr, meta.SingularName)
a.Define(gvrStr, gvr.AsResourceName())
// Allow single shot commands for k8s resources only!
if isStandardGroup(gvr.String()) {
a.Define(gvrStr, strings.ToLower(meta.Kind), meta.Name)
if meta.SingularName != "" {
a.Define(gvrStr, meta.SingularName)
}
}
if meta.ShortNames != nil {
a.Define(gvrStr, meta.ShortNames...)

View File

@ -41,7 +41,7 @@ func (d *Dynamic) List(ctx context.Context, ns string) ([]runtime.Object, error)
func (d *Dynamic) toTable(ctx context.Context, fqn string) ([]runtime.Object, error) {
strLabel, _ := ctx.Value(internal.KeyLabels).(string)
opts := []string{d.gvr.R()}
opts := []string{d.gvr.AsResourceName()}
ns, n := client.Namespaced(fqn)
if n != "" {
opts = append(opts, n)

View File

@ -6,6 +6,8 @@ package model1
import (
"fmt"
"sort"
"github.com/rs/zerolog/log"
)
type ReRangeFn func(int, RowEvent) bool
@ -272,6 +274,14 @@ func (r *RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity,
r.reindex()
}
// For debguging...
func (re RowEvents) Dump(msg string) {
log.Debug().Msg(msg)
for _, r := range re.events {
log.Debug().Msgf("!!YO!! %#v", r)
}
}
// ----------------------------------------------------------------------------
// RowEventSorter sorts row events by a given colon.

View File

@ -20,16 +20,19 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
type (
// SortFn represent a function that can sort columnar data.
SortFn func(rows Rows, sortCol SortColumn)
// SortFn represent a function that can sort columnar data.
type SortFn func(rows Rows, sortCol SortColumn)
// SortColumn represents a sortable column.
SortColumn struct {
Name string
ASC bool
}
)
// SortColumn represents a sortable column.
type SortColumn struct {
Name string
ASC bool
}
// IsSet checks if the sort column is set.
func (s SortColumn) IsSet() bool {
return s.Name != ""
}
const spacer = " "
@ -174,8 +177,11 @@ func (t *TableData) rxFilter(q string, inverse bool) (*RowEvents, error) {
rr := NewRowEvents(t.RowCount() / 2)
ageIndex, _ := t.header.IndexOf("AGE", true)
t.rowEvents.Range(func(_ int, re RowEvent) bool {
ff := re.Row.Fields[startIndex:]
if ageIndex >= 0 && ageIndex+1 <= len(ff) {
ff := make([]string, 0, len(re.Row.Fields))
for _, r := range re.Row.Fields[startIndex:] {
ff = append(ff, r)
}
if ageIndex >= 0 && startIndex != ageIndex && ageIndex+1 <= len(ff) {
ff = append(ff[0:ageIndex], ff[ageIndex+1:]...)
}
match := rx.MatchString(strings.Join(ff, spacer))
@ -321,22 +327,25 @@ func (t *TableData) Labelize(labels []string) *TableData {
return &data
}
// Customize returns a new model with customized column layout.
func (t *TableData) Customize(vs *config.ViewSetting, sc SortColumn, manual bool) (*TableData, SortColumn) {
// ComputeSortCol computes the best matched sort column.
func (t *TableData) ComputeSortCol(vs *config.ViewSetting, sc SortColumn, manual bool) SortColumn {
if vs.IsBlank() {
if sc.Name != "" {
return t, sc
return sc
}
if psc, err := t.sortCol(vs); err == nil {
return t, psc
return psc
}
return t, sc
return sc
}
if manual && sc.IsSet() {
return sc
}
if s, asc, err := vs.SortCol(); err == nil {
return t, SortColumn{Name: s, ASC: asc}
return SortColumn{Name: s, ASC: asc}
}
return t, sc
return sc
}
func (t *TableData) sortCol(vs *config.ViewSetting) (SortColumn, error) {

View File

@ -16,12 +16,13 @@ func init() {
zerolog.SetGlobalLevel(zerolog.FatalLevel)
}
func TestTableDataCustomize(t *testing.T) {
func TestTableDataComputeSortCol(t *testing.T) {
uu := map[string]struct {
t1, e *TableData
t1 *TableData
vs config.ViewSetting
sc SortColumn
wide, manual bool
e SortColumn
}{
"same": {
t1: NewTableDataWithRows(
@ -37,20 +38,8 @@ func TestTableDataCustomize(t *testing.T) {
RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
),
),
vs: config.ViewSetting{Columns: []string{"A", "B", "C"}},
e: NewTableDataWithRows(
client.NewGVR("test"),
Header{
HeaderColumn{Name: "A"},
HeaderColumn{Name: "B"},
HeaderColumn{Name: "C"},
},
NewRowEventsWithEvts(
RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
),
),
vs: config.ViewSetting{Columns: []string{"A", "B", "C"}, SortColumn: "A:asc"},
e: SortColumn{Name: "A", ASC: true},
},
"wide-col": {
t1: NewTableDataWithRows(
@ -66,21 +55,10 @@ func TestTableDataCustomize(t *testing.T) {
RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
),
),
vs: config.ViewSetting{Columns: []string{"A", "B", "C"}},
e: NewTableDataWithRows(
client.NewGVR("test"),
Header{
HeaderColumn{Name: "A"},
HeaderColumn{Name: "B", Attrs: Attrs{Wide: true}},
HeaderColumn{Name: "C"},
},
NewRowEventsWithEvts(
RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
),
),
vs: config.ViewSetting{Columns: []string{"A", "B", "C"}, SortColumn: "B:desc"},
e: SortColumn{Name: "B"},
},
"wide": {
t1: NewTableDataWithRows(
client.NewGVR("test"),
@ -96,28 +74,16 @@ func TestTableDataCustomize(t *testing.T) {
),
),
wide: true,
vs: config.ViewSetting{Columns: []string{"A", "C"}},
e: NewTableDataWithRows(
client.NewGVR("test"),
Header{
HeaderColumn{Name: "A"},
HeaderColumn{Name: "B", Attrs: Attrs{Wide: true}},
HeaderColumn{Name: "C"},
},
NewRowEventsWithEvts(
RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
),
),
vs: config.ViewSetting{Columns: []string{"A", "C"}, SortColumn: ""},
e: SortColumn{Name: ""},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
td, _ := u.t1.Customize(&u.vs, u.sc, u.manual)
assert.Equal(t, u.e, td)
sc := u.t1.ComputeSortCol(&u.vs, u.sc, u.manual)
assert.Equal(t, u.e, sc)
})
}
}

View File

@ -22,8 +22,9 @@ type Alias struct {
func (Alias) Header(ns string) model1.Header {
return model1.Header{
model1.HeaderColumn{Name: "RESOURCE"},
model1.HeaderColumn{Name: "GROUP"},
model1.HeaderColumn{Name: "VERSION"},
model1.HeaderColumn{Name: "COMMAND"},
model1.HeaderColumn{Name: "API-GROUP"},
}
}
@ -35,13 +36,13 @@ func (Alias) Render(o interface{}, ns string, r *model1.Row) error {
return fmt.Errorf("expected AliasRes, but got %T", o)
}
r.ID = a.GVR
gvr := client.NewGVR(a.GVR)
res, grp := gvr.RG()
r.ID = gvr.String()
r.Fields = append(r.Fields,
res,
gvr.R(),
gvr.G(),
gvr.V(),
strings.Join(a.Aliases, ","),
grp,
)
return nil

View File

@ -54,17 +54,18 @@ func TestAliasColorer(t *testing.T) {
func TestAliasHeader(t *testing.T) {
h := model1.Header{
model1.HeaderColumn{Name: "RESOURCE"},
model1.HeaderColumn{Name: "GROUP"},
model1.HeaderColumn{Name: "VERSION"},
model1.HeaderColumn{Name: "COMMAND"},
model1.HeaderColumn{Name: "API-GROUP"},
}
var a render.Alias
assert.Equal(t, h, a.Header("fred"))
assert.Equal(t, h, a.Header("ns-1"))
assert.Equal(t, h, a.Header(client.NamespaceAll))
}
func TestAliasRender(t *testing.T) {
a := render.Alias{}
var a render.Alias
o := render.AliasRes{
GVR: "fred/v1/blee",
@ -73,7 +74,10 @@ func TestAliasRender(t *testing.T) {
var r model1.Row
assert.Nil(t, a.Render(o, "fred/v1/blee", &r))
assert.Equal(t, model1.Row{ID: "fred/v1/blee", Fields: model1.Fields{"blee", "a,b,c", "fred"}}, r)
assert.Equal(t, model1.Row{
ID: "fred/v1/blee",
Fields: model1.Fields{"blee", "fred", "v1", "a,b,c"},
}, r)
}
func BenchmarkAlias(b *testing.B) {

View File

@ -12,7 +12,7 @@ import (
"k8s.io/kubectl/pkg/cmd/get"
)
var fullRX = regexp.MustCompile(`\A([\w\s%/\-]+)\:?([^\|]*)\|?([T|N|W|L|R|H]{0,3})\b`)
var fullRX = regexp.MustCompile(`\A([\w\s%\/-]+)\:?([^\|]*)\|?([T|N|W|L|R|H]{0,3})\b`)
type colAttr byte

View File

@ -157,7 +157,7 @@ func (t *Table) GVR() client.GVR { return t.gvr }
func (t *Table) ViewSettingsChanged(vs *config.ViewSetting) {
if t.setViewSetting(vs) {
if vs == nil {
if !t.getMSort() {
if !t.getMSort() && !t.sortCol.IsSet() {
t.setSortCol(model1.SortColumn{})
}
} else {
@ -280,10 +280,9 @@ func (t *Table) doUpdate(data *model1.TableData) *model1.TableData {
t.actions.Delete(KeyShiftP)
}
cdata, sortCol := data.Customize(t.getViewSetting(), t.getSortCol(), t.getMSort())
t.setSortCol(sortCol)
t.setSortCol(data.ComputeSortCol(t.getViewSetting(), t.getSortCol(), t.getMSort()))
return cdata
return data
}
func (t *Table) UpdateUI(cdata, data *model1.TableData) {

View File

@ -5,7 +5,6 @@ package view
import (
"context"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
@ -63,13 +62,12 @@ func (a *Alias) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
return a.GetTable().activateCmd(evt)
}
r, _ := a.GetTable().GetSelection()
if r != 0 {
s := ui.TrimCell(a.GetTable().SelectTable, r, 1)
tokens := strings.Split(s, ",")
a.App().gotoResource(tokens[0], "", true, true)
return nil
path := a.GetTable().GetSelectedItem()
if path == "" {
return evt
}
gvr := client.NewGVR(path)
a.App().gotoResource(gvr.AsResourceName(), "", true, true)
return evt
return nil
}

View File

@ -476,10 +476,6 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
if client.IsAllNamespace(ns) {
b.GetTable().SetSortCol("NAMESPACE", true)
}
if err := b.app.switchNS(ns); err != nil {
b.App().Flash().Err(err)
return nil

View File

@ -341,7 +341,7 @@ func (c *Command) exec(p *cmd.Interpreter, gvr client.GVR, comp model.Component,
if comp == nil {
return fmt.Errorf("no component found for %s", gvr)
}
c.app.Flash().Infof("Viewing %s...", gvr.R())
c.app.Flash().Infof("Viewing %s...", gvr)
if clearStack {
cmd := contextRX.ReplaceAllString(p.GetLine(), "")
c.app.Config.SetActiveView(cmd)

View File

@ -1,6 +1,6 @@
name: k9s
base: core22
version: 'v0.40.3'
version: 'v0.40.4'
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.