Rel v0.50.7 (#3436)

* fix #3406 - update and clean history navigation

* fix #3383 - cronjob auth fix

* clean up and updates

* update prompt indicator to diff cmd vs filter

* fix #3398 - dialog focus update

* update deps

* fix #3435 - noexit on ctrl-c

* fix #3412 - toggle decode

* fix #3424 - add gpu on node

* fix #3422 - resource switch ns

* update rel notes
mine
Fernand Galiana 2025-07-05 10:36:36 -06:00 committed by GitHub
parent 3b7cd99fbc
commit 00b28ceeee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 666 additions and 206 deletions

View File

@ -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 ?=

View File

@ -0,0 +1,49 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# 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
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#

3
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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))

View File

@ -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"`

View File

@ -299,7 +299,7 @@ func newCharts() Charts {
MEM: {Color("yellow"), Color("goldenrod")},
},
FocusFgColor: "white",
FocusBgColor: "aqua",
FocusBgColor: "orange",
}
}

View File

@ -76,6 +76,8 @@ k9s:
bgColor: black
dialBgColor: black
chartBgColor: black
focusFgColor: white
focusBgColor: orange
defaultDialColors:
- palegreen
- orangered

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -3,9 +3,7 @@
package model
import (
"sort"
)
import "sort"
// SuggestionListener listens for suggestions.
type SuggestionListener interface {

View File

@ -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.

View File

@ -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)
})
}
}

View File

@ -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++
}

View File

@ -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
}

View File

@ -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)

View File

@ -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))
})
}
}

View File

@ -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", "<none>", "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", "<none>", "0", "10", "20", "0", "0", "4000", "7874", "n/a"}
assert.Equal(t, e, r.Fields[:18])
}
func BenchmarkNodeRender(b *testing.B) {

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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)

View File

@ -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 '🐩', '/'
}
}

View File

@ -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())
}

View File

@ -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:])
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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
}

57
skins/vercel.yaml Normal file
View File

@ -0,0 +1,57 @@
foreground: &foreground "#ffffff"
background: &background "#000000"
current_line: &current_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

View File

@ -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.