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.