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
|
NAME := k9s
|
||||||
VERSION ?= v0.50.6
|
VERSION ?= v0.50.7
|
||||||
PACKAGE := github.com/derailed/$(NAME)
|
PACKAGE := github.com/derailed/$(NAME)
|
||||||
OUTPUT_BIN ?= execs/${NAME}
|
OUTPUT_BIN ?= execs/${NAME}
|
||||||
GO_FLAGS ?=
|
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
|
module github.com/derailed/k9s
|
||||||
|
|
||||||
go 1.24.1
|
go 1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/adrg/xdg v0.5.3
|
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/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||||
github.com/containerd/typeurl/v2 v2.2.2 // 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/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // 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.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
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.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
github.com/creack/pty v1.1.20 h1:VIPb/a2s17qNeQgDnkfZC35RScx+blkKF8GV68n80J4=
|
||||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
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 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
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=
|
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
|
// IsDecodable checks if the k8s resource has a decodable view
|
||||||
func (g *GVR) IsDecodable() bool {
|
func (g *GVR) IsDecodable() bool {
|
||||||
return g.GVK().Kind == "secrets"
|
return g == SecGVR
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = yaml.Marshaler((*GVR)(nil))
|
var _ = yaml.Marshaler((*GVR)(nil))
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,12 @@ import (
|
||||||
"github.com/derailed/k9s/internal/slogs"
|
"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.
|
// K9s tracks K9s configuration options.
|
||||||
type K9s struct {
|
type K9s struct {
|
||||||
LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"`
|
LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"`
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,7 @@ func newCharts() Charts {
|
||||||
MEM: {Color("yellow"), Color("goldenrod")},
|
MEM: {Color("yellow"), Color("goldenrod")},
|
||||||
},
|
},
|
||||||
FocusFgColor: "white",
|
FocusFgColor: "white",
|
||||||
FocusBgColor: "aqua",
|
FocusBgColor: "orange",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,8 @@ k9s:
|
||||||
bgColor: black
|
bgColor: black
|
||||||
dialBgColor: black
|
dialBgColor: black
|
||||||
chartBgColor: black
|
chartBgColor: black
|
||||||
|
focusFgColor: white
|
||||||
|
focusBgColor: orange
|
||||||
defaultDialColors:
|
defaultDialColors:
|
||||||
- palegreen
|
- palegreen
|
||||||
- orangered
|
- orangered
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func (c *CronJob) ListImages(_ context.Context, fqn string) ([]string, error) {
|
||||||
// Run a CronJob.
|
// Run a CronJob.
|
||||||
func (c *CronJob) Run(path string) error {
|
func (c *CronJob) Run(path string) error {
|
||||||
ns, n := client.Namespaced(path)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,9 +106,8 @@ func ToYAML(o runtime.Object, showManaged bool) (string, error) {
|
||||||
delete(meta, "managedFields")
|
delete(meta, "managedFields")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := p.PrintObj(o, &buff)
|
if err := p.PrintObj(o, &buff); err != nil {
|
||||||
if err != nil {
|
slog.Error("PrintObj failed", slogs.Error, err)
|
||||||
slog.Error("Marshal failed", slogs.Error, err)
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"github.com/sahilm/fuzzy"
|
"github.com/sahilm/fuzzy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type podColors map[string]string
|
||||||
|
|
||||||
var podPalette = []string{
|
var podPalette = []string{
|
||||||
"teal",
|
"teal",
|
||||||
"green",
|
"green",
|
||||||
|
|
@ -28,7 +30,7 @@ var podPalette = []string{
|
||||||
// LogItems represents a collection of log items.
|
// LogItems represents a collection of log items.
|
||||||
type LogItems struct {
|
type LogItems struct {
|
||||||
items []*LogItem
|
items []*LogItem
|
||||||
podColors map[string]string
|
podColors podColors
|
||||||
mx sync.RWMutex
|
mx sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,25 +106,28 @@ func (l *LogItems) Add(ii ...*LogItem) {
|
||||||
l.items = append(l.items, ii...)
|
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.
|
// Lines returns a collection of log lines.
|
||||||
func (l *LogItems) Lines(index int, showTime bool, ll [][]byte) {
|
func (l *LogItems) Lines(index int, showTime bool, ll [][]byte) {
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
defer l.mx.Unlock()
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
var colorIndex int
|
|
||||||
for i, item := range l.items[index:] {
|
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()))
|
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()
|
ll[i] = bb.Bytes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +140,7 @@ func (l *LogItems) StrLines(index int, showTime bool) []string {
|
||||||
ll := make([]string, len(l.items[index:]))
|
ll := make([]string, len(l.items[index:]))
|
||||||
for i, item := range l.items[index:] {
|
for i, item := range l.items[index:] {
|
||||||
bb := bytes.NewBuffer(make([]byte, 0, item.Size()))
|
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()
|
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.
|
// Render returns logs as a collection of strings.
|
||||||
func (l *LogItems) Render(index int, showTime bool, ll [][]byte) {
|
func (l *LogItems) Render(index int, showTime bool, ll [][]byte) {
|
||||||
var colorIndex int
|
|
||||||
for i, item := range l.items[index:] {
|
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()))
|
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()
|
ll[i] = bb.Bytes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,17 @@
|
||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/slogs"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Secret represents a secret K8s resource.
|
// Secret represents a secret K8s resource.
|
||||||
|
|
@ -25,11 +29,54 @@ func (s *Secret) Describe(path string) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if !s.decodeData {
|
if s.decodeData {
|
||||||
return encodedDescription, nil
|
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.
|
// 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
|
// Decode removes the encoded part from the secret's description and appends the
|
||||||
// secret's decoded data.
|
// secret's decoded data.
|
||||||
func (s *Secret) Decode(encodedDescription, path string) (string, error) {
|
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, "====")
|
dataEndIndex := strings.Index(encodedDescription, "====")
|
||||||
if dataEndIndex == -1 {
|
if dataEndIndex == -1 {
|
||||||
return "", fmt.Errorf("unable to find data section in secret description")
|
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
|
// 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]
|
body := encodedDescription[0:dataEndIndex]
|
||||||
|
|
||||||
|
o, err := s.Get(context.Background(), path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
data, err := ExtractSecrets(o)
|
data, err := ExtractSecrets(o)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
decodedSecrets := make([]string, 0, len(data))
|
decodedSecrets := make([]string, 0, len(data))
|
||||||
for k, v := range data {
|
for k, v := range data {
|
||||||
line := fmt.Sprintf("%s: %s", k, v)
|
line := fmt.Sprintf("%s: %s", k, v)
|
||||||
|
|
@ -88,7 +133,6 @@ func ExtractSecrets(o runtime.Object) (map[string]string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
secretData := make(map[string]string, len(secret.Data))
|
secretData := make(map[string]string, len(secret.Data))
|
||||||
for k, val := range secret.Data {
|
for k, val := range secret.Data {
|
||||||
secretData[k] = string(val)
|
secretData[k] = string(val)
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,6 @@ func (d *Describe) describe(ctx context.Context, gvr *client.GVR, path string) (
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
|
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
|
||||||
}
|
}
|
||||||
|
|
||||||
if desc, ok := meta.DAO.(*dao.Secret); ok {
|
if desc, ok := meta.DAO.(*dao.Secret); ok {
|
||||||
desc.SetDecodeData(d.decode)
|
desc.SetDecodeData(d.decode)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import "sort"
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SuggestionListener listens for suggestions.
|
// SuggestionListener listens for suggestions.
|
||||||
type SuggestionListener interface {
|
type SuggestionListener interface {
|
||||||
|
|
|
||||||
|
|
@ -12,117 +12,102 @@ const MaxHistory = 20
|
||||||
|
|
||||||
// History represents a command history.
|
// History represents a command history.
|
||||||
type History struct {
|
type History struct {
|
||||||
commands []string
|
commands []string
|
||||||
limit int
|
limit int
|
||||||
activeCommandIndex int
|
currentIdx int
|
||||||
previousCommandIndex int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHistory returns a new instance.
|
// NewHistory returns a new instance.
|
||||||
func NewHistory(limit int) *History {
|
func NewHistory(limit int) *History {
|
||||||
return &History{
|
return &History{
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
currentIdx: -1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last switches the current and previous history index positions so the
|
// List returns the command history.
|
||||||
// new command referenced by the index is the previous command
|
func (h *History) List() []string {
|
||||||
func (h *History) Last() bool {
|
return h.commands
|
||||||
if h.Empty() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
h.activeCommandIndex, h.previousCommandIndex = h.previousCommandIndex, h.activeCommandIndex
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back moves the history position index back by one
|
// Top returns the last command in the history if present.
|
||||||
func (h *History) Back() bool {
|
func (h *History) Top() (string, bool) {
|
||||||
if h.Empty() {
|
h.currentIdx = len(h.commands) - 1
|
||||||
return false
|
|
||||||
|
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
|
return h.commands[idx], true
|
||||||
if h.activeCommandIndex == 0 {
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
h.previousCommandIndex = h.activeCommandIndex
|
// Back moves the history position index back by one.
|
||||||
h.activeCommandIndex--
|
func (h *History) Back() (string, bool) {
|
||||||
return true
|
if h.Empty() || h.currentIdx <= 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
h.currentIdx--
|
||||||
|
|
||||||
|
return h.at(h.currentIdx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward moves the history position index forward by one
|
// Forward moves the history position index forward by one
|
||||||
func (h *History) Forward() bool {
|
func (h *History) Forward() (string, bool) {
|
||||||
if h.Empty() {
|
h.currentIdx++
|
||||||
return false
|
if h.Empty() || h.currentIdx >= len(h.commands) {
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return if there are no more commands left in the forward history
|
return h.at(h.currentIdx)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pop removes the single most recent history item
|
// Pop removes the single most recent history item
|
||||||
// and returns a bool if the list changed.
|
// and returns a bool if the list changed.
|
||||||
func (h *History) Pop() bool {
|
func (h *History) Pop() bool {
|
||||||
return h.PopN(1)
|
return h.popN(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PopN removes the N most recent history item
|
// PopN removes the N most recent history item
|
||||||
// and returns a bool if the list changed.
|
// and returns a bool if the list changed.
|
||||||
// Argument specifies how many to remove from the history
|
// Argument specifies how many to remove from the history
|
||||||
func (h *History) PopN(n int) bool {
|
func (h *History) popN(n int) bool {
|
||||||
cmdLength := len(h.commands)
|
pop := len(h.commands) - n
|
||||||
if cmdLength == 0 {
|
if h.Empty() || pop < 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
h.commands = h.commands[:pop]
|
||||||
|
h.currentIdx = len(h.commands) - 1
|
||||||
|
|
||||||
h.commands = h.commands[:cmdLength-n]
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns the current command history.
|
|
||||||
func (h *History) List() []string {
|
|
||||||
return h.commands
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push adds a new item.
|
// Push adds a new item.
|
||||||
func (h *History) Push(c string) {
|
func (h *History) Push(c string) {
|
||||||
if c == "" {
|
if c == "" || len(h.commands) >= h.limit {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if h.currentIdx < len(h.commands)-1 {
|
||||||
c = strings.ToLower(c)
|
h.commands = h.commands[:h.currentIdx+1]
|
||||||
if len(h.commands) < h.limit {
|
|
||||||
h.commands = append(h.commands, c)
|
|
||||||
h.previousCommandIndex = h.activeCommandIndex
|
|
||||||
h.activeCommandIndex = len(h.commands) - 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
h.commands = append(h.commands, strings.ToLower(c))
|
||||||
|
h.currentIdx = len(h.commands) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear clears out the stack.
|
// Clear clears out the stack.
|
||||||
func (h *History) Clear() {
|
func (h *History) Clear() {
|
||||||
h.commands = nil
|
h.commands = nil
|
||||||
h.activeCommandIndex = 0
|
h.currentIdx = -1
|
||||||
h.previousCommandIndex = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty returns true if no history.
|
// Empty returns true if no history.
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,18 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHistory(t *testing.T) {
|
func TestHistoryClear(t *testing.T) {
|
||||||
h := model.NewHistory(3)
|
h := model.NewHistory(3)
|
||||||
for i := 1; i < 5; i++ {
|
for i := 1; i < 5; i++ {
|
||||||
h.Push(fmt.Sprintf("cmd%d", i))
|
h.Push(fmt.Sprintf("cmd%d", i))
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, []string{"cmd1", "cmd2", "cmd3"}, h.List())
|
assert.Equal(t, []string{"cmd1", "cmd2", "cmd3"}, h.List())
|
||||||
|
|
||||||
h.Clear()
|
h.Clear()
|
||||||
assert.True(t, h.Empty())
|
assert.True(t, h.Empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHistoryDups(t *testing.T) {
|
func TestHistoryPush(t *testing.T) {
|
||||||
h := model.NewHistory(3)
|
h := model.NewHistory(3)
|
||||||
for i := 1; i < 4; i++ {
|
for i := 1; i < 4; i++ {
|
||||||
h.Push(fmt.Sprintf("cmd%d", 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())
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -20,6 +21,10 @@ import (
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
slog.SetDefault(slog.New(slog.DiscardHandler))
|
||||||
|
}
|
||||||
|
|
||||||
func TestLogFullBuffer(t *testing.T) {
|
func TestLogFullBuffer(t *testing.T) {
|
||||||
size := 4
|
size := 4
|
||||||
m := model.NewLog(client.NewGVR("fred"), makeLogOpts(size), 10*time.Millisecond)
|
m := model.NewLog(client.NewGVR("fred"), makeLogOpts(size), 10*time.Millisecond)
|
||||||
|
|
@ -272,8 +277,7 @@ func (t *testView) LogCleared() {
|
||||||
t.data = nil
|
t.data = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testView) LogFailed(err error) {
|
func (t *testView) LogFailed(error) {
|
||||||
fmt.Println("LogErr", err)
|
|
||||||
t.errCalled++
|
t.errCalled++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ type YAML struct {
|
||||||
lines []string
|
lines []string
|
||||||
listeners []ResourceViewerListener
|
listeners []ResourceViewerListener
|
||||||
options ViewerToggleOpts
|
options ViewerToggleOpts
|
||||||
|
decode bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewYAML return a new yaml resource model.
|
// NewYAML return a new yaml resource model.
|
||||||
|
|
@ -195,7 +196,7 @@ func (y *YAML) RemoveListener(l ResourceViewerListener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToYAML returns a resource yaml.
|
// 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)
|
meta, err := getMeta(ctx, gvr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
@ -205,6 +206,14 @@ func (*YAML) ToYAML(ctx context.Context, gvr *client.GVR, path string, showManag
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", fmt.Errorf("no describer for %q", meta.DAO.GVR())
|
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)
|
return desc.ToYAML(path, showManaged)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle toggles the decode flag.
|
||||||
|
func (y *YAML) Toggle() {
|
||||||
|
y.decode = !y.decode
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/model1"
|
"github.com/derailed/k9s/internal/model1"
|
||||||
"github.com/derailed/k9s/internal/slogs"
|
"github.com/derailed/k9s/internal/slogs"
|
||||||
"github.com/derailed/tview"
|
"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: "%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: "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: "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: "LABELS", Attrs: model1.Attrs{Wide: true}},
|
||||||
model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
|
model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
|
||||||
model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: 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),
|
client.ToPercentageStr(c.mem, a.mem),
|
||||||
toMc(a.cpu),
|
toMc(a.cpu),
|
||||||
toMi(a.mem),
|
toMi(a.mem),
|
||||||
|
n.gpuSpec(no.Status.Capacity, no.Status.Allocatable),
|
||||||
mapToStr(no.Labels),
|
mapToStr(no.Labels),
|
||||||
AsStatus(n.diagnose(statuses)),
|
AsStatus(n.diagnose(statuses)),
|
||||||
ToAge(no.GetCreationTimestamp()),
|
ToAge(no.GetCreationTimestamp()),
|
||||||
|
|
@ -133,6 +136,21 @@ func (n Node) defaultRow(nwm *NodeWithMetrics, r *model1.Row) error {
|
||||||
return nil
|
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.
|
// Healthy checks component health.
|
||||||
func (n Node) Healthy(_ context.Context, o any) error {
|
func (n Node) Healthy(_ context.Context, o any) error {
|
||||||
nwm, ok := o.(*NodeWithMetrics)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "minikube", r.ID)
|
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"}
|
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[:17])
|
assert.Equal(t, e, r.Fields[:18])
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkNodeRender(b *testing.B) {
|
func BenchmarkNodeRender(b *testing.B) {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
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()
|
dt := pwm.Raw.GetDeletionTimestamp()
|
||||||
_, _, irc, _ := p.Statuses(st.InitContainerStatuses)
|
cReady, _, cRestarts, lastRestart := p.ContainerStats(st.ContainerStatuses)
|
||||||
cr, _, rc, lr := p.Statuses(st.ContainerStatuses)
|
|
||||||
|
|
||||||
rcr, rcc := p.initContainerCounts(spec.InitContainers, st.InitContainerStatuses)
|
iReady, iTerminated, iRestarts := p.initContainerStats(spec.InitContainers, st.InitContainerStatuses)
|
||||||
cr += rcr
|
cReady += iReady
|
||||||
cc := len(spec.Containers) + rcc
|
allCounts := len(spec.Containers) + iTerminated
|
||||||
|
|
||||||
var ccmx []mv1beta1.ContainerMetrics
|
var ccmx []mv1beta1.ContainerMetrics
|
||||||
if pwm.MX != nil {
|
if pwm.MX != nil {
|
||||||
|
|
@ -179,10 +179,10 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
|
||||||
n,
|
n,
|
||||||
computeVulScore(ns, pwm.Raw.GetLabels(), spec),
|
computeVulScore(ns, pwm.Raw.GetLabels(), spec),
|
||||||
"●",
|
"●",
|
||||||
strconv.Itoa(cr) + "/" + strconv.Itoa(cc),
|
strconv.Itoa(cReady) + "/" + strconv.Itoa(allCounts),
|
||||||
phase,
|
phase,
|
||||||
strconv.Itoa(rc + irc),
|
strconv.Itoa(cRestarts + iRestarts),
|
||||||
ToAge(lr),
|
ToAge(lastRestart),
|
||||||
toMc(c.cpu),
|
toMc(c.cpu),
|
||||||
toMi(c.mem),
|
toMi(c.mem),
|
||||||
toMc(r.cpu) + ":" + toMc(r.lcpu),
|
toMc(r.cpu) + ":" + toMc(r.lcpu),
|
||||||
|
|
@ -198,7 +198,7 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
|
||||||
asReadinessGate(spec, &st),
|
asReadinessGate(spec, &st),
|
||||||
p.mapQOS(st.QOSClass),
|
p.mapQOS(st.QOSClass),
|
||||||
mapToStr(pwm.Raw.GetLabels()),
|
mapToStr(pwm.Raw.GetLabels()),
|
||||||
AsStatus(p.diagnose(phase, cr, cc)),
|
AsStatus(p.diagnose(phase, cReady, allCounts)),
|
||||||
ToAge(pwm.Raw.GetCreationTimestamp()),
|
ToAge(pwm.Raw.GetCreationTimestamp()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,13 +224,13 @@ func (p Pod) Healthy(_ context.Context, o any) error {
|
||||||
}
|
}
|
||||||
dt := pwm.Raw.GetDeletionTimestamp()
|
dt := pwm.Raw.GetDeletionTimestamp()
|
||||||
phase := p.Phase(dt, spec, &st)
|
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)
|
icr, ict, _ := p.initContainerStats(spec.InitContainers, st.InitContainerStatuses)
|
||||||
cr += rcr
|
cr += icr
|
||||||
cc := len(spec.Containers) + rcc
|
ct += ict
|
||||||
|
|
||||||
return p.diagnose(phase, cr, cc)
|
return p.diagnose(phase, cr, ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Pod) diagnose(phase string, cr, ct int) error {
|
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.
|
// ContainerStats reports pod container stats.
|
||||||
func (*Pod) Statuses(cc []v1.ContainerStatus) (cr, ct, rc int, latest metav1.Time) {
|
func (*Pod) ContainerStats(cc []v1.ContainerStatus) (readyCnt, terminatedCnt, restartCnt int, latest metav1.Time) {
|
||||||
for i := range cc {
|
for i := range cc {
|
||||||
if cc[i].State.Terminated != nil {
|
if cc[i].State.Terminated != nil {
|
||||||
ct++
|
terminatedCnt++
|
||||||
}
|
}
|
||||||
if cc[i].Ready {
|
if cc[i].Ready {
|
||||||
cr++
|
readyCnt++
|
||||||
}
|
}
|
||||||
rc += int(cc[i].RestartCount)
|
restartCnt += int(cc[i].RestartCount)
|
||||||
|
|
||||||
if t := cc[i].LastTerminationState.Terminated; t != nil {
|
if t := cc[i].LastTerminationState.Terminated; t != nil {
|
||||||
ts := cc[i].LastTerminationState.Terminated.FinishedAt
|
ts := cc[i].LastTerminationState.Terminated.FinishedAt
|
||||||
|
|
@ -393,15 +393,16 @@ func (*Pod) Statuses(cc []v1.ContainerStatus) (cr, ct, rc int, latest metav1.Tim
|
||||||
return
|
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 {
|
for i := range cos {
|
||||||
if !restartableInitCO(cc[i].RestartPolicy) {
|
if !IsSideCarContainer(cc[i].RestartPolicy) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
total++
|
total++
|
||||||
if cos[i].Ready {
|
if cos[i].Ready {
|
||||||
ready++
|
ready++
|
||||||
}
|
}
|
||||||
|
restart += int(cos[i].RestartCount)
|
||||||
}
|
}
|
||||||
return
|
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) {
|
func (*Pod) initContainerPhase(spec *v1.PodSpec, pst *v1.PodStatus, status string) (string, bool) {
|
||||||
count := len(spec.InitContainers)
|
count := len(spec.InitContainers)
|
||||||
rs := make(map[string]bool, count)
|
sidecars := sets.New[string]()
|
||||||
for i := range spec.InitContainers {
|
for i := range spec.InitContainers {
|
||||||
co := spec.InitContainers[i]
|
co := spec.InitContainers[i]
|
||||||
rs[co.Name] = restartableInitCO(co.RestartPolicy)
|
if IsSideCarContainer(co.RestartPolicy) {
|
||||||
|
sidecars.Insert(co.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for i := range pst.InitContainerStatuses {
|
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
|
return s, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -585,7 +588,7 @@ func hasPodReadyCondition(conditions []v1.PodCondition) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func restartableInitCO(p *v1.ContainerRestartPolicy) bool {
|
func IsSideCarContainer(p *v1.ContainerRestartPolicy) bool {
|
||||||
return p != nil && *p == v1.ContainerRestartPolicyAlways
|
return p != nil && *p == v1.ContainerRestartPolicyAlways
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ func Test_restartableInitCO(t *testing.T) {
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
u := uu[k]
|
u := uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
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
|
var p Pod
|
||||||
for name, u := range uu {
|
for name, u := range uu {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
_, _, _, lr := p.Statuses(u.containerStatuses)
|
_, _, _, lr := p.ContainerStats(u.containerStatuses)
|
||||||
assert.Equal(t, u.expected, lr)
|
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.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())
|
||||||
b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())
|
b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())
|
||||||
}
|
}
|
||||||
f.SetFocus(0)
|
f.SetFocus(1)
|
||||||
|
|
||||||
message := opts.Message
|
message := opts.Message
|
||||||
modal.SetText(message)
|
modal.SetText(message)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultPrompt = "%c> [::b]%s"
|
defaultPrompt = "%c%c [::b]%s"
|
||||||
defaultSpacer = 4
|
defaultSpacer = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -81,6 +81,7 @@ type Prompt struct {
|
||||||
app *App
|
app *App
|
||||||
noIcons bool
|
noIcons bool
|
||||||
icon rune
|
icon rune
|
||||||
|
prefix rune
|
||||||
styles *config.Styles
|
styles *config.Styles
|
||||||
model PromptModel
|
model PromptModel
|
||||||
spacer int
|
spacer int
|
||||||
|
|
@ -230,12 +231,11 @@ func (p *Prompt) write(text, suggest string) {
|
||||||
defer p.mx.Unlock()
|
defer p.mx.Unlock()
|
||||||
|
|
||||||
p.SetCursorIndex(p.spacer + len(text))
|
p.SetCursorIndex(p.spacer + len(text))
|
||||||
txt := text
|
|
||||||
if suggest != "" {
|
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)
|
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.SetBorder(true)
|
||||||
p.SetTextColor(p.styles.FgColor())
|
p.SetTextColor(p.styles.FgColor())
|
||||||
p.SetBorderColor(p.colorFor(kind))
|
p.SetBorderColor(p.colorFor(kind))
|
||||||
p.icon = p.iconFor(kind)
|
p.icon, p.prefix = p.prefixesFor(kind)
|
||||||
p.activate()
|
p.activate()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -274,17 +274,19 @@ func (p *Prompt) BufferActive(activate bool, kind model.BufferKind) {
|
||||||
p.Clear()
|
p.Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Prompt) iconFor(k model.BufferKind) rune {
|
func (p *Prompt) prefixesFor(k model.BufferKind) (ic, prefix rune) {
|
||||||
if p.noIcons {
|
defer func() {
|
||||||
return ' '
|
if p.noIcons {
|
||||||
}
|
ic = ' '
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
//nolint:exhaustive
|
//nolint:exhaustive
|
||||||
switch k {
|
switch k {
|
||||||
case model.CommandBuffer:
|
case model.CommandBuffer:
|
||||||
return '🐶'
|
return '🐶', '>'
|
||||||
default:
|
default:
|
||||||
return '🐩'
|
return '🐩', '/'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,52 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCmdNew(t *testing.T) {
|
func TestCmdNew(t *testing.T) {
|
||||||
v := ui.NewPrompt(nil, true, config.NewStyles())
|
uu := map[string]struct {
|
||||||
m := model.NewFishBuff(':', model.CommandBuffer)
|
mode rune
|
||||||
v.SetModel(m)
|
kind model.BufferKind
|
||||||
m.AddListener(v)
|
noIcon bool
|
||||||
for _, r := range "blee" {
|
e string
|
||||||
m.Add(r)
|
}{
|
||||||
|
"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) {
|
func TestCmdUpdate(t *testing.T) {
|
||||||
|
|
@ -34,7 +71,7 @@ func TestCmdUpdate(t *testing.T) {
|
||||||
m.SetText("blee", "")
|
m.SetText("blee", "")
|
||||||
m.Add('!')
|
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())
|
assert.False(t, v.InCmdMode())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ func TrimCell(tv *SelectTable, row, col int) string {
|
||||||
|
|
||||||
// TrimLabelSelector extracts label query.
|
// TrimLabelSelector extracts label query.
|
||||||
func TrimLabelSelector(s string) (labels.Selector, error) {
|
func TrimLabelSelector(s string) (labels.Selector, error) {
|
||||||
var selStr string
|
selStr := s
|
||||||
if strings.Index(s, "-l") == 0 {
|
if strings.Index(s, "-l") == 0 {
|
||||||
selStr = strings.TrimSpace(s[2:])
|
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 {
|
func (a *App) quitCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
noExit := a.Config.K9s.NoExitOnCtrlC
|
||||||
if a.InCmdMode() {
|
if a.InCmdMode() {
|
||||||
|
if isBailoutEvt(evt) && noExit {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
if !a.Config.K9s.NoExitOnCtrlC {
|
if !noExit {
|
||||||
a.BailOut(0)
|
a.BailOut(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// overwrite the default ctrl-c behavior of tview
|
|
||||||
return nil
|
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() {
|
if evt != nil && evt.Rune() == rune(ui.KeyLeftBracket) && a.Prompt().InCmdMode() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
cmds := a.cmdHistory.List()
|
c, ok := a.cmdHistory.Back()
|
||||||
if !a.cmdHistory.Back() {
|
if !ok {
|
||||||
a.App.Flash().Warn("Can't go back any further")
|
a.App.Flash().Warn("Can't go back any further")
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
a.gotoResource(cmds[a.cmdHistory.CurrentIndex()], "", true, false)
|
a.gotoResource(c, "", true, false)
|
||||||
return nil
|
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() {
|
if evt != nil && evt.Rune() == rune(ui.KeyRightBracket) && a.Prompt().InCmdMode() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
cmds := a.cmdHistory.List()
|
c, ok := a.cmdHistory.Forward()
|
||||||
if !a.cmdHistory.Forward() {
|
if !ok {
|
||||||
a.App.Flash().Warn("Can't go forward any further")
|
a.App.Flash().Warn("Can't go forward any further")
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
// We go to the resource before updating the history so that
|
// We go to the resource before updating the history so that
|
||||||
// gotoResource doesn't add this command to the history
|
// gotoResource doesn't add this command to the history
|
||||||
a.gotoResource(cmds[a.cmdHistory.CurrentIndex()], "", true, false)
|
a.gotoResource(c, "", true, false)
|
||||||
return nil
|
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() {
|
if evt != nil && evt.Rune() == ui.KeyDash && a.Prompt().InCmdMode() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
cmds := a.cmdHistory.List()
|
c, ok := a.cmdHistory.Top()
|
||||||
if len(cmds) < 1 {
|
if !ok {
|
||||||
a.App.Flash().Warn("No previous view to switch to")
|
a.App.Flash().Warn("No previous view to switch to")
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
a.cmdHistory.Last()
|
a.gotoResource(c, "", true, false)
|
||||||
a.gotoResource(cmds[a.cmdHistory.CurrentIndex()], "", true, false)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,15 @@ func NewInterpreter(s string) *Interpreter {
|
||||||
return &c
|
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() {
|
func (c *Interpreter) grok() {
|
||||||
ff := strings.Fields(c.line)
|
ff := strings.Fields(c.line)
|
||||||
if len(ff) == 0 {
|
if len(ff) == 0 {
|
||||||
|
|
|
||||||
|
|
@ -229,11 +229,11 @@ func (c *Command) defaultCmd(isRoot bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.run(p, "", true, true); err != nil {
|
if err := c.run(p, "", true, true); err != nil {
|
||||||
p = p.Reset(defCmd)
|
|
||||||
slog.Error("Command exec failed. Using default command",
|
slog.Error("Command exec failed. Using default command",
|
||||||
slogs.Command, p.GetLine(),
|
slogs.Command, p.GetLine(),
|
||||||
slogs.Error, err,
|
slogs.Error, err,
|
||||||
)
|
)
|
||||||
|
p = p.Reset(defCmd)
|
||||||
return c.run(p, "", true, true)
|
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()))
|
slog.Error("Dumping stack", slogs.Stack, string(debug.Stack()))
|
||||||
|
|
||||||
ci := cmd.NewInterpreter(podCmd)
|
ci := cmd.NewInterpreter(podCmd)
|
||||||
cmds := c.app.cmdHistory.List()
|
currentCommand, ok := c.app.cmdHistory.Top()
|
||||||
currentCommand := cmds[c.app.cmdHistory.CurrentIndex()]
|
if ok {
|
||||||
if currentCommand != podCmd {
|
|
||||||
ci = ci.Reset(currentCommand)
|
ci = ci.Reset(currentCommand)
|
||||||
}
|
}
|
||||||
err = c.run(ci, "", true, true)
|
err = c.run(ci, "", true, true)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"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] {
|
func aliases(m *v1.APIResource, aa sets.Set[string]) sets.Set[string] {
|
||||||
ss := sets.New(aa.UnsortedList()...)
|
ss := sets.New(aa.UnsortedList()...)
|
||||||
ss.Insert(m.Name)
|
ss.Insert(m.Name)
|
||||||
|
|
|
||||||
|
|
@ -162,14 +162,13 @@ func (v *LiveView) bindKeys() {
|
||||||
if v.title == yamlAction {
|
if v.title == yamlAction {
|
||||||
v.actions.Add(ui.KeyM, ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true))
|
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))
|
v.actions.Add(ui.KeyX, ui.NewKeyAction("Toggle Decode", v.toggleEncodedDecodedCmd, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *LiveView) toggleEncodedDecodedCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *LiveView) toggleEncodedDecodedCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
m, ok := v.model.(model.EncDecResourceViewer)
|
m, ok := v.model.(model.EncDecResourceViewer)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ import (
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/model1"
|
"github.com/derailed/k9s/internal/model1"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
|
cmd2 "github.com/derailed/k9s/internal/view/cmd"
|
||||||
"github.com/derailed/tcell/v2"
|
"github.com/derailed/tcell/v2"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -41,7 +43,14 @@ func (n *Namespace) bindKeys(aa *ui.KeyActions) {
|
||||||
|
|
||||||
func (n *Namespace) switchNs(app *App, _ ui.Tabular, _ *client.GVR, path string) {
|
func (n *Namespace) switchNs(app *App, _ ui.Tabular, _ *client.GVR, path string) {
|
||||||
n.useNamespace(path)
|
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 {
|
func (n *Namespace) useNsCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
@ -85,17 +94,16 @@ func (n *Namespace) decorate(td *model1.TableData) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
favs := make(map[string]struct{})
|
var (
|
||||||
for _, ns := range n.App().Config.FavNamespaces() {
|
favs = sets.New(n.App().Config.FavNamespaces()...)
|
||||||
favs[ns] = struct{}{}
|
activeNS = n.App().Config.ActiveNamespace()
|
||||||
}
|
)
|
||||||
ans := n.App().Config.ActiveNamespace()
|
|
||||||
td.RowsRange(func(i int, re model1.RowEvent) bool {
|
td.RowsRange(func(i int, re model1.RowEvent) bool {
|
||||||
_, n := client.Namespaced(re.Row.ID)
|
_, n := client.Namespaced(re.Row.ID)
|
||||||
if _, ok := favs[n]; ok {
|
if favs.Has(n) {
|
||||||
re.Row.Fields[0] += favNSIndicator
|
re.Row.Fields[0] += favNSIndicator
|
||||||
}
|
}
|
||||||
if ans == re.Row.ID {
|
if n == activeNS {
|
||||||
re.Row.Fields[0] += defaultNSIndicator
|
re.Row.Fields[0] += defaultNSIndicator
|
||||||
}
|
}
|
||||||
re.Kind = model1.EventUnchanged
|
re.Kind = model1.EventUnchanged
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,6 @@ func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error {
|
||||||
|
|
||||||
return startFwdCB(v, path, pts)
|
return startFwdCB(v, path, pts)
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowPortForwards(v, path, ports, anns, cb)
|
ShowPortForwards(v, path, ports, anns, cb)
|
||||||
|
|
||||||
return nil
|
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 {
|
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
|
// put the default container as the first entry
|
||||||
defaultContainer, ok := dao.GetDefaultContainer(meta, spec)
|
defaultContainer, ok := dao.GetDefaultContainer(meta, spec)
|
||||||
if ok {
|
if ok {
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,9 @@ func (p *Pod) validate(node *TreeNode, po v1.Pod) error {
|
||||||
var re render.Pod
|
var re render.Pod
|
||||||
phase := re.Phase(po.DeletionTimestamp, &po.Spec, &po.Status)
|
phase := re.Phase(po.DeletionTimestamp, &po.Spec, &po.Status)
|
||||||
ss := po.Status.ContainerStatuses
|
ss := po.Status.ContainerStatuses
|
||||||
cr, _, _, _ := re.Statuses(ss)
|
readyCnt, _, _, _ := re.ContainerStats(ss)
|
||||||
status := OkStatus
|
status := OkStatus
|
||||||
if cr != len(ss) {
|
if readyCnt != len(ss) {
|
||||||
status = ToastStatus
|
status = ToastStatus
|
||||||
}
|
}
|
||||||
if phase == "Completed" {
|
if phase == "Completed" {
|
||||||
|
|
@ -77,7 +77,7 @@ func (p *Pod) validate(node *TreeNode, po v1.Pod) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
node.Extras[StatusKey] = status
|
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
|
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
|
name: k9s
|
||||||
base: core22
|
base: core22
|
||||||
version: 'v0.50.6'
|
version: 'v0.50.7'
|
||||||
summary: K9s is a CLI to view and manage your Kubernetes clusters.
|
summary: K9s is a CLI to view and manage your Kubernetes clusters.
|
||||||
description: |
|
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.
|
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