diff --git a/Makefile b/Makefile
index d193f5bd..1c7b0d0a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
NAME := k9s
-VERSION ?= v0.50.6
+VERSION ?= v0.50.7
PACKAGE := github.com/derailed/$(NAME)
OUTPUT_BIN ?= execs/${NAME}
GO_FLAGS ?=
diff --git a/change_logs/release_v0.50.7.md b/change_logs/release_v0.50.7.md
new file mode 100644
index 00000000..462c5786
--- /dev/null
+++ b/change_logs/release_v0.50.7.md
@@ -0,0 +1,49 @@
+
+
+# Release v0.50.7
+
+## 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
+
+* [#3435](https://github.com/derailed/k9s/issues/3435) noExitOnCtrlC
+* [#3434](https://github.com/derailed/k9s/issues/3434) Pulses - navigation selection is invisible
+* [#3424](https://github.com/derailed/k9s/issues/3424) feat: Add GPUs to nodes view
+* [#3422](https://github.com/derailed/k9s/issues/3422) Changing ns should keep current kind
+* [#3412](https://github.com/derailed/k9s/issues/3412) "Toggle Decode" for secret has no effect
+* [#3406](https://github.com/derailed/k9s/issues/3406) History navigation: new view after going back should truncate forward history
+* [#3398](https://github.com/derailed/k9s/issues/3398) Improve the UX of FieldManager field on restart
+* [#3383](https://github.com/derailed/k9s/issues/3383) Triggering a CronJob fails as Unauthorized since v0.50
+* [#3406](https://github.com/derailed/k9s/issues/3406) History navigation: new view after going back should truncate forward history
+
+---
+
+## 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!!
+
+* [#3433](https://github.com/derailed/k9s/pull/3433) feat(plugins): add kube-metrics plugin
+* [#3371](https://github.com/derailed/k9s/pull/3371) Add context to condition in keda-toggle plugin
+* [#3347](https://github.com/derailed/k9s/pull/3347) Fix GVR Title option in readme
+* [#3346](https://github.com/derailed/k9s/pull/3346) revert: #3322
+
+
+---
+
© 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 d903370b..f0959575 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/derailed/k9s
-go 1.24.1
+go 1.24.4
require (
github.com/adrg/xdg v0.5.3
@@ -123,6 +123,7 @@ require (
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/containerd/typeurl/v2 v2.2.2 // indirect
+ github.com/creack/pty v1.1.20 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // indirect
diff --git a/go.sum b/go.sum
index 9d49e33f..bb617140 100644
--- a/go.sum
+++ b/go.sum
@@ -871,8 +871,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
-github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/creack/pty v1.1.20 h1:VIPb/a2s17qNeQgDnkfZC35RScx+blkKF8GV68n80J4=
+github.com/creack/pty v1.1.20/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/internal/client/gvr.go b/internal/client/gvr.go
index c991aa0e..91befcaf 100644
--- a/internal/client/gvr.go
+++ b/internal/client/gvr.go
@@ -213,7 +213,7 @@ func (g *GVR) G() string {
// IsDecodable checks if the k8s resource has a decodable view
func (g *GVR) IsDecodable() bool {
- return g.GVK().Kind == "secrets"
+ return g == SecGVR
}
var _ = yaml.Marshaler((*GVR)(nil))
diff --git a/internal/config/k9s.go b/internal/config/k9s.go
index 92536810..a3e78d50 100644
--- a/internal/config/k9s.go
+++ b/internal/config/k9s.go
@@ -19,6 +19,12 @@ import (
"github.com/derailed/k9s/internal/slogs"
)
+var KnownGPUVendors = map[string]string{
+ "nvidia": "nvidia.com/gpu",
+ "amd": "amd.com/gpu",
+ "intel": "gpu.intel.com/i915",
+}
+
// K9s tracks K9s configuration options.
type K9s struct {
LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"`
diff --git a/internal/config/styles.go b/internal/config/styles.go
index 727b9227..5ef799e9 100644
--- a/internal/config/styles.go
+++ b/internal/config/styles.go
@@ -299,7 +299,7 @@ func newCharts() Charts {
MEM: {Color("yellow"), Color("goldenrod")},
},
FocusFgColor: "white",
- FocusBgColor: "aqua",
+ FocusBgColor: "orange",
}
}
diff --git a/internal/config/templates/stock-skin.yaml b/internal/config/templates/stock-skin.yaml
index 3d677bf8..ab1095ed 100644
--- a/internal/config/templates/stock-skin.yaml
+++ b/internal/config/templates/stock-skin.yaml
@@ -76,6 +76,8 @@ k9s:
bgColor: black
dialBgColor: black
chartBgColor: black
+ focusFgColor: white
+ focusBgColor: orange
defaultDialColors:
- palegreen
- orangered
diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go
index 98497e93..d7a12564 100644
--- a/internal/dao/cronjob.go
+++ b/internal/dao/cronjob.go
@@ -46,7 +46,7 @@ func (c *CronJob) ListImages(_ context.Context, fqn string) ([]string, error) {
// Run a CronJob.
func (c *CronJob) Run(path string) error {
ns, n := client.Namespaced(path)
- auth, err := c.Client().CanI(ns, c.gvr, n, []string{client.GetVerb, client.CreateVerb})
+ auth, err := c.Client().CanI(ns, client.JobGVR, n, []string{client.GetVerb, client.CreateVerb})
if err != nil {
return err
}
diff --git a/internal/dao/helpers.go b/internal/dao/helpers.go
index b0149ad3..1d376b39 100644
--- a/internal/dao/helpers.go
+++ b/internal/dao/helpers.go
@@ -106,9 +106,8 @@ func ToYAML(o runtime.Object, showManaged bool) (string, error) {
delete(meta, "managedFields")
}
}
- err := p.PrintObj(o, &buff)
- if err != nil {
- slog.Error("Marshal failed", slogs.Error, err)
+ if err := p.PrintObj(o, &buff); err != nil {
+ slog.Error("PrintObj failed", slogs.Error, err)
return "", err
}
diff --git a/internal/dao/log_items.go b/internal/dao/log_items.go
index c5ad94f8..b0f06efa 100644
--- a/internal/dao/log_items.go
+++ b/internal/dao/log_items.go
@@ -14,6 +14,8 @@ import (
"github.com/sahilm/fuzzy"
)
+type podColors map[string]string
+
var podPalette = []string{
"teal",
"green",
@@ -28,7 +30,7 @@ var podPalette = []string{
// LogItems represents a collection of log items.
type LogItems struct {
items []*LogItem
- podColors map[string]string
+ podColors podColors
mx sync.RWMutex
}
@@ -104,25 +106,28 @@ func (l *LogItems) Add(ii ...*LogItem) {
l.items = append(l.items, ii...)
}
+func (l *LogItems) podColorFor(id string) string {
+ color, ok := l.podColors[id]
+ if ok {
+ return color
+ }
+ var idx int
+ for i, r := range id {
+ idx += i * int(r)
+ }
+ l.podColors[id] = podPalette[idx%len(podPalette)]
+
+ return l.podColors[id]
+}
+
// Lines returns a collection of log lines.
func (l *LogItems) Lines(index int, showTime bool, ll [][]byte) {
l.mx.Lock()
defer l.mx.Unlock()
- var colorIndex int
for i, item := range l.items[index:] {
- id := item.ID()
- color, ok := l.podColors[id]
- if !ok {
- if colorIndex >= len(podPalette) {
- colorIndex = 0
- }
- color = podPalette[colorIndex]
- l.podColors[id] = color
- colorIndex++
- }
bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
- item.Render(color, showTime, bb)
+ item.Render(l.podColorFor(item.ID()), showTime, bb)
ll[i] = bb.Bytes()
}
}
@@ -135,7 +140,7 @@ func (l *LogItems) StrLines(index int, showTime bool) []string {
ll := make([]string, len(l.items[index:]))
for i, item := range l.items[index:] {
bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
- item.Render("white", showTime, bb)
+ item.Render(l.podColorFor(item.ID()), showTime, bb)
ll[i] = bb.String()
}
@@ -144,20 +149,9 @@ func (l *LogItems) StrLines(index int, showTime bool) []string {
// Render returns logs as a collection of strings.
func (l *LogItems) Render(index int, showTime bool, ll [][]byte) {
- var colorIndex int
for i, item := range l.items[index:] {
- id := item.ID()
- color, ok := l.podColors[id]
- if !ok {
- if colorIndex >= len(podPalette) {
- colorIndex = 0
- }
- color = podPalette[colorIndex]
- l.podColors[id] = color
- colorIndex++
- }
bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
- item.Render(color, showTime, bb)
+ item.Render(l.podColorFor(item.ID()), showTime, bb)
ll[i] = bb.Bytes()
}
}
diff --git a/internal/dao/secret.go b/internal/dao/secret.go
index 298b804b..c619003a 100644
--- a/internal/dao/secret.go
+++ b/internal/dao/secret.go
@@ -4,13 +4,17 @@
package dao
import (
+ "bytes"
+ "context"
"fmt"
+ "log/slog"
"strings"
+ "github.com/derailed/k9s/internal/slogs"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/cli-runtime/pkg/printers"
)
// Secret represents a secret K8s resource.
@@ -25,11 +29,54 @@ func (s *Secret) Describe(path string) (string, error) {
if err != nil {
return "", err
}
- if !s.decodeData {
- return encodedDescription, nil
+ if s.decodeData {
+ return s.Decode(encodedDescription, path)
}
- return s.Decode(encodedDescription, path)
+ return encodedDescription, nil
+}
+
+// ToYAML returns a resource yaml.
+func (s *Secret) ToYAML(path string, showManaged bool) (string, error) {
+ if s.decodeData {
+ return s.decodeYAML(path, showManaged)
+ }
+
+ return s.Generic.ToYAML(path, showManaged)
+}
+
+func (s *Secret) decodeYAML(path string, showManaged bool) (string, error) {
+ o, err := s.Get(context.Background(), path)
+ if err != nil {
+ return "", err
+ }
+ o = o.DeepCopyObject()
+ u, ok := o.(*unstructured.Unstructured)
+ if !ok {
+ return "", fmt.Errorf("expecting unstructured but got %T", o)
+ }
+ if u.Object == nil {
+ return "", fmt.Errorf("expecting unstructured object but got nil")
+ }
+ if !showManaged {
+ if meta, ok := u.Object["metadata"].(map[string]any); ok {
+ delete(meta, "managedFields")
+ }
+ }
+ if decoded, err := ExtractSecrets(o); err == nil {
+ u.Object["data"] = decoded
+ }
+
+ var (
+ buff bytes.Buffer
+ p printers.YAMLPrinter
+ )
+ if err := p.PrintObj(o, &buff); err != nil {
+ slog.Error("PrintObj failed", slogs.Error, err)
+ return "", err
+ }
+
+ return buff.String(), nil
}
// SetDecodeData toggles decode mode.
@@ -40,11 +87,6 @@ func (s *Secret) SetDecodeData(b bool) {
// Decode removes the encoded part from the secret's description and appends the
// secret's decoded data.
func (s *Secret) Decode(encodedDescription, path string) (string, error) {
- o, err := s.getFactory().Get(s.gvr, path, true, labels.Everything())
- if err != nil {
- return "", err
- }
-
dataEndIndex := strings.Index(encodedDescription, "====")
if dataEndIndex == -1 {
return "", fmt.Errorf("unable to find data section in secret description")
@@ -59,11 +101,14 @@ func (s *Secret) Decode(encodedDescription, path string) (string, error) {
// More details about the reasoning of index: https://github.com/kubernetes/kubectl/blob/v0.29.0/pkg/describe/describe.go#L2542
body := encodedDescription[0:dataEndIndex]
+ o, err := s.Get(context.Background(), path)
+ if err != nil {
+ return "", err
+ }
data, err := ExtractSecrets(o)
if err != nil {
return "", err
}
-
decodedSecrets := make([]string, 0, len(data))
for k, v := range data {
line := fmt.Sprintf("%s: %s", k, v)
@@ -88,7 +133,6 @@ func ExtractSecrets(o runtime.Object) (map[string]string, error) {
if err != nil {
return nil, err
}
-
secretData := make(map[string]string, len(secret.Data))
for k, val := range secret.Data {
secretData[k] = string(val)
diff --git a/internal/model/describe.go b/internal/model/describe.go
index 20edb19c..bf933b71 100644
--- a/internal/model/describe.go
+++ b/internal/model/describe.go
@@ -181,7 +181,6 @@ func (d *Describe) describe(ctx context.Context, gvr *client.GVR, path string) (
if !ok {
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
}
-
if desc, ok := meta.DAO.(*dao.Secret); ok {
desc.SetDecodeData(d.decode)
}
diff --git a/internal/model/fish_buff.go b/internal/model/fish_buff.go
index ff2233cd..b0df7a61 100644
--- a/internal/model/fish_buff.go
+++ b/internal/model/fish_buff.go
@@ -3,9 +3,7 @@
package model
-import (
- "sort"
-)
+import "sort"
// SuggestionListener listens for suggestions.
type SuggestionListener interface {
diff --git a/internal/model/history.go b/internal/model/history.go
index 4d6ce114..930f3940 100644
--- a/internal/model/history.go
+++ b/internal/model/history.go
@@ -12,117 +12,102 @@ const MaxHistory = 20
// History represents a command history.
type History struct {
- commands []string
- limit int
- activeCommandIndex int
- previousCommandIndex int
+ commands []string
+ limit int
+ currentIdx int
}
// NewHistory returns a new instance.
func NewHistory(limit int) *History {
return &History{
- limit: limit,
+ limit: limit,
+ currentIdx: -1,
}
}
-// Last switches the current and previous history index positions so the
-// new command referenced by the index is the previous command
-func (h *History) Last() bool {
- if h.Empty() {
- return false
- }
-
- h.activeCommandIndex, h.previousCommandIndex = h.previousCommandIndex, h.activeCommandIndex
- return true
+// List returns the command history.
+func (h *History) List() []string {
+ return h.commands
}
-// Back moves the history position index back by one
-func (h *History) Back() bool {
- if h.Empty() {
- return false
+// Top returns the last command in the history if present.
+func (h *History) Top() (string, bool) {
+ h.currentIdx = len(h.commands) - 1
+
+ return h.at(h.currentIdx)
+}
+
+// Last returns the nth command prior to last.
+func (h *History) Last(idx int) (string, bool) {
+ h.currentIdx = len(h.commands) - idx
+
+ return h.at(h.currentIdx)
+}
+
+func (h *History) at(idx int) (string, bool) {
+ if idx < 0 || idx >= len(h.commands) {
+ return "", false
}
- // Return if there are no more commands left in the backward history
- if h.activeCommandIndex == 0 {
- return false
- }
+ return h.commands[idx], true
+}
- h.previousCommandIndex = h.activeCommandIndex
- h.activeCommandIndex--
- return true
+// Back moves the history position index back by one.
+func (h *History) Back() (string, bool) {
+ if h.Empty() || h.currentIdx <= 0 {
+ return "", false
+ }
+ h.currentIdx--
+
+ return h.at(h.currentIdx)
}
// Forward moves the history position index forward by one
-func (h *History) Forward() bool {
- if h.Empty() {
- return false
+func (h *History) Forward() (string, bool) {
+ h.currentIdx++
+ if h.Empty() || h.currentIdx >= len(h.commands) {
+ return "", false
}
- // Return if there are no more commands left in the forward history
- if h.activeCommandIndex >= len(h.commands)-1 {
- return false
- }
-
- h.previousCommandIndex = h.activeCommandIndex
- h.activeCommandIndex++
- return true
-}
-
-// CurrentIndex returns the current index of the active command in the history
-func (h *History) CurrentIndex() int {
- return h.activeCommandIndex
-}
-
-// PreviousIndex returns the index of the command that was the most recent
-// active command in the history
-func (h *History) PreviousIndex() int {
- return h.previousCommandIndex
+ return h.at(h.currentIdx)
}
// Pop removes the single most recent history item
// and returns a bool if the list changed.
func (h *History) Pop() bool {
- return h.PopN(1)
+ return h.popN(1)
}
// PopN removes the N most recent history item
// and returns a bool if the list changed.
// Argument specifies how many to remove from the history
-func (h *History) PopN(n int) bool {
- cmdLength := len(h.commands)
- if cmdLength == 0 {
+func (h *History) popN(n int) bool {
+ pop := len(h.commands) - n
+ if h.Empty() || pop < 0 {
return false
}
+ h.commands = h.commands[:pop]
+ h.currentIdx = len(h.commands) - 1
- h.commands = h.commands[:cmdLength-n]
return true
}
-// List returns the current command history.
-func (h *History) List() []string {
- return h.commands
-}
-
// Push adds a new item.
func (h *History) Push(c string) {
- if c == "" {
+ if c == "" || len(h.commands) >= h.limit {
return
}
-
- c = strings.ToLower(c)
- if len(h.commands) < h.limit {
- h.commands = append(h.commands, c)
- h.previousCommandIndex = h.activeCommandIndex
- h.activeCommandIndex = len(h.commands) - 1
- return
+ if h.currentIdx < len(h.commands)-1 {
+ h.commands = h.commands[:h.currentIdx+1]
}
+ h.commands = append(h.commands, strings.ToLower(c))
+ h.currentIdx = len(h.commands) - 1
}
// Clear clears out the stack.
func (h *History) Clear() {
h.commands = nil
- h.activeCommandIndex = 0
- h.previousCommandIndex = 0
+ h.currentIdx = -1
}
// Empty returns true if no history.
diff --git a/internal/model/history_test.go b/internal/model/history_test.go
index 02431bee..cf6d6009 100644
--- a/internal/model/history_test.go
+++ b/internal/model/history_test.go
@@ -11,18 +11,18 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestHistory(t *testing.T) {
+func TestHistoryClear(t *testing.T) {
h := model.NewHistory(3)
for i := 1; i < 5; i++ {
h.Push(fmt.Sprintf("cmd%d", i))
}
-
assert.Equal(t, []string{"cmd1", "cmd2", "cmd3"}, h.List())
+
h.Clear()
assert.True(t, h.Empty())
}
-func TestHistoryDups(t *testing.T) {
+func TestHistoryPush(t *testing.T) {
h := model.NewHistory(3)
for i := 1; i < 4; i++ {
h.Push(fmt.Sprintf("cmd%d", i))
@@ -32,3 +32,158 @@ func TestHistoryDups(t *testing.T) {
assert.Equal(t, []string{"cmd1", "cmd2", "cmd3"}, h.List())
}
+
+func TestHistoryTop(t *testing.T) {
+ uu := map[string]struct {
+ push []string
+ pop int
+ cmd string
+ ok bool
+ }{
+ "empty": {},
+
+ "no-one-left": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ pop: 3,
+ },
+
+ "last": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ cmd: "cmd3",
+ ok: true,
+ },
+
+ "middle": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ pop: 1,
+ cmd: "cmd2",
+ ok: true,
+ },
+
+ "first": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ pop: 2,
+ cmd: "cmd1",
+ ok: true,
+ },
+ }
+
+ for k, u := range uu {
+ t.Run(k, func(t *testing.T) {
+ h := model.NewHistory(3)
+ for _, cmd := range u.push {
+ h.Push(cmd)
+ }
+ for range u.pop {
+ _ = h.Pop()
+ }
+
+ cmd, ok := h.Top()
+ assert.Equal(t, u.ok, ok)
+ assert.Equal(t, u.cmd, cmd)
+ })
+ }
+}
+
+func TestHistoryBack(t *testing.T) {
+ uu := map[string]struct {
+ push []string
+ pop int
+ cmd string
+ ok bool
+ }{
+ "empty": {},
+
+ "pop-all": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ pop: 3,
+ },
+
+ "pop-none": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ cmd: "cmd2",
+ ok: true,
+ },
+
+ "pop-one": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ pop: 1,
+ cmd: "cmd1",
+ ok: true,
+ },
+
+ "pop-to-first": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ pop: 2,
+ },
+ }
+
+ for k, u := range uu {
+ t.Run(k, func(t *testing.T) {
+ h := model.NewHistory(3)
+ for _, cmd := range u.push {
+ h.Push(cmd)
+ }
+ for range u.pop {
+ _ = h.Pop()
+ }
+
+ cmd, ok := h.Back()
+ assert.Equal(t, u.ok, ok)
+ assert.Equal(t, u.cmd, cmd)
+ })
+ }
+}
+
+func TestHistoryForward(t *testing.T) {
+ uu := map[string]struct {
+ push []string
+ back int
+ cmd string
+ ok bool
+ }{
+ "empty": {},
+
+ "back-2": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ back: 2,
+ cmd: "cmd2",
+ ok: true,
+ },
+
+ "back-1": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ back: 1,
+ cmd: "cmd3",
+ ok: true,
+ },
+
+ "back-all": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ back: 3,
+ cmd: "cmd2",
+ ok: true,
+ },
+
+ "back-none": {
+ push: []string{"cmd1", "cmd2", "cmd3"},
+ back: 0,
+ },
+ }
+
+ for k, u := range uu {
+ t.Run(k, func(t *testing.T) {
+ h := model.NewHistory(3)
+ for _, cmd := range u.push {
+ h.Push(cmd)
+ }
+ for range u.back {
+ _, _ = h.Back()
+ }
+
+ cmd, ok := h.Forward()
+ assert.Equal(t, u.ok, ok)
+ assert.Equal(t, u.cmd, cmd)
+ })
+ }
+}
diff --git a/internal/model/log_test.go b/internal/model/log_test.go
index 6296d7b8..0b8f5284 100644
--- a/internal/model/log_test.go
+++ b/internal/model/log_test.go
@@ -6,6 +6,7 @@ package model_test
import (
"context"
"fmt"
+ "log/slog"
"strconv"
"testing"
"time"
@@ -20,6 +21,10 @@ import (
"k8s.io/client-go/informers"
)
+func init() {
+ slog.SetDefault(slog.New(slog.DiscardHandler))
+}
+
func TestLogFullBuffer(t *testing.T) {
size := 4
m := model.NewLog(client.NewGVR("fred"), makeLogOpts(size), 10*time.Millisecond)
@@ -272,8 +277,7 @@ func (t *testView) LogCleared() {
t.data = nil
}
-func (t *testView) LogFailed(err error) {
- fmt.Println("LogErr", err)
+func (t *testView) LogFailed(error) {
t.errCalled++
}
diff --git a/internal/model/yaml.go b/internal/model/yaml.go
index 407cc302..988f0707 100644
--- a/internal/model/yaml.go
+++ b/internal/model/yaml.go
@@ -32,6 +32,7 @@ type YAML struct {
lines []string
listeners []ResourceViewerListener
options ViewerToggleOpts
+ decode bool
}
// NewYAML return a new yaml resource model.
@@ -195,7 +196,7 @@ func (y *YAML) RemoveListener(l ResourceViewerListener) {
}
// ToYAML returns a resource yaml.
-func (*YAML) ToYAML(ctx context.Context, gvr *client.GVR, path string, showManaged bool) (string, error) {
+func (y *YAML) ToYAML(ctx context.Context, gvr *client.GVR, path string, showManaged bool) (string, error) {
meta, err := getMeta(ctx, gvr)
if err != nil {
return "", err
@@ -205,6 +206,14 @@ func (*YAML) ToYAML(ctx context.Context, gvr *client.GVR, path string, showManag
if !ok {
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
}
+ if desc, ok := meta.DAO.(*dao.Secret); ok {
+ desc.SetDecodeData(y.decode)
+ }
return desc.ToYAML(path, showManaged)
}
+
+// Toggle toggles the decode flag.
+func (y *YAML) Toggle() {
+ y.decode = !y.decode
+}
diff --git a/internal/render/node.go b/internal/render/node.go
index f1fd094c..d29b779e 100644
--- a/internal/render/node.go
+++ b/internal/render/node.go
@@ -13,6 +13,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/tview"
@@ -46,6 +47,7 @@ var defaultNOHeader = model1.Header{
model1.HeaderColumn{Name: "%MEM", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "CPU/A", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
model1.HeaderColumn{Name: "MEM/A", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},
+ model1.HeaderColumn{Name: "GPU"},
model1.HeaderColumn{Name: "LABELS", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
@@ -125,6 +127,7 @@ func (n Node) defaultRow(nwm *NodeWithMetrics, r *model1.Row) error {
client.ToPercentageStr(c.mem, a.mem),
toMc(a.cpu),
toMi(a.mem),
+ n.gpuSpec(no.Status.Capacity, no.Status.Allocatable),
mapToStr(no.Labels),
AsStatus(n.diagnose(statuses)),
ToAge(no.GetCreationTimestamp()),
@@ -133,6 +136,21 @@ func (n Node) defaultRow(nwm *NodeWithMetrics, r *model1.Row) error {
return nil
}
+func (Node) gpuSpec(capacity, allocatable v1.ResourceList) string {
+ spec := NAValue
+ for k, v := range config.KnownGPUVendors {
+ key := v1.ResourceName(v)
+ if capacity, ok := capacity[key]; ok {
+ if allocs, ok := allocatable[key]; ok {
+ spec = fmt.Sprintf("%s/%s (%s)", capacity.String(), allocs.String(), k)
+ break
+ }
+ }
+ }
+
+ return spec
+}
+
// Healthy checks component health.
func (n Node) Healthy(_ context.Context, o any) error {
nwm, ok := o.(*NodeWithMetrics)
diff --git a/internal/render/node_int_test.go b/internal/render/node_int_test.go
new file mode 100644
index 00000000..74cc904d
--- /dev/null
+++ b/internal/render/node_int_test.go
@@ -0,0 +1,78 @@
+package render
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ v1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/resource"
+)
+
+func Test_gpuSpec(t *testing.T) {
+ uu := map[string]struct {
+ capacity v1.ResourceList
+ allocatable v1.ResourceList
+ e string
+ }{
+ "empty": {
+ e: NAValue,
+ },
+
+ "nvidia": {
+ capacity: v1.ResourceList{
+ v1.ResourceName("nvidia.com/gpu"): resource.MustParse("2"),
+ },
+ allocatable: v1.ResourceList{
+ v1.ResourceName("nvidia.com/gpu"): resource.MustParse("4"),
+ },
+ e: "2/4 (nvidia)",
+ },
+
+ "intel": {
+ capacity: v1.ResourceList{
+ v1.ResourceName("gpu.intel.com/i915"): resource.MustParse("2"),
+ },
+ allocatable: v1.ResourceList{
+ v1.ResourceName("gpu.intel.com/i915"): resource.MustParse("4"),
+ },
+ e: "2/4 (intel)",
+ },
+
+ "amd": {
+ capacity: v1.ResourceList{
+ v1.ResourceName("amd.com/gpu"): resource.MustParse("2"),
+ },
+ allocatable: v1.ResourceList{
+ v1.ResourceName("amd.com/gpu"): resource.MustParse("4"),
+ },
+ e: "2/4 (amd)",
+ },
+
+ "toast-cap": {
+ capacity: v1.ResourceList{
+ v1.ResourceName("gpu.intel.com/iBOZO"): resource.MustParse("2"),
+ },
+ allocatable: v1.ResourceList{
+ v1.ResourceName("gpu.intel.com/i915"): resource.MustParse("4"),
+ },
+ e: NAValue,
+ },
+
+ "toast-alloc": {
+ capacity: v1.ResourceList{
+ v1.ResourceName("gpu.intel.com/i915"): resource.MustParse("2"),
+ },
+ allocatable: v1.ResourceList{
+ v1.ResourceName("gpu.intel.com/iBOZO"): resource.MustParse("4"),
+ },
+ e: NAValue,
+ },
+ }
+
+ for k, u := range uu {
+ t.Run(k, func(t *testing.T) {
+ var n Node
+ assert.Equal(t, u.e, n.gpuSpec(u.capacity, u.allocatable))
+ })
+ }
+}
diff --git a/internal/render/node_test.go b/internal/render/node_test.go
index 50d33b60..b361d96d 100644
--- a/internal/render/node_test.go
+++ b/internal/render/node_test.go
@@ -26,8 +26,8 @@ func TestNodeRender(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "minikube", r.ID)
- e := model1.Fields{"minikube", "Ready", "master", "amd64", "0", "v1.15.2", "Buildroot 2018.05.3", "4.15.0", "192.168.64.107", "", "0", "10", "20", "0", "0", "4000", "7874"}
- assert.Equal(t, e, r.Fields[:17])
+ e := model1.Fields{"minikube", "Ready", "master", "amd64", "0", "v1.15.2", "Buildroot 2018.05.3", "4.15.0", "192.168.64.107", "", "0", "10", "20", "0", "0", "4000", "7874", "n/a"}
+ assert.Equal(t, e, r.Fields[:18])
}
func BenchmarkNodeRender(b *testing.B) {
diff --git a/internal/render/pod.go b/internal/render/pod.go
index fb024917..a2cf1570 100644
--- a/internal/render/pod.go
+++ b/internal/render/pod.go
@@ -21,6 +21,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apimachinery/pkg/util/sets"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
@@ -157,12 +158,11 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
}
dt := pwm.Raw.GetDeletionTimestamp()
- _, _, irc, _ := p.Statuses(st.InitContainerStatuses)
- cr, _, rc, lr := p.Statuses(st.ContainerStatuses)
+ cReady, _, cRestarts, lastRestart := p.ContainerStats(st.ContainerStatuses)
- rcr, rcc := p.initContainerCounts(spec.InitContainers, st.InitContainerStatuses)
- cr += rcr
- cc := len(spec.Containers) + rcc
+ iReady, iTerminated, iRestarts := p.initContainerStats(spec.InitContainers, st.InitContainerStatuses)
+ cReady += iReady
+ allCounts := len(spec.Containers) + iTerminated
var ccmx []mv1beta1.ContainerMetrics
if pwm.MX != nil {
@@ -179,10 +179,10 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
n,
computeVulScore(ns, pwm.Raw.GetLabels(), spec),
"●",
- strconv.Itoa(cr) + "/" + strconv.Itoa(cc),
+ strconv.Itoa(cReady) + "/" + strconv.Itoa(allCounts),
phase,
- strconv.Itoa(rc + irc),
- ToAge(lr),
+ strconv.Itoa(cRestarts + iRestarts),
+ ToAge(lastRestart),
toMc(c.cpu),
toMi(c.mem),
toMc(r.cpu) + ":" + toMc(r.lcpu),
@@ -198,7 +198,7 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
asReadinessGate(spec, &st),
p.mapQOS(st.QOSClass),
mapToStr(pwm.Raw.GetLabels()),
- AsStatus(p.diagnose(phase, cr, cc)),
+ AsStatus(p.diagnose(phase, cReady, allCounts)),
ToAge(pwm.Raw.GetCreationTimestamp()),
}
@@ -224,13 +224,13 @@ func (p Pod) Healthy(_ context.Context, o any) error {
}
dt := pwm.Raw.GetDeletionTimestamp()
phase := p.Phase(dt, spec, &st)
- cr, _, _, _ := p.Statuses(st.ContainerStatuses)
+ cr, ct, _, _ := p.ContainerStats(st.ContainerStatuses)
- rcr, rcc := p.initContainerCounts(spec.InitContainers, st.InitContainerStatuses)
- cr += rcr
- cc := len(spec.Containers) + rcc
+ icr, ict, _ := p.initContainerStats(spec.InitContainers, st.InitContainerStatuses)
+ cr += icr
+ ct += ict
- return p.diagnose(phase, cr, cc)
+ return p.diagnose(phase, cr, ct)
}
func (*Pod) diagnose(phase string, cr, ct int) error {
@@ -371,16 +371,16 @@ func (*Pod) mapQOS(class v1.PodQOSClass) string {
}
}
-// Statuses reports current pod container statuses.
-func (*Pod) Statuses(cc []v1.ContainerStatus) (cr, ct, rc int, latest metav1.Time) {
+// ContainerStats reports pod container stats.
+func (*Pod) ContainerStats(cc []v1.ContainerStatus) (readyCnt, terminatedCnt, restartCnt int, latest metav1.Time) {
for i := range cc {
if cc[i].State.Terminated != nil {
- ct++
+ terminatedCnt++
}
if cc[i].Ready {
- cr++
+ readyCnt++
}
- rc += int(cc[i].RestartCount)
+ restartCnt += int(cc[i].RestartCount)
if t := cc[i].LastTerminationState.Terminated; t != nil {
ts := cc[i].LastTerminationState.Terminated.FinishedAt
@@ -393,15 +393,16 @@ func (*Pod) Statuses(cc []v1.ContainerStatus) (cr, ct, rc int, latest metav1.Tim
return
}
-func (*Pod) initContainerCounts(cc []v1.Container, cos []v1.ContainerStatus) (ready, total int) {
+func (*Pod) initContainerStats(cc []v1.Container, cos []v1.ContainerStatus) (ready, total, restart int) {
for i := range cos {
- if !restartableInitCO(cc[i].RestartPolicy) {
+ if !IsSideCarContainer(cc[i].RestartPolicy) {
continue
}
total++
if cos[i].Ready {
ready++
}
+ restart += int(cos[i].RestartCount)
}
return
}
@@ -457,13 +458,15 @@ func (*Pod) containerPhase(st *v1.PodStatus, status string) (string, bool) {
func (*Pod) initContainerPhase(spec *v1.PodSpec, pst *v1.PodStatus, status string) (string, bool) {
count := len(spec.InitContainers)
- rs := make(map[string]bool, count)
+ sidecars := sets.New[string]()
for i := range spec.InitContainers {
co := spec.InitContainers[i]
- rs[co.Name] = restartableInitCO(co.RestartPolicy)
+ if IsSideCarContainer(co.RestartPolicy) {
+ sidecars.Insert(co.Name)
+ }
}
for i := range pst.InitContainerStatuses {
- if s := checkInitContainerStatus(&pst.InitContainerStatuses[i], i, count, rs[pst.InitContainerStatuses[i].Name]); s != "" {
+ if s := checkInitContainerStatus(&pst.InitContainerStatuses[i], i, count, sidecars.Has(pst.InitContainerStatuses[i].Name)); s != "" {
return s, true
}
}
@@ -585,7 +588,7 @@ func hasPodReadyCondition(conditions []v1.PodCondition) bool {
return false
}
-func restartableInitCO(p *v1.ContainerRestartPolicy) bool {
+func IsSideCarContainer(p *v1.ContainerRestartPolicy) bool {
return p != nil && *p == v1.ContainerRestartPolicyAlways
}
diff --git a/internal/render/pod_int_test.go b/internal/render/pod_int_test.go
index 8bee5f7a..7eef0224 100644
--- a/internal/render/pod_int_test.go
+++ b/internal/render/pod_int_test.go
@@ -293,7 +293,7 @@ func Test_restartableInitCO(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, restartableInitCO(u.p))
+ assert.Equal(t, u.e, IsSideCarContainer(u.p))
})
}
}
@@ -427,7 +427,7 @@ func Test_lastRestart(t *testing.T) {
var p Pod
for name, u := range uu {
t.Run(name, func(t *testing.T) {
- _, _, _, lr := p.Statuses(u.containerStatuses)
+ _, _, _, lr := p.ContainerStats(u.containerStatuses)
assert.Equal(t, u.expected, lr)
})
}
diff --git a/internal/ui/dialog/restart.go b/internal/ui/dialog/restart.go
index fa40e3c1..96144e41 100644
--- a/internal/ui/dialog/restart.go
+++ b/internal/ui/dialog/restart.go
@@ -56,7 +56,7 @@ func ShowRestart(styles *config.Dialog, pages *ui.Pages, opts *RestartDialogOpts
b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())
b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())
}
- f.SetFocus(0)
+ f.SetFocus(1)
message := opts.Message
modal.SetText(message)
diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go
index 5878dc75..4b2eac3c 100644
--- a/internal/ui/prompt.go
+++ b/internal/ui/prompt.go
@@ -14,7 +14,7 @@ import (
)
const (
- defaultPrompt = "%c> [::b]%s"
+ defaultPrompt = "%c%c [::b]%s"
defaultSpacer = 4
)
@@ -81,6 +81,7 @@ type Prompt struct {
app *App
noIcons bool
icon rune
+ prefix rune
styles *config.Styles
model PromptModel
spacer int
@@ -230,12 +231,11 @@ func (p *Prompt) write(text, suggest string) {
defer p.mx.Unlock()
p.SetCursorIndex(p.spacer + len(text))
- txt := text
if suggest != "" {
- txt += fmt.Sprintf("[%s::-]%s", p.styles.Prompt().SuggestColor, suggest)
+ text += fmt.Sprintf("[%s::-]%s", p.styles.Prompt().SuggestColor, suggest)
}
p.StylesChanged(p.styles)
- _, _ = fmt.Fprintf(p, defaultPrompt, p.icon, txt)
+ _, _ = fmt.Fprintf(p, defaultPrompt, p.icon, p.prefix, text)
}
// ----------------------------------------------------------------------------
@@ -263,7 +263,7 @@ func (p *Prompt) BufferActive(activate bool, kind model.BufferKind) {
p.SetBorder(true)
p.SetTextColor(p.styles.FgColor())
p.SetBorderColor(p.colorFor(kind))
- p.icon = p.iconFor(kind)
+ p.icon, p.prefix = p.prefixesFor(kind)
p.activate()
return
}
@@ -274,17 +274,19 @@ func (p *Prompt) BufferActive(activate bool, kind model.BufferKind) {
p.Clear()
}
-func (p *Prompt) iconFor(k model.BufferKind) rune {
- if p.noIcons {
- return ' '
- }
+func (p *Prompt) prefixesFor(k model.BufferKind) (ic, prefix rune) {
+ defer func() {
+ if p.noIcons {
+ ic = ' '
+ }
+ }()
//nolint:exhaustive
switch k {
case model.CommandBuffer:
- return '🐶'
+ return '🐶', '>'
default:
- return '🐩'
+ return '🐩', '/'
}
}
diff --git a/internal/ui/prompt_test.go b/internal/ui/prompt_test.go
index 61a5b7c2..7f94d51e 100644
--- a/internal/ui/prompt_test.go
+++ b/internal/ui/prompt_test.go
@@ -14,15 +14,52 @@ import (
)
func TestCmdNew(t *testing.T) {
- v := ui.NewPrompt(nil, true, config.NewStyles())
- m := model.NewFishBuff(':', model.CommandBuffer)
- v.SetModel(m)
- m.AddListener(v)
- for _, r := range "blee" {
- m.Add(r)
+ uu := map[string]struct {
+ mode rune
+ kind model.BufferKind
+ noIcon bool
+ e string
+ }{
+ "cmd": {
+ mode: ':',
+ noIcon: true,
+ kind: model.CommandBuffer,
+ e: " > [::b]blee\n",
+ },
+
+ "cmd-ic": {
+ mode: ':',
+ kind: model.CommandBuffer,
+ e: "🐶> [::b]blee\n",
+ },
+
+ "search": {
+ mode: '/',
+ kind: model.FilterBuffer,
+ noIcon: true,
+ e: " / [::b]blee\n",
+ },
+
+ "search-ic": {
+ mode: '/',
+ kind: model.FilterBuffer,
+ e: "🐩/ [::b]blee\n",
+ },
}
- assert.Equal(t, "\x00> [::b]blee\n", v.GetText(false))
+ for k, u := range uu {
+ t.Run(k, func(t *testing.T) {
+ v := ui.NewPrompt(nil, u.noIcon, config.NewStyles())
+ m := model.NewFishBuff(u.mode, u.kind)
+ v.SetModel(m)
+ m.AddListener(v)
+ for _, r := range "blee" {
+ m.Add(r)
+ }
+ m.SetActive(true)
+ assert.Equal(t, u.e, v.GetText(false))
+ })
+ }
}
func TestCmdUpdate(t *testing.T) {
@@ -34,7 +71,7 @@ func TestCmdUpdate(t *testing.T) {
m.SetText("blee", "")
m.Add('!')
- assert.Equal(t, "\x00> [::b]blee!\n", v.GetText(false))
+ assert.Equal(t, "\x00\x00 [::b]blee!\n", v.GetText(false))
assert.False(t, v.InCmdMode())
}
diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go
index 6e66d9a0..e854aafe 100644
--- a/internal/ui/table_helper.go
+++ b/internal/ui/table_helper.go
@@ -60,7 +60,7 @@ func TrimCell(tv *SelectTable, row, col int) string {
// TrimLabelSelector extracts label query.
func TrimLabelSelector(s string) (labels.Selector, error) {
- var selStr string
+ selStr := s
if strings.Index(s, "-l") == 0 {
selStr = strings.TrimSpace(s[2:])
}
diff --git a/internal/view/app.go b/internal/view/app.go
index a5bb8e9b..cb1cbadb 100644
--- a/internal/view/app.go
+++ b/internal/view/app.go
@@ -671,15 +671,18 @@ func (a *App) dirCmd(path string, pushCmd bool) error {
}
func (a *App) quitCmd(evt *tcell.EventKey) *tcell.EventKey {
+ noExit := a.Config.K9s.NoExitOnCtrlC
if a.InCmdMode() {
+ if isBailoutEvt(evt) && noExit {
+ return nil
+ }
return evt
}
- if !a.Config.K9s.NoExitOnCtrlC {
+ if !noExit {
a.BailOut(0)
}
- // overwrite the default ctrl-c behavior of tview
return nil
}
@@ -707,12 +710,12 @@ func (a *App) previousCommand(evt *tcell.EventKey) *tcell.EventKey {
if evt != nil && evt.Rune() == rune(ui.KeyLeftBracket) && a.Prompt().InCmdMode() {
return evt
}
- cmds := a.cmdHistory.List()
- if !a.cmdHistory.Back() {
+ c, ok := a.cmdHistory.Back()
+ if !ok {
a.App.Flash().Warn("Can't go back any further")
return evt
}
- a.gotoResource(cmds[a.cmdHistory.CurrentIndex()], "", true, false)
+ a.gotoResource(c, "", true, false)
return nil
}
@@ -721,14 +724,14 @@ func (a *App) nextCommand(evt *tcell.EventKey) *tcell.EventKey {
if evt != nil && evt.Rune() == rune(ui.KeyRightBracket) && a.Prompt().InCmdMode() {
return evt
}
- cmds := a.cmdHistory.List()
- if !a.cmdHistory.Forward() {
+ c, ok := a.cmdHistory.Forward()
+ if !ok {
a.App.Flash().Warn("Can't go forward any further")
return evt
}
// We go to the resource before updating the history so that
// gotoResource doesn't add this command to the history
- a.gotoResource(cmds[a.cmdHistory.CurrentIndex()], "", true, false)
+ a.gotoResource(c, "", true, false)
return nil
}
@@ -737,13 +740,12 @@ func (a *App) lastCommand(evt *tcell.EventKey) *tcell.EventKey {
if evt != nil && evt.Rune() == ui.KeyDash && a.Prompt().InCmdMode() {
return evt
}
- cmds := a.cmdHistory.List()
- if len(cmds) < 1 {
+ c, ok := a.cmdHistory.Top()
+ if !ok {
a.App.Flash().Warn("No previous view to switch to")
return evt
}
- a.cmdHistory.Last()
- a.gotoResource(cmds[a.cmdHistory.CurrentIndex()], "", true, false)
+ a.gotoResource(c, "", true, false)
return nil
}
diff --git a/internal/view/cmd/interpreter.go b/internal/view/cmd/interpreter.go
index 7ddaeb17..27b74158 100644
--- a/internal/view/cmd/interpreter.go
+++ b/internal/view/cmd/interpreter.go
@@ -27,6 +27,15 @@ func NewInterpreter(s string) *Interpreter {
return &c
}
+func (c *Interpreter) TrimNS() string {
+ if !c.HasNS() {
+ return c.line
+ }
+ ns, _ := c.NSArg()
+
+ return strings.TrimSpace(strings.Replace(c.line, ns, "", 1))
+}
+
func (c *Interpreter) grok() {
ff := strings.Fields(c.line)
if len(ff) == 0 {
diff --git a/internal/view/command.go b/internal/view/command.go
index e910141f..66cf5b98 100644
--- a/internal/view/command.go
+++ b/internal/view/command.go
@@ -229,11 +229,11 @@ func (c *Command) defaultCmd(isRoot bool) error {
}
if err := c.run(p, "", true, true); err != nil {
- p = p.Reset(defCmd)
slog.Error("Command exec failed. Using default command",
slogs.Command, p.GetLine(),
slogs.Error, err,
)
+ p = p.Reset(defCmd)
return c.run(p, "", true, true)
}
@@ -331,9 +331,8 @@ func (c *Command) exec(p *cmd.Interpreter, gvr *client.GVR, comp model.Component
slog.Error("Dumping stack", slogs.Stack, string(debug.Stack()))
ci := cmd.NewInterpreter(podCmd)
- cmds := c.app.cmdHistory.List()
- currentCommand := cmds[c.app.cmdHistory.CurrentIndex()]
- if currentCommand != podCmd {
+ currentCommand, ok := c.app.cmdHistory.Top()
+ if ok {
ci = ci.Reset(currentCommand)
}
err = c.run(ci, "", true, true)
diff --git a/internal/view/helpers.go b/internal/view/helpers.go
index 11caa5d8..6fc7f13e 100644
--- a/internal/view/helpers.go
+++ b/internal/view/helpers.go
@@ -31,6 +31,10 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
)
+func isBailoutEvt(evt *tcell.EventKey) bool {
+ return evt.Name() == "Ctrl+C"
+}
+
func aliases(m *v1.APIResource, aa sets.Set[string]) sets.Set[string] {
ss := sets.New(aa.UnsortedList()...)
ss.Insert(m.Name)
diff --git a/internal/view/live_view.go b/internal/view/live_view.go
index bebd6885..e34347dc 100644
--- a/internal/view/live_view.go
+++ b/internal/view/live_view.go
@@ -162,14 +162,13 @@ func (v *LiveView) bindKeys() {
if v.title == yamlAction {
v.actions.Add(ui.KeyM, ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true))
}
- if v.model != nil && v.model.GVR().IsDecodable() {
+ if _, ok := v.model.(model.EncDecResourceViewer); ok {
v.actions.Add(ui.KeyX, ui.NewKeyAction("Toggle Decode", v.toggleEncodedDecodedCmd, true))
}
}
func (v *LiveView) toggleEncodedDecodedCmd(evt *tcell.EventKey) *tcell.EventKey {
m, ok := v.model.(model.EncDecResourceViewer)
-
if !ok {
return evt
}
diff --git a/internal/view/ns.go b/internal/view/ns.go
index fb67feb8..352de125 100644
--- a/internal/view/ns.go
+++ b/internal/view/ns.go
@@ -7,7 +7,9 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/ui"
+ cmd2 "github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
+ "k8s.io/apimachinery/pkg/util/sets"
)
const (
@@ -41,7 +43,14 @@ func (n *Namespace) bindKeys(aa *ui.KeyActions) {
func (n *Namespace) switchNs(app *App, _ ui.Tabular, _ *client.GVR, path string) {
n.useNamespace(path)
- app.gotoResource(client.PodGVR.String(), "", false, true)
+ cmd, ok := app.cmdHistory.Last(2)
+ if !ok || cmd == "" {
+ cmd = client.PodGVR.String()
+ } else {
+ i := cmd2.NewInterpreter(cmd)
+ cmd = i.TrimNS()
+ }
+ app.gotoResource(cmd, "", false, true)
}
func (n *Namespace) useNsCmd(*tcell.EventKey) *tcell.EventKey {
@@ -85,17 +94,16 @@ func (n *Namespace) decorate(td *model1.TableData) {
)
}
- favs := make(map[string]struct{})
- for _, ns := range n.App().Config.FavNamespaces() {
- favs[ns] = struct{}{}
- }
- ans := n.App().Config.ActiveNamespace()
+ var (
+ favs = sets.New(n.App().Config.FavNamespaces()...)
+ activeNS = n.App().Config.ActiveNamespace()
+ )
td.RowsRange(func(i int, re model1.RowEvent) bool {
_, n := client.Namespaced(re.Row.ID)
- if _, ok := favs[n]; ok {
+ if favs.Has(n) {
re.Row.Fields[0] += favNSIndicator
}
- if ans == re.Row.ID {
+ if n == activeNS {
re.Row.Fields[0] += defaultNSIndicator
}
re.Kind = model1.EventUnchanged
diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go
index 9f795cac..9673427c 100644
--- a/internal/view/pf_extender.go
+++ b/internal/view/pf_extender.go
@@ -203,7 +203,6 @@ func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error {
return startFwdCB(v, path, pts)
}
-
ShowPortForwards(v, path, ports, anns, cb)
return nil
diff --git a/internal/view/pod.go b/internal/view/pod.go
index bf4bc982..5bc6639c 100644
--- a/internal/view/pod.go
+++ b/internal/view/pod.go
@@ -504,7 +504,7 @@ func buildShellArgs(cmd, path, co string, flags *genericclioptions.ConfigFlags)
}
func fetchContainers(meta *metav1.ObjectMeta, spec *v1.PodSpec, allContainers bool) []string {
- nn := make([]string, 0, len(spec.Containers)+len(spec.InitContainers))
+ nn := make([]string, 0, len(spec.Containers)+len(spec.EphemeralContainers)+len(spec.InitContainers))
// put the default container as the first entry
defaultContainer, ok := dao.GetDefaultContainer(meta, spec)
if ok {
diff --git a/internal/xray/pod.go b/internal/xray/pod.go
index 0771b5f4..0959fbab 100644
--- a/internal/xray/pod.go
+++ b/internal/xray/pod.go
@@ -67,9 +67,9 @@ func (p *Pod) validate(node *TreeNode, po v1.Pod) error {
var re render.Pod
phase := re.Phase(po.DeletionTimestamp, &po.Spec, &po.Status)
ss := po.Status.ContainerStatuses
- cr, _, _, _ := re.Statuses(ss)
+ readyCnt, _, _, _ := re.ContainerStats(ss)
status := OkStatus
- if cr != len(ss) {
+ if readyCnt != len(ss) {
status = ToastStatus
}
if phase == "Completed" {
@@ -77,7 +77,7 @@ func (p *Pod) validate(node *TreeNode, po v1.Pod) error {
}
node.Extras[StatusKey] = status
- node.Extras[InfoKey] = strconv.Itoa(cr) + "/" + strconv.Itoa(len(ss))
+ node.Extras[InfoKey] = strconv.Itoa(readyCnt) + "/" + strconv.Itoa(len(ss))
return nil
}
diff --git a/skins/vercel.yaml b/skins/vercel.yaml
new file mode 100644
index 00000000..2d243b4f
--- /dev/null
+++ b/skins/vercel.yaml
@@ -0,0 +1,57 @@
+foreground: &foreground "#ffffff"
+background: &background "#000000"
+current_line: ¤t_line "#1a1a1a"
+selection: &selection "#e63946"
+comment: &comment "#555555"
+cyan: &cyan "#00bcd4"
+green: &green "#2ecc71"
+orange: &orange "#f4a261"
+magenta: &magenta "#9d0191"
+blue: &blue "#0070f3"
+red: &red "#e63946"
+
+k9s:
+ body:
+ fgColor: *foreground
+ bgColor: *background
+ logoColor: *red
+ prompt:
+ fgColor: *foreground
+ bgColor: *background
+ suggestColor: *red
+ info:
+ fgColor: *red
+ sectionColor: *foreground
+ help:
+ fgColor: *foreground
+ bgColor: *background
+ keyColor: *red
+ numKeyColor: *blue
+ sectionColor: *green
+ dialog:
+ fgColor: *foreground
+ bgColor: *background
+ buttonFgColor: *foreground
+ buttonBgColor: *red
+ buttonFocusFgColor: *background
+ buttonFocusBgColor: *red
+ labelFgColor: *orange
+ fieldFgColor: *foreground
+ frame:
+ border:
+ fgColor: *selection
+ focusColor: *current_line
+ menu:
+ fgColor: *foreground
+ keyColor: *red
+ numKeyColor: *red
+ crumbs:
+ fgColor: *foreground
+ bgColor: *comment
+ activeColor: *red
+ status:
+ newColor: *cyan
+ modifyColor: *blue
+ addColor: *green
+ errorColor: *red
+ highlightColor: *orange
\ No newline at end of file
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 35435935..2e82b101 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,6 +1,6 @@
name: k9s
base: core22
-version: 'v0.50.6'
+version: 'v0.50.7'
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.