From e44c223e91c7fe5f6c77e3236272d1cf94bdd536 Mon Sep 17 00:00:00 2001 From: Fernand Galiana Date: Mon, 17 Feb 2025 23:27:56 -0700 Subject: [PATCH] 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 --- Dockerfile | 6 ++-- Makefile | 2 +- README.md | 7 ++++ change_logs/release_v0.40.4.md | 56 +++++++++++++++++++++++++++++ internal/client/gvr.go | 4 +++ internal/client/gvr_test.go | 6 ++-- internal/config/alias.go | 5 ++- internal/dao/alias.go | 11 ++++-- internal/dao/dynamic.go | 2 +- internal/model1/row_event.go | 10 ++++++ internal/model1/table_data.go | 45 +++++++++++++---------- internal/model1/table_data_test.go | 58 +++++++----------------------- internal/render/alias.go | 11 +++--- internal/render/alias_test.go | 12 ++++--- internal/render/cust_col.go | 2 +- internal/ui/table.go | 7 ++-- internal/view/alias.go | 14 ++++---- internal/view/browser.go | 4 --- internal/view/command.go | 2 +- snap/snapcraft.yaml | 2 +- 20 files changed, 160 insertions(+), 106 deletions(-) create mode 100644 change_logs/release_v0.40.4.md diff --git a/Dockerfile b/Dockerfile index 064dca01..f458e06c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/Makefile b/Makefile index dddc8fff..71f89c67 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.40.3 +VERSION ?= v0.40.4 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/README.md b/README.md index c3670705..c7c02a21 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/change_logs/release_v0.40.4.md b/change_logs/release_v0.40.4.md new file mode 100644 index 00000000..591d5da1 --- /dev/null +++ b/change_logs/release_v0.40.4.md @@ -0,0 +1,56 @@ + + +# 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 + + + © 2024 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/gvr.go b/internal/client/gvr.go index 853efb80..a4a3d182 100644 --- a/internal/client/gvr.go +++ b/internal/client/gvr.go @@ -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 } diff --git a/internal/client/gvr_test.go b/internal/client/gvr_test.go index 744ef267..fb310df2 100644 --- a/internal/client/gvr_test.go +++ b/internal/client/gvr_test.go @@ -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 { diff --git a/internal/config/alias.go b/internal/config/alias.go index 426d41d7..ec2b9bd1 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -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 } } diff --git a/internal/dao/alias.go b/internal/dao/alias.go index dc4e3df9..3fb8b622 100644 --- a/internal/dao/alias.go +++ b/internal/dao/alias.go @@ -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...) diff --git a/internal/dao/dynamic.go b/internal/dao/dynamic.go index 8f7d6550..de97b268 100644 --- a/internal/dao/dynamic.go +++ b/internal/dao/dynamic.go @@ -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) diff --git a/internal/model1/row_event.go b/internal/model1/row_event.go index c475f38d..1c5e065b 100644 --- a/internal/model1/row_event.go +++ b/internal/model1/row_event.go @@ -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. diff --git a/internal/model1/table_data.go b/internal/model1/table_data.go index d6e5bc82..3c70a8d5 100644 --- a/internal/model1/table_data.go +++ b/internal/model1/table_data.go @@ -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) { diff --git a/internal/model1/table_data_test.go b/internal/model1/table_data_test.go index 80237701..dddbdf36 100644 --- a/internal/model1/table_data_test.go +++ b/internal/model1/table_data_test.go @@ -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) }) } } diff --git a/internal/render/alias.go b/internal/render/alias.go index c9362a81..b7faa76e 100644 --- a/internal/render/alias.go +++ b/internal/render/alias.go @@ -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 diff --git a/internal/render/alias_test.go b/internal/render/alias_test.go index af85a3f6..a71b39e7 100644 --- a/internal/render/alias_test.go +++ b/internal/render/alias_test.go @@ -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) { diff --git a/internal/render/cust_col.go b/internal/render/cust_col.go index 55b3320a..a309f62d 100644 --- a/internal/render/cust_col.go +++ b/internal/render/cust_col.go @@ -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 diff --git a/internal/ui/table.go b/internal/ui/table.go index c8ceafe9..fdf68b7d 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -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) { diff --git a/internal/view/alias.go b/internal/view/alias.go index 9ca8a79e..1a67d7fe 100644 --- a/internal/view/alias.go +++ b/internal/view/alias.go @@ -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 } diff --git a/internal/view/browser.go b/internal/view/browser.go index d5c9cfdf..52b6a2cb 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -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 diff --git a/internal/view/command.go b/internal/view/command.go index bf570f78..6f3d49ea 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -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) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 070f6405..79cbf935 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -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.