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.