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 notesmine
parent
3b7cd99fbc
commit
00b28ceeee
2
Makefile
2
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 ?=
|
||||
|
|
|
|||
|
|
@ -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
3
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
|
||||
|
|
|
|||
4
go.sum
4
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=
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ func newCharts() Charts {
|
|||
MEM: {Color("yellow"), Color("goldenrod")},
|
||||
},
|
||||
FocusFgColor: "white",
|
||||
FocusBgColor: "aqua",
|
||||
FocusBgColor: "orange",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ k9s:
|
|||
bgColor: black
|
||||
dialBgColor: black
|
||||
chartBgColor: black
|
||||
focusFgColor: white
|
||||
focusBgColor: orange
|
||||
defaultDialColors:
|
||||
- palegreen
|
||||
- orangered
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
|
||||
package model
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
import "sort"
|
||||
|
||||
// SuggestionListener listens for suggestions.
|
||||
type SuggestionListener interface {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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++
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 '🐩', '/'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue