checkpoint

mine
derailed 2020-04-02 13:08:41 -06:00
parent 402b0e440e
commit e211611d4e
47 changed files with 485 additions and 353 deletions

View File

@ -114,7 +114,7 @@ linters-settings:
local-prefixes: github.com/org/project
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 15
min-complexity: 20
gocognit:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 20

View File

@ -98,6 +98,48 @@ K9s is available on Linux, macOS and Windows platforms.
---
## The Command Line
```shell
# List all available CLI options
k9s help
# To get info about K9s runtime (logs, configs, etc..)
k9s info
# To run K9s in a given namespace
k9s -n mycoolns
# Start K9s in an existing KubeConfig context
k9s --context coolCtx
# Start K9s in readonly mode - with all modification commands disabled
k9s --readonly
```
## Key Bindings
K9s uses aliases to navigate most K8s resources.
| Action | Command | Comment |
|---------------------------------------------------------------|-----------------------|-------------------------------------------------------------|
| Show active keyboard mnemonics and help | `?` | |
| Show all available resource alias | `ctrl-a` | |
| To bail out of K9s | `:q`, `ctrl-c` | |
| View a Kubernetes resource using singular/plural or shortname | `:`po⏎ | accepts singular, plural, shortname or alias ie pod or pods |
| View a Kubernetes resource in a given namespace | `:`alias namespace⏎ | |
| Filter out a resource view given a filter | `/`filter⏎ | |
| Filter resource view by labels | `/`-l label-selector⏎ | |
| Fuzzy find a resource given a filter | `/`-f filter⏎ | |
| Bails out of view/command/filter mode | `<esc>` | |
| Key mapping to describe, view, edit, view logs,... | `d`,`v`, `e`, `l`,... | |
| To view and switch to another Kubernetes context | `:`ctx⏎ | |
| To view and switch to another Kubernetes context | `:`ctx context-name⏎ | |
| To view and switch to another Kubernetes namespace | `:`ns⏎ | |
| To view all saved resources | `:`screendump or sd⏎ | |
| To delete a resource (TAB and ENTER to confirm) | `ctrl-d` | |
| To kill a resource (no confirmation dialog!) | `ctrl-k` | |
| Launch pulses view | `:`pulses or pu⏎ | |
| Launch XRay view | `:`xray pod⏎ | accepts po, svc, dp, rs, sts or ds |
---
## Screenshots
1. Pods
@ -146,47 +188,6 @@ K9s is available on Linux, macOS and Windows platforms.
---
## The Command Line
```shell
# List all available CLI options
k9s help
# To get info about K9s runtime (logs, configs, etc..)
k9s info
# To run K9s in a given namespace
k9s -n mycoolns
# Start K9s in an existing KubeConfig context
k9s --context coolCtx
# Start K9s in readonly mode - with all modification commands disabled
k9s --readonly
```
## Key Bindings
K9s uses aliases to navigate most K8s resources.
| Command | Result | Example |
|-----------------------------|----------------------------------------------------|----------------------------|
| `:dp`, `:deploy` | View deployments | |
| `:no`, `:nodes` | View nodes | |
| `:svc`, `:service` | View services | |
| `:`alias`<ENTER>` | View a Kubernetes resource aliases | `:po<ENTER>` |
| `?` | Show keyboard shortcuts and help | |
| `Ctrl-a` | Show all available resource alias | select+`<ENTER>` to view |
| `/`filter`ENTER` | Filter out a resource view given a filter | `/bumblebeetuna` |
| `/`-l label-selector`ENTER` | Filter resource view by labels | `/-l app=fred` |
| `/`-f filter `ENTER` | Fuzzy find a resource given a filter | `/-f ngx` |
| `<Esc>` | Bails out of view/command/filter mode | |
| `d`,`v`, `e`, `l`,... | Key mapping to describe, view, edit, view logs,... | `d` (describes a resource) |
| `:`ctx`<ENTER>` | To view and switch to another Kubernetes context | `:`+`ctx`+`<ENTER>` |
| `:`ns`<ENTER>` | To view and switch to another Kubernetes namespace | `:`+`ns`+`<ENTER>` |
| `:screendump`, `:sd` | To view all saved resources | |
| `Ctrl-d` | To delete a resource (TAB and ENTER to confirm) | |
| `Ctrl-k` | To kill a resource (no confirmation dialog!) | |
| `:q`, `Ctrl-c` | To bail out of K9s | |
---
## K9s Configuration
K9s keeps its configurations in a .k9s directory in your home directory `$HOME/.k9s/config.yml`.

View File

@ -15,7 +15,7 @@ On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_inv
## A Word From Our Sponsors...
It makes me always very happy to hear folks are digging this effort and using K9s daily! If you feel this way please tell us and consider joining our [sponsorship](https://github.com/sponsors/derailed) program.
Big Thanks! to [hornbech](https://github.com/hornbech) for joining our sponsors!
Big THANKS!! to [hornbech](https://github.com/hornbech) for joining our sponsors!
## K8s v1.18.0 Released!

4
go.mod
View File

@ -2,11 +2,9 @@ module github.com/derailed/k9s
go 1.13
replace github.com/derailed/popeye => /Users/fernand/go_wk/derailed/src/github.com/derailed/popeye
require (
github.com/atotto/clipboard v0.1.2
github.com/derailed/popeye v0.0.0-00010101000000-000000000000
github.com/derailed/popeye v0.8.0
github.com/derailed/tview v0.3.9
github.com/drone/envsubst v1.0.2 // indirect
github.com/fatih/color v1.9.0

2
go.sum
View File

@ -123,6 +123,8 @@ github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1
github.com/deislabs/oras v0.8.1 h1:If674KraJVpujYR00rzdi0QAmW4BxzMJPVAZJKuhQ0c=
github.com/deislabs/oras v0.8.1/go.mod h1:Mx0rMSbBNaNfY9hjpccEnxkOqJL6KGjtxNHPLC4G4As=
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
github.com/derailed/popeye v0.8.0 h1:D+5fHiMmuXqaF5J2bJTI+YLrD77ag5Wb1dkAuR4bPGI=
github.com/derailed/popeye v0.8.0/go.mod h1:OBHcJDa50VpE9QNyOU243bNOtHb29MyLlVHJolwlwas=
github.com/derailed/tview v0.3.9 h1:6iUtOmzN6gdk6yx1KNSwhMgrsLYjgldduulKPqHnqwk=
github.com/derailed/tview v0.3.9/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII=
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=

View File

@ -235,6 +235,8 @@ func (a *APIClient) HasMetrics() bool {
defer cancel()
if _, err := dial.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{Limit: 1}); err == nil {
flag = true
} else {
log.Error().Err(err).Msgf("List metrics failed")
}
a.cache.Add(cacheMXKey, flag, cacheExpiry)
@ -359,6 +361,7 @@ func (a *APIClient) supportsMetricsResources() (supported bool) {
apiGroups, err := a.CachedDiscoveryOrDie().ServerGroups()
if err != nil {
log.Debug().Msgf("Unable to access servergroups %#v", err)
return
}
for _, grp := range apiGroups.Groups {

View File

@ -143,7 +143,7 @@ func (a *Aliases) loadDefaultAliases() {
a.declare("quit", "q", "Q")
a.declare("aliases", "alias", "a")
a.declare("popeye", "pop")
a.declare("sanitize", "san", "sanitize")
// a.declare("sanitize", "san", "sanitize")
a.declare("contexts", "context", "ctx")
a.declare("users", "user", "usr")
a.declare("groups", "group", "grp")
@ -151,6 +151,7 @@ func (a *Aliases) loadDefaultAliases() {
a.declare("benchmarks", "benchmark", "be")
a.declare("screendumps", "screendump", "sd")
a.declare("pulses", "pulse", "pu", "hz")
a.declare("xrays", "xray", "x")
}
// Save alias to disk.

View File

@ -263,6 +263,7 @@ var expectedConfig = `k9s:
refreshRate: 100
headless: false
readOnly: true
noIcons: false
logger:
tail: 500
buffer: 800
@ -313,6 +314,7 @@ var resetConfig = `k9s:
refreshRate: 2
headless: false
readOnly: false
noIcons: false
logger:
tail: 200
buffer: 2000

View File

@ -2,16 +2,14 @@ package config
import "github.com/derailed/k9s/internal/client"
const (
defaultRefreshRate = 2
defaultReadOnly = false
)
const defaultRefreshRate = 2
// K9s tracks K9s configuration options.
type K9s struct {
RefreshRate int `yaml:"refreshRate"`
Headless bool `yaml:"headless"`
ReadOnly bool `yaml:"readOnly"`
NoIcons bool `yaml:"noIcons"`
Logger *Logger `yaml:"logger"`
CurrentContext string `yaml:"currentContext"`
CurrentCluster string `yaml:"currentCluster"`
@ -27,7 +25,6 @@ type K9s struct {
func NewK9s() *K9s {
return &K9s{
RefreshRate: defaultRefreshRate,
ReadOnly: defaultReadOnly,
Logger: NewLogger(),
Clusters: make(map[string]*Cluster),
Thresholds: NewThreshold(),

View File

@ -145,7 +145,6 @@ type (
BgColor Color `yaml:"bgColor"`
CursorColor Color `yaml:"cursorColor"`
GraphicColor Color `yaml:"graphicColor"`
ShowIcons bool `yaml:"showIcons"`
}
// Menu tracks menu styles.
@ -312,7 +311,6 @@ func newXray() Xray {
BgColor: "black",
CursorColor: "whitesmoke",
GraphicColor: "floralwhite",
ShowIcons: true,
}
}

View File

@ -34,13 +34,15 @@ type Node struct {
// ToggleCordon toggles cordon/uncordon a node.
func (n *Node) ToggleCordon(path string, cordon bool) error {
o, err := n.Get(context.Background(), path)
log.Debug().Msgf("CORDON %q::%t -- %q", path, cordon, n.gvr.GVK())
o, err := FetchNode(context.Background(), n.Factory, path)
if err != nil {
return err
}
h, err := drain.NewCordonHelperFromRuntimeObject(o, scheme.Scheme, n.gvr.GVK())
if err != nil {
log.Debug().Msgf("BOOM %v", err)
return err
}

View File

@ -5,10 +5,13 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
cfg "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/render"
@ -17,8 +20,6 @@ import (
"github.com/derailed/popeye/types"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions"
restclient "k8s.io/client-go/rest"
)
var _ Accessor = (*Popeye)(nil)
@ -51,11 +52,22 @@ func (p *Popeye) List(ctx context.Context, _ string) ([]runtime.Object, error) {
log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t))
}(time.Now())
js := "json"
flags := config.NewFlags()
spinach := filepath.Join(cfg.K9sHome, "spinach.yml")
flags.Spinach = &spinach
js := "json"
flags.Output = &js
if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" {
sections := []string{report}
flags.Sections = &sections
}
spinach := filepath.Join(cfg.K9sHome, "spinach.yml")
if c, err := p.Factory.Client().Config().CurrentContextName(); err == nil {
spinach = filepath.Join(cfg.K9sHome, fmt.Sprintf("%s_spinach.yml", c))
}
if _, err := os.Stat(spinach); err == nil {
flags.Spinach = &spinach
}
popeye, err := pkg.NewPopeye(flags, &log.Logger)
if err != nil {
return nil, err
@ -68,6 +80,7 @@ func (p *Popeye) List(ctx context.Context, _ string) ([]runtime.Object, error) {
buff := readWriteCloser{Buffer: bytes.NewBufferString("")}
popeye.SetOutputTarget(buff)
if err = popeye.Sanitize(); err != nil {
log.Debug().Msgf("BOOM %#v", *flags.Sections)
return nil, err
}
@ -93,6 +106,8 @@ func (a *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("NYI!!")
}
// Helpers...
type popFactory struct {
Factory
}
@ -102,7 +117,6 @@ var _ types.Factory = (*popFactory)(nil)
func newPopFactory(f Factory) *popFactory {
return &popFactory{Factory: f}
}
func (p *popFactory) Client() types.Connection {
return &popConnection{Connection: p.Factory.Client()}
}
@ -116,16 +130,3 @@ var _ types.Connection = (*popConnection)(nil)
func (c *popConnection) Config() types.Config {
return c.Connection.Config()
}
func (c *popConnection) CurrentNamespaceName() (string, error) {
return c.ActiveNamespace(), nil
}
func (c *popConnection) CurrentClusterName() (string, error) {
return c.Connection.ActiveCluster(), nil
}
func (c *popConnection) Flags() *genericclioptions.ConfigFlags {
return c.Connection.Config().Flags()
}
func (c *popConnection) RESTConfig() (*restclient.Config, error) {
return c.Connection.Config().RESTConfig()
}

View File

@ -47,11 +47,12 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
client.NewGVR("apps/v1/statefulsets"): &StatefulSet{},
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
client.NewGVR("batch/v1/jobs"): &Job{},
// BOZO!! v1.18.0
// client.NewGVR("charts"): &Chart{},
client.NewGVR("openfaas"): &OpenFaas{},
client.NewGVR("popeye"): &Popeye{},
client.NewGVR("report"): &Sanitizer{},
client.NewGVR("sanitizer"): &Popeye{},
// BOZO!! v1.18.0
// client.NewGVR("charts"): &Chart{},
}
r, ok := m[gvr]
@ -173,10 +174,10 @@ func loadK9s(m ResourceMetas) {
Verbs: []string{},
Categories: []string{"k9s"},
}
m[client.NewGVR("report")] = metav1.APIResource{
Name: "report",
Kind: "Report",
SingularName: "report",
m[client.NewGVR("sanitizer")] = metav1.APIResource{
Name: "sanitizer",
Kind: "Sanitizer",
SingularName: "sanitizer",
Verbs: []string{},
Categories: []string{"k9s"},
}

View File

@ -1,80 +0,0 @@
package dao
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
cfg "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/popeye/pkg"
"github.com/derailed/popeye/pkg/config"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime"
)
var _ Accessor = (*Sanitizer)(nil)
// Sanitizer tracks cluster sanitization.
type Sanitizer struct {
NonResource
}
// NewSanitizer returns a new set of aliases.
func NewSanitizer(f Factory) *Sanitizer {
s := Sanitizer{}
s.Init(f, client.NewGVR("report"))
return &s
}
// List returns a collection of aliases.
func (s *Sanitizer) List(ctx context.Context, _ string) ([]runtime.Object, error) {
report, ok := ctx.Value(internal.KeyPath).(string)
if !ok {
return nil, fmt.Errorf("no sanitizer report path")
}
sections := []string{report}
js := "json"
flags := config.NewFlags()
flags.Sections = &sections
spinach := filepath.Join(cfg.K9sHome, "spinach.yml")
flags.Spinach = &spinach
flags.Output = &js
popeye, err := pkg.NewPopeye(flags, &log.Logger)
if err != nil {
return nil, err
}
popeye.SetFactory(newPopFactory(s.Factory))
if err = popeye.Init(); err != nil {
return nil, err
}
buff := readWriteCloser{Buffer: bytes.NewBufferString("")}
popeye.SetOutputTarget(buff)
if err = popeye.Sanitize(); err != nil {
return nil, err
}
var b render.Builder
if err = json.Unmarshal(buff.Bytes(), &b); err != nil {
return nil, err
}
oo := make([]runtime.Object, len(b.Report.Sections))
for i, s := range b.Report.Sections {
oo[i] = s
}
return oo, nil
}
// Get fetch a resource.
func (*Sanitizer) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("NYI!!")
}

View File

@ -27,7 +27,6 @@ type CmdBuff struct {
listeners []BuffWatcher
hotKey rune
kind BufferKind
sticky bool
active bool
}
@ -41,16 +40,6 @@ func NewCmdBuff(key rune, kind BufferKind) *CmdBuff {
}
}
// IsSticky checks if the cmd is going to perist or not.
func (c *CmdBuff) IsSticky() bool {
return c.sticky
}
// SetSticky returns cmd stickness.
func (c *CmdBuff) SetSticky(b bool) {
c.sticky = b
}
// InCmdMode checks if a command exists and the buffer is active.
func (c *CmdBuff) InCmdMode() bool {
return c.active || len(c.buff) > 0

View File

@ -32,6 +32,14 @@ func (f *FishBuff) SetSuggestionFn(fn SuggestionFunc) {
f.suggestionFn = fn
}
func (f *FishBuff) Activate() {
if f.suggestionFn == nil {
return
}
cc := f.suggestionFn(string(f.buff))
f.fireSuggest(cc)
}
// Delete removes the last character from the buffer.
func (f *FishBuff) Delete() {
f.CmdBuff.Delete()

50
internal/model/history.go Normal file
View File

@ -0,0 +1,50 @@
package model
// MaxHistory tracks max command history
const MaxHistory = 20
// History represents a command history.
type History struct {
commands []string
limit int
}
// NewHistory returns a new instance.
func NewHistory(limit int) *History {
return &History{limit: limit}
}
func (h *History) List() []string {
return h.commands
}
// Push adds a new item.
func (h *History) Push(c string) {
if i := h.indexOf(c); i != -1 {
h.commands = append(h.commands[:i], h.commands[i+1:]...)
}
if len(h.commands) < h.limit {
h.commands = append(h.commands, c)
return
}
h.commands = append(h.commands[1:], c)
}
// Clear clear out the stack using pops.
func (h *History) Clear() {
h.commands = nil
}
// Empty returns true if no history.
func (h *History) Empty() bool {
return len(h.commands) == 0
}
func (h *History) indexOf(s string) int {
for i, c := range h.commands {
if c == s {
return i
}
}
return -1
}

View File

@ -0,0 +1,30 @@
package model_test
import (
"fmt"
"testing"
"github.com/derailed/k9s/internal/model"
"github.com/stretchr/testify/assert"
)
func TestHistory(t *testing.T) {
h := model.NewHistory(3)
for i := 1; i < 5; i++ {
h.Push(fmt.Sprintf("cmd%d", i))
}
assert.Equal(t, []string{"cmd2", "cmd3", "cmd4"}, h.List())
h.Clear()
assert.True(t, h.Empty())
}
func TestHistoryDups(t *testing.T) {
h := model.NewHistory(3)
for i := 1; i < 4; i++ {
h.Push(fmt.Sprintf("cmd%d", i))
}
h.Push("cmd1")
assert.Equal(t, []string{"cmd2", "cmd3", "cmd1"}, h.List())
}

View File

@ -67,8 +67,8 @@ var Registry = map[string]ResourceMeta{
DAO: &dao.Popeye{},
Renderer: &render.Popeye{},
},
"report": {
DAO: &dao.Sanitizer{},
"sanitizer": {
DAO: &dao.Popeye{},
TreeRenderer: &xray.Section{},
},

View File

@ -179,7 +179,7 @@ func (t *Table) Peek() render.TableData {
}
func (t *Table) updater(ctx context.Context) {
defer log.Debug().Msgf("Model canceled -- %q", t.gvr)
defer log.Debug().Msgf("TABLE-MODEL canceled -- %q", t.gvr)
rate := initRefreshRate
for {

View File

@ -219,6 +219,7 @@ func (t *Tree) reconcile(ctx context.Context) error {
}
} else {
if err := treeHydrate(ctx, ns, oo, meta.TreeRenderer); err != nil {
return err
}
}

View File

@ -1,6 +1,7 @@
package render
import (
"regexp"
"sort"
"strconv"
"strings"
@ -15,6 +16,35 @@ import (
"k8s.io/apimachinery/pkg/util/duration"
)
var durationRx = regexp.MustCompile(`\A(\d*d)*?(\d*h)*?(\d*m)*?(\d*s)*?\z`)
func durationToSeconds(duration string) string {
tokens := durationRx.FindAllStringSubmatch(duration, -1)
if len(tokens) == 0 {
return duration
}
if len(tokens[0]) < 5 {
return duration
}
d, h, m, s := tokens[0][1], tokens[0][2], tokens[0][3], tokens[0][4]
var n int
if v, err := strconv.Atoi(strings.Replace(d, "d", "", 1)); err == nil {
n += v * 24 * 60 * 60
}
if v, err := strconv.Atoi(strings.Replace(h, "h", "", 1)); err == nil {
n += v * 60 * 60
}
if v, err := strconv.Atoi(strings.Replace(m, "m", "", 1)); err == nil {
n += v * 60
}
if v, err := strconv.Atoi(strings.Replace(s, "s", "", 1)); err == nil {
n += v
}
return strconv.Itoa(n)
}
// AsThousands prints a number with thousand separator.
func AsThousands(n int64) string {
p := message.NewPrinter(language.English)

View File

@ -9,6 +9,27 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestDurationToNumber(t *testing.T) {
uu := map[string]struct {
s, e string
}{
"seconds": {s: "22s", e: "22"},
"minutes": {s: "22m", e: "1320"},
"hours": {s: "12h", e: "43200"},
"days": {s: "3d", e: "259200"},
"day_hour": {s: "3d9h", e: "291600"},
"day_hour_minute": {s: "2d22h3m", e: "252180"},
"day_hour_minute_seconds": {s: "2d22h3m50s", e: "252230"},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, durationToSeconds(u.s))
})
}
}
func TestToAge(t *testing.T) {
uu := map[string]struct {
t time.Time

View File

@ -91,6 +91,7 @@ type (
// Section represents a sanitizer pass
Section struct {
Title string `json:"sanitizer" yaml:"sanitizer"`
GVR string `yaml:"gvr" json:"gvr"`
Tally *Tally `json:"tally" yaml:"tally"`
Outcome Outcome `json:"issues,omitempty" yaml:"issues,omitempty"`
}
@ -100,6 +101,7 @@ type (
Issue struct {
Group string `yaml:"group" json:"group"`
GVR string `yaml:"gvr" json:"gvr"`
Level config.Level `yaml:"level" json:"level"`
Message string `yaml:"message" json:"message"`
}

View File

@ -3,6 +3,7 @@ package render
import (
"reflect"
"sort"
"strconv"
"time"
"vbom.ml/util/sortorder"
@ -160,36 +161,21 @@ func (s RowSorter) Less(i, j int) bool {
// ----------------------------------------------------------------------------
// Helpers...
// Less return true if c1 < c2.
func Less(asc bool, c1, c2 string) bool {
if o, ok := isDurationSort(asc, c1, c2); ok {
return o
func toAgeDuration(dur string) string {
d, err := time.ParseDuration(dur)
if err != nil {
return durationToSeconds(dur)
}
return strconv.Itoa(int(d.Seconds()))
}
// Less return true if c1 < c2.
func Less(asc bool, c1, c2 string) bool {
c1, c2 = toAgeDuration(c1), toAgeDuration(c2)
b := sortorder.NaturalLess(c1, c2)
if asc {
return b
}
return !b
}
func isDurationSort(asc bool, s1, s2 string) (bool, bool) {
d1, ok1 := isDuration(s1)
d2, ok2 := isDuration(s2)
if !ok1 || !ok2 {
return false, false
}
if asc {
return d1 <= d2, true
}
return d1 >= d2, true
}
func isDuration(s string) (time.Duration, bool) {
d, err := time.ParseDuration(s)
if err != nil {
return d, false
}
return d, true
}

View File

@ -3,10 +3,8 @@ package render
import (
"fmt"
"sort"
"time"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/util/duration"
)
const (
@ -170,39 +168,25 @@ func (r RowEvents) Sort(ns string, sortCol int, ageCol bool, asc bool) {
t := RowEventSorter{NS: ns, Events: r, Index: sortCol, Asc: asc}
sort.Sort(t)
gg, kk := map[string][]string{}, make(StringSet, 0, len(r))
iids, fields := map[string][]string{}, make(StringSet, 0, len(r))
for _, re := range r {
g := re.Row.Fields[sortCol]
field := re.Row.Fields[sortCol]
if ageCol {
g = toAgeDuration(g)
}
kk = kk.Add(g)
if ss, ok := gg[g]; ok {
gg[g] = append(ss, re.Row.ID)
} else {
gg[g] = []string{re.Row.ID}
field = toAgeDuration(field)
}
fields = fields.Add(field)
iids[field] = append(iids[field], re.Row.ID)
}
ids := make([]string, 0, len(r))
for _, k := range kk {
sort.StringSlice(gg[k]).Sort()
ids = append(ids, gg[k]...)
for _, field := range fields {
sort.StringSlice(iids[field]).Sort()
ids = append(ids, iids[field]...)
}
s := IdSorter{Ids: ids, Events: r}
sort.Sort(s)
}
// Helpers...
func toAgeDuration(dur string) string {
d, err := time.ParseDuration(dur)
if err != nil {
return dur
}
return duration.HumanDuration(d)
}
// ----------------------------------------------------------------------------
// RowEventSorter sorts row events by a given colon.

View File

@ -411,9 +411,39 @@ func TestRowEventsSort(t *testing.T) {
uu := map[string]struct {
re render.RowEvents
col int
asc bool
age, asc bool
e render.RowEvents
}{
"age_time": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "1h10m10.5s"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "10.5s"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3h20m5.2s"}}},
},
col: 2,
asc: true,
age: true,
e: render.RowEvents{
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "10.5s"}}},
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "1h10m10.5s"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3h20m5.2s"}}},
},
},
"age_duration": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "32d"}}},
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "1m10s"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3h20m5s"}}},
},
col: 2,
asc: true,
age: true,
e: render.RowEvents{
{Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "1m10s"}}},
{Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3h20m5s"}}},
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "32d"}}},
},
},
"col0": {
re: render.RowEvents{
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
@ -453,7 +483,7 @@ func TestRowEventsSort(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
u.re.Sort("", u.col, false, u.asc)
u.re.Sort("", u.col, u.age, u.asc)
assert.Equal(t, u.e, u.re)
})
}

View File

@ -22,10 +22,13 @@ type App struct {
}
// NewApp returns a new app.
func NewApp(context string) *App {
func NewApp(cfg *config.Config, context string) *App {
a := App{
Application: tview.NewApplication(),
actions: make(KeyActions),
Configurator: Configurator{
Config: cfg,
},
Main: NewPages(),
flash: model.NewFlash(model.DefaultFlashDelay),
cmdBuff: model.NewFishBuff(':', model.Command),
@ -35,7 +38,7 @@ func NewApp(context string) *App {
a.views = map[string]tview.Primitive{
"menu": NewMenu(a.Styles),
"logo": NewLogo(a.Styles),
"cmd": NewCommand(a.Styles, a.cmdBuff),
"cmd": NewCommand(a.Config.K9s.NoIcons, a.Styles, a.cmdBuff),
"crumbs": NewCrumbs(a.Styles),
}

View File

@ -3,12 +3,13 @@ package ui_test
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/ui"
"github.com/stretchr/testify/assert"
)
func TestAppGetCmd(t *testing.T) {
a := ui.NewApp("")
a := ui.NewApp(config.NewConfig(nil), "")
a.Init()
a.CmdBuff().Set("blee")
@ -16,7 +17,7 @@ func TestAppGetCmd(t *testing.T) {
}
func TestAppInCmdMode(t *testing.T) {
a := ui.NewApp("")
a := ui.NewApp(config.NewConfig(nil), "")
a.Init()
a.CmdBuff().Set("blee")
assert.False(t, a.InCmdMode())
@ -26,7 +27,7 @@ func TestAppInCmdMode(t *testing.T) {
}
func TestAppResetCmd(t *testing.T) {
a := ui.NewApp("")
a := ui.NewApp(config.NewConfig(nil), "")
a.Init()
a.CmdBuff().Set("blee")
@ -36,7 +37,7 @@ func TestAppResetCmd(t *testing.T) {
}
func TestAppHasCmd(t *testing.T) {
a := ui.NewApp("")
a := ui.NewApp(config.NewConfig(nil), "")
a.Init()
a.ActivateCmd(true)
@ -47,7 +48,7 @@ func TestAppHasCmd(t *testing.T) {
}
func TestAppGetActions(t *testing.T) {
a := ui.NewApp("")
a := ui.NewApp(config.NewConfig(nil), "")
a.Init()
a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}})
@ -56,7 +57,7 @@ func TestAppGetActions(t *testing.T) {
}
func TestAppViews(t *testing.T) {
a := ui.NewApp("")
a := ui.NewApp(config.NewConfig(nil), "")
a.Init()
vv := []string{"crumbs", "logo", "cmd", "menu"}

View File

@ -9,29 +9,40 @@ import (
"github.com/gdamore/tcell"
)
const defaultPrompt = "%c> [::b]%s"
const (
defaultPrompt = "%c> [::b]%s"
defaultSpacer = 4
)
// Command captures users free from command input.
type Command struct {
*tview.TextView
activated bool
noIcons bool
icon rune
text string
suggestion string
styles *config.Styles
model *model.FishBuff
suggestions []string
suggestionIndex int
spacer int
}
// NewCommand returns a new command view.
func NewCommand(styles *config.Styles, m *model.FishBuff) *Command {
func NewCommand(noIcons bool, styles *config.Styles, m *model.FishBuff) *Command {
c := Command{
styles: styles,
noIcons: noIcons,
TextView: tview.NewTextView(),
spacer: defaultSpacer,
model: m,
suggestionIndex: -1,
}
if noIcons {
c.spacer--
}
c.SetWordWrap(true)
c.ShowCursor(true)
c.SetWrap(true)
@ -55,7 +66,7 @@ func (c *Command) keyboard(evt *tcell.EventKey) *tcell.EventKey {
case tcell.KeyCtrlW, tcell.KeyCtrlU:
c.model.Clear()
case tcell.KeyDown:
if c.text == "" || c.suggestionIndex < 0 {
if c.suggestionIndex < 0 {
return evt
}
c.suggestionIndex++
@ -64,7 +75,7 @@ func (c *Command) keyboard(evt *tcell.EventKey) *tcell.EventKey {
}
c.suggest(c.model.String(), c.suggestions[c.suggestionIndex])
case tcell.KeyUp:
if c.text == "" || c.suggestionIndex < 0 {
if c.suggestionIndex < 0 {
return evt
}
c.suggestionIndex--
@ -95,7 +106,8 @@ func (c *Command) InCmdMode() bool {
func (c *Command) activate() {
c.SetCursorIndex(len(c.text))
c.write(c.text, "")
c.write(false, c.text, "")
c.model.Activate()
}
func (c *Command) update(s string) {
@ -104,20 +116,25 @@ func (c *Command) update(s string) {
}
c.text = s
c.Clear()
c.write(s, "")
c.write(false, s, "")
}
func (c *Command) suggest(text, suggestion string) {
c.Clear()
c.write(text, suggestion)
c.write(false, text, suggestion)
}
func (c *Command) write(text, suggest string) {
c.SetCursorIndex(4 + len(text))
func (c *Command) write(append bool, text, suggest string) {
c.suggestion = suggest
c.SetCursorIndex(c.spacer + len(text))
txt := text
if suggest != "" {
txt += "[gray::-]" + suggest
}
if append {
fmt.Fprintf(c, "[gray::-]%s", suggest)
return
}
fmt.Fprintf(c, defaultPrompt, c.icon, txt)
}
@ -131,7 +148,10 @@ func (c *Command) SuggestionChanged(ss []string) {
c.suggestionIndex = -1
return
}
fmt.Fprintf(c, "[gray::-]%s", ss[c.suggestionIndex])
if c.suggestion == ss[c.suggestionIndex] {
return
}
c.write(true, c.text, ss[c.suggestionIndex])
}
// BufferChanged indicates the buffer was changed.
@ -145,7 +165,7 @@ func (c *Command) BufferActive(f bool, k model.BufferKind) {
c.SetBorder(true)
c.SetTextColor(c.styles.FgColor())
c.SetBorderColor(colorFor(k))
c.icon = iconFor(k)
c.icon = c.iconFor(k)
c.activate()
} else {
c.SetBorder(false)
@ -154,6 +174,22 @@ func (c *Command) BufferActive(f bool, k model.BufferKind) {
}
}
func (c *Command) iconFor(k model.BufferKind) rune {
if c.noIcons {
return ' '
}
switch k {
case model.Command:
return '🐶'
default:
return '🐩'
}
}
// ----------------------------------------------------------------------------
// Helpers...
func colorFor(k model.BufferKind) tcell.Color {
switch k {
case model.Command:
@ -162,12 +198,3 @@ func colorFor(k model.BufferKind) tcell.Color {
return tcell.ColorSeaGreen
}
}
func iconFor(k model.BufferKind) rune {
switch k {
case model.Command:
return '🐶'
default:
return '🐩'
}
}

View File

@ -11,7 +11,7 @@ import (
func TestCmdNew(t *testing.T) {
model := model.NewFishBuff(':', model.Command)
v := ui.NewCommand(config.NewStyles(), model)
v := ui.NewCommand(true, config.NewStyles(), model)
model.AddListener(v)
model.Set("blee")
@ -21,7 +21,7 @@ func TestCmdNew(t *testing.T) {
func TestCmdUpdate(t *testing.T) {
model := model.NewFishBuff(':', model.Command)
v := ui.NewCommand(config.NewStyles(), model)
v := ui.NewCommand(true, config.NewStyles(), model)
model.AddListener(v)
model.Set("blee")
@ -33,7 +33,7 @@ func TestCmdUpdate(t *testing.T) {
func TestCmdMode(t *testing.T) {
model := model.NewFishBuff(':', model.Command)
v := ui.NewCommand(config.NewStyles(), model)
v := ui.NewCommand(true, config.NewStyles(), model)
model.AddListener(v)
for _, f := range []bool{false, true} {

View File

@ -71,7 +71,7 @@ func (f *Flash) SetMessage(m model.LevelMessage) {
return
}
f.SetTextColor(flashColor(m.Level))
f.SetText(flashEmoji(m.Level) + " " + m.Text)
f.SetText(f.flashEmoji(m.Level) + " " + m.Text)
}
if f.testMode {
@ -81,7 +81,10 @@ func (f *Flash) SetMessage(m model.LevelMessage) {
}
}
func flashEmoji(l model.FlashLevel) string {
func (f *Flash) flashEmoji(l model.FlashLevel) string {
if f.app.Config.K9s.NoIcons {
return ""
}
switch l {
case model.FlashWarn:
return emoDoh
@ -92,6 +95,8 @@ func flashEmoji(l model.FlashLevel) string {
}
}
// Helpers...
func flashColor(l model.FlashLevel) tcell.Color {
switch l {
case model.FlashWarn:

View File

@ -5,6 +5,7 @@ import (
"testing"
"time"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/stretchr/testify/assert"
@ -21,7 +22,7 @@ func TestFlash(t *testing.T) {
"err": {l: model.FlashErr, i: "hello", e: "😡 hello\n"},
}
a := ui.NewApp("test")
a := ui.NewApp(config.NewConfig(nil), "test")
f := ui.NewFlash(a)
f.SetTestMode(true)
ctx, cancel := context.WithCancel(context.Background())

View File

@ -9,7 +9,7 @@ import (
)
func TestIndicatorReset(t *testing.T) {
i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles())
i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles())
i.SetPermanent("Blee")
i.Info("duh")
i.Reset()
@ -18,21 +18,21 @@ func TestIndicatorReset(t *testing.T) {
}
func TestIndicatorInfo(t *testing.T) {
i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles())
i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles())
i.Info("Blee")
assert.Equal(t, "[lawngreen::b] <Blee> \n", i.GetText(false))
}
func TestIndicatorWarn(t *testing.T) {
i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles())
i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles())
i.Warn("Blee")
assert.Equal(t, "[mediumvioletred::b] <Blee> \n", i.GetText(false))
}
func TestIndicatorErr(t *testing.T) {
i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles())
i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles())
i.Err("Blee")
assert.Equal(t, "[orangered::b] <Blee> \n", i.GetText(false))

View File

@ -34,6 +34,7 @@ type Table struct {
actions KeyActions
gvr client.GVR
Path string
Extras string
cmdBuff *model.CmdBuff
styles *config.Styles
viewSetting *config.ViewSetting
@ -225,7 +226,12 @@ func (t *Table) doUpdate(data render.TableData) {
c.SetTextColor(fg)
col++
}
custData.RowEvents.Sort(custData.Namespace, custData.Header.IndexOf(t.sortCol.name, false), t.sortCol.name == "AGE", t.sortCol.asc)
custData.RowEvents.Sort(
custData.Namespace,
custData.Header.IndexOf(t.sortCol.name, false),
t.sortCol.name == "AGE",
t.sortCol.asc,
)
pads := make(MaxyPad, len(custData.Header))
ComputeMaxColumns(pads, t.sortCol.name, custData.Header, custData.RowEvents)
@ -322,8 +328,13 @@ func (t *Table) Refresh() {
}
// GetSelectedRow returns the entire selected row.
func (t *Table) GetSelectedRow() render.Row {
return t.model.Peek().RowEvents[t.GetSelectedRowIndex()-1].Row
func (t *Table) GetSelectedRow(path string) (render.Row, bool) {
data := t.model.Peek()
i, ok := data.RowEvents.FindIndex(path)
if !ok {
return render.Row{}, ok
}
return data.RowEvents[i].Row, true
}
// NameColIndex returns the index of the resource name column.
@ -409,7 +420,9 @@ func (t *Table) styleTitle() string {
ns = path
}
}
if t.Extras != "" {
ns = t.Extras
}
var title string
if ns == client.ClusterScope {
title = SkinTitle(fmt.Sprintf(TitleFmt, base, rc), t.styles.Frame())

View File

@ -41,8 +41,10 @@ func TestTableSelection(t *testing.T) {
v.Update(m.Peek())
v.SelectRow(1, true)
r, ok := v.GetSelectedRow("r1")
assert.True(t, ok)
assert.Equal(t, "r1", v.GetSelectedItem())
assert.Equal(t, render.Row{ID: "r1", Fields: render.Fields{"blee", "duh", "fred"}}, v.GetSelectedRow())
assert.Equal(t, render.Row{ID: "r1", Fields: render.Fields{"blee", "duh", "fred"}}, r)
assert.Equal(t, "blee", v.GetSelectedCell(0))
assert.Equal(t, 1, v.GetSelectedRowIndex())
assert.Equal(t, []string{"r1"}, v.GetSelectedItems())

View File

@ -43,15 +43,16 @@ type App struct {
cancelFn context.CancelFunc
conRetry int32
clusterModel *model.ClusterInfo
history *model.History
}
// NewApp returns a K9s app instance.
func NewApp(cfg *config.Config) *App {
a := App{
App: ui.NewApp(cfg.K9s.CurrentContext),
App: ui.NewApp(cfg, cfg.K9s.CurrentContext),
Content: NewPageStack(),
history: model.NewHistory(model.MaxHistory),
}
a.Config = cfg
a.Views()["statusIndicator"] = ui.NewStatusIndicator(a.App, a.Styles)
a.Views()["clusterInfo"] = NewClusterInfo(&a)
@ -121,22 +122,26 @@ func (a *App) Init(version string, rate int) error {
func (a *App) suggestCommand() func(s string) (entries sort.StringSlice) {
return func(s string) (entries sort.StringSlice) {
if s == "" {
if a.history.Empty() {
return
}
return a.history.List()
}
lowS := strings.ToLower(s)
for _, k := range a.command.alias.Aliases.Keys() {
lok, los := strings.ToLower(k), strings.ToLower(s)
if lok == los {
lowK := strings.ToLower(k)
if lowK == lowS {
continue
}
if strings.HasPrefix(lok, los) {
entries = append(entries, strings.Replace(k, los, "", 1))
if strings.HasPrefix(lowK, lowS) {
entries = append(entries, strings.Replace(k, lowS, "", 1))
}
}
if len(entries) == 0 {
entries = nil
return nil
}
entries.Sort()
return
}
}

View File

@ -85,10 +85,10 @@ func (c *Command) xrayCmd(cmd string) error {
}
gvr, ok := c.alias.AsGVR(tokens[1])
if !ok {
return fmt.Errorf("Huh? `%s` Command not found", cmd)
return fmt.Errorf("Huh? `%s` command not found", cmd)
}
if !allowedXRay(gvr) {
return fmt.Errorf("Huh? `%s` Command not found", cmd)
return fmt.Errorf("Huh? `%s` command not found", cmd)
}
x := NewXray(gvr)
@ -106,6 +106,20 @@ func (c *Command) xrayCmd(cmd string) error {
return c.exec(cmd, "xrays", x, true)
}
func (c *Command) checkAccess(gvr string) error {
m, err := dao.MetaAccess.MetaFor(client.NewGVR(gvr))
if err != nil {
return err
}
ns := client.CleanseNamespace(c.app.Config.ActiveNamespace())
if dao.IsK8sMeta(m) && c.app.ConOK() {
if _, e := c.app.factory.CanForResource(ns, gvr, client.MonitorAccess); e != nil {
return e
}
}
return nil
}
// Exec the Command by showing associated display.
func (c *Command) run(cmd, path string, clearStack bool) error {
if c.specialCmd(cmd) {
@ -116,6 +130,10 @@ func (c *Command) run(cmd, path string, clearStack bool) error {
if err != nil {
return err
}
if err := c.checkAccess(gvr); err != nil {
return err
}
switch cmds[0] {
case "ctx", "context", "contexts":
if len(cmds) == 2 {
@ -184,7 +202,7 @@ func (c *Command) specialCmd(cmd string) bool {
func (c *Command) viewMetaFor(cmd string) (string, *MetaViewer, error) {
gvr, ok := c.alias.AsGVR(cmd)
if !ok {
return "", nil, fmt.Errorf("Huh? `%s` Command not found", cmd)
return "", nil, fmt.Errorf("Huh? `%s` command not found", cmd)
}
v, ok := customViewers[gvr]
@ -224,5 +242,10 @@ func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) e
c.app.Content.Stack.Clear()
}
return c.app.inject(comp)
if err := c.app.inject(comp); err != nil {
return err
}
c.app.history.Push(cmd)
return nil
}

View File

@ -62,12 +62,12 @@ func (c *Container) bindKeys(aa ui.KeyActions) {
}
func (c *Container) k9sEnv() Env {
env := defaultEnv(
c.App().Conn().Config(),
c.GetTable().GetSelectedItem(),
c.GetTable().GetModel().Peek().Header,
c.GetTable().GetSelectedRow(),
)
path := c.GetTable().GetSelectedItem()
row, ok := c.GetTable().GetSelectedRow(path)
if !ok {
log.Error().Msgf("unable to locate seleted row for %q", path)
}
env := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header, row)
env["NAMESPACE"], env["POD"] = client.Namespaced(c.GetTable().Path)
return env

View File

@ -50,9 +50,10 @@ func k8sEnv(c *client.Config) Env {
func defaultEnv(c *client.Config, path string, header render.Header, row render.Row) Env {
env := k8sEnv(c)
log.Debug().Msgf("PATH %q::%q", path, row.Fields[1])
env["NAMESPACE"], env["NAME"] = client.Namespaced(path)
for i := range header {
env["COL-"+header[i].Name] = row.Fields[i]
for _, col := range header.Columns(true) {
env["COL-"+col] = row.Fields[header.IndexOf(col, true)]
}
return env

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
@ -38,6 +39,7 @@ func (p *Popeye) Init(ctx context.Context) error {
return err
}
p.GetTable().GetModel().SetNamespace("*")
p.GetTable().GetModel().SetRefreshRate(5 * time.Second)
return nil
}
@ -52,7 +54,7 @@ func (p *Popeye) decorateRows(data render.TableData) render.TableData {
sum += n
}
score := sum / len(data.RowEvents)
p.GetTable().Path = fmt.Sprintf("Score %d -- %s", score, grade(score))
p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, grade(score))
return data
}
@ -75,7 +77,7 @@ func (p *Popeye) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
v := NewSanitizer(client.NewGVR("report"))
v := NewSanitizer(client.NewGVR("sanitizer"))
v.SetContextFn(sanitizerCtx(path))
if err := p.App().inject(v); err != nil {

View File

@ -75,7 +75,7 @@ func miscViewers(vv MetaViewers) {
vv[client.NewGVR("popeye")] = MetaViewer{
viewerFn: NewPopeye,
}
vv[client.NewGVR("report")] = MetaViewer{
vv[client.NewGVR("sanitizer")] = MetaViewer{
viewerFn: NewSanitizer,
}

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"strings"
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
@ -68,7 +67,6 @@ func (s *Sanitizer) Init(ctx context.Context) error {
s.SetGraphicsColor(s.app.Styles.Xray().GraphicColor.Color())
s.SetTitle(strings.Title(s.gvr.R()))
s.model.SetRefreshRate(time.Duration(s.app.Config.K9s.GetRefreshRate()) * time.Second)
s.model.SetNamespace(client.CleanseNamespace(s.app.Config.ActiveNamespace()))
s.model.AddListener(s)
@ -88,7 +86,7 @@ func (s *Sanitizer) Init(ctx context.Context) error {
// ExtraHints returns additional hints.
func (s *Sanitizer) ExtraHints() map[string]string {
if !s.app.Styles.Xray().ShowIcons {
if s.app.Config.K9s.NoIcons {
return nil
}
return xray.EmojiInfo()
@ -282,7 +280,7 @@ func (s *Sanitizer) TreeLoadFailed(err error) {
}
func (s *Sanitizer) update(node *xray.TreeNode) {
root := makeTreeNode(node, s.ExpandNodes(), s.app.Styles)
root := makeTreeNode(node, s.ExpandNodes(), s.app.Config.K9s.NoIcons, s.app.Styles)
if node == nil {
s.app.QueueUpdateDraw(func() {
s.SetRoot(root)
@ -329,7 +327,7 @@ func (s *Sanitizer) TreeChanged(node *xray.TreeNode) {
}
func (s *Sanitizer) hydrate(parent *tview.TreeNode, n *xray.TreeNode) {
node := makeTreeNode(n, s.ExpandNodes(), s.app.Styles)
node := makeTreeNode(n, s.ExpandNodes(), s.app.Config.K9s.NoIcons, s.app.Styles)
for _, c := range n.Children {
s.hydrate(node, c)
}

View File

@ -92,7 +92,11 @@ func (t *Table) EnvFn() EnvFunc {
func (t *Table) defaultEnv() Env {
path := t.GetSelectedItem()
env := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header, t.GetSelectedRow())
row, ok := t.GetSelectedRow(path)
if !ok {
log.Error().Msgf("unable to locate selected row for %q", path)
}
env := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header, row)
env["FILTER"] = t.SearchBuff().String()
if env["FILTER"] == "" {
env["NAMESPACE"], env["FILTER"] = client.Namespaced(path)

View File

@ -94,7 +94,7 @@ func (x *Xray) Init(ctx context.Context) error {
// ExtraHints returns additional hints.
func (x *Xray) ExtraHints() map[string]string {
if !x.app.Styles.Xray().ShowIcons {
if x.app.Config.K9s.NoIcons {
return nil
}
return xray.EmojiInfo()
@ -510,7 +510,7 @@ func (x *Xray) TreeLoadFailed(err error) {
}
func (x *Xray) update(node *xray.TreeNode) {
root := makeTreeNode(node, x.ExpandNodes(), x.app.Styles)
root := makeTreeNode(node, x.ExpandNodes(), x.app.Config.K9s.NoIcons, x.app.Styles)
if node == nil {
x.app.QueueUpdateDraw(func() {
x.SetRoot(root)
@ -557,7 +557,7 @@ func (x *Xray) TreeChanged(node *xray.TreeNode) {
}
func (x *Xray) hydrate(parent *tview.TreeNode, n *xray.TreeNode) {
node := makeTreeNode(n, x.ExpandNodes(), x.app.Styles)
node := makeTreeNode(n, x.ExpandNodes(), x.app.Config.K9s.NoIcons, x.app.Styles)
for _, c := range n.Children {
x.hydrate(node, c)
}
@ -715,10 +715,10 @@ func rxFilter(q, path string) bool {
return false
}
func makeTreeNode(node *xray.TreeNode, expanded bool, styles *config.Styles) *tview.TreeNode {
func makeTreeNode(node *xray.TreeNode, expanded bool, showIcons bool, styles *config.Styles) *tview.TreeNode {
n := tview.NewTreeNode("No data...")
if node != nil {
n.SetText(node.Title(styles.Xray()))
n.SetText(node.Title(showIcons))
n.SetReference(node.Spec())
}
n.SetSelectable(true)

View File

@ -18,7 +18,7 @@ func (s *Section) Render(ctx context.Context, ns string, o interface{}) error {
if !ok {
return fmt.Errorf("Expected Section, but got %T", o)
}
root := NewTreeNode(section.Title, section.Title)
root := NewTreeNode(section.GVR, section.Title)
parent, ok := ctx.Value(KeyParent).(*TreeNode)
if !ok {
return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent))
@ -38,37 +38,26 @@ func cleanse(s string) string {
func (c *Section) outcomeRefs(parent *TreeNode, section render.Section) {
for k, issues := range section.Outcome {
p := NewTreeNode(section.Title, cleanse(k))
p := NewTreeNode(section.GVR, cleanse(k))
parent.Add(p)
for _, i := range issues {
msg := colorize(cleanse(i.Message), i.Level)
c := NewTreeNode(fmt.Sprintf("issue_%d", i.Level), msg)
if i.Group == "__root__" {
for _, issue := range issues {
msg := colorize(cleanse(issue.Message), issue.Level)
c := NewTreeNode(fmt.Sprintf("issue_%d", issue.Level), msg)
if issue.Group == "__root__" {
p.Add(c)
continue
}
if pa := p.Find(childOf(section.Title), i.Group); pa != nil {
if pa := p.Find(issue.GVR, issue.Group); pa != nil {
pa.Add(c)
continue
}
pa := NewTreeNode(childOf(section.Title), i.Group)
pa := NewTreeNode(issue.GVR, issue.Group)
pa.Add(c)
p.Add(pa)
}
}
}
func childOf(s string) string {
switch s {
case "deployment", "statefulset", "daemonset":
return "v1/pods"
case "pod":
return "containers"
default:
return ""
}
}
func colorize(s string, l config.Level) string {
c := "green"
switch l {

View File

@ -7,7 +7,6 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/rs/zerolog/log"
"vbom.ml/util/sortorder"
@ -336,8 +335,8 @@ func (t *TreeNode) Find(gvr, id string) *TreeNode {
}
// Title computes the node title.
func (t *TreeNode) Title(styles config.Xray) string {
return t.computeTitle(styles)
func (t *TreeNode) Title(noIcons bool) string {
return t.computeTitle(noIcons)
}
// ----------------------------------------------------------------------------
@ -384,8 +383,8 @@ func category(gvr string) string {
return meta.SingularName
}
func (t TreeNode) computeTitle(styles config.Xray) string {
if styles.ShowIcons {
func (t TreeNode) computeTitle(noIcons bool) string {
if !noIcons {
return t.toEmojiTitle()
}
@ -473,20 +472,22 @@ func toEmoji(gvr string) string {
return ic
}
switch gvr {
case "replicasets", "replicaset":
case "apps/v1/replicasets":
return "👯‍♂️"
case "nodes", "node":
case "v1/nodes":
return "🖥 "
case "horizontalpodautoscalers", "horizontalpodautoscaler":
case "autoscaling/v1/horizontalpodautoscalers":
return "♎️"
case "clusterrolebindings", "clusterrolebinding", "clusterroles", "clusterrole":
case "rbac.authorization.k8s.io/v1/clusterrolebindings", "rbac.authorization.k8s.io/v1/clusterroles":
return "👩‍"
case "rolebindings", "rolebinding", "roles", "role":
case "rbac.authorization.k8s.io/v1/rolebindings", "rbac.authorization.k8s.io/v1/roles":
return "👨🏻‍"
case "networkpolicies", "networkpolicy":
case "networking.k8s.io/v1/networkpolicies":
return "📕"
case "poddisruptionbudgets", "poddisruptionbudget":
case "policy/v1beta1/poddisruptionbudgets":
return "🏷 "
case "policy/v1beta1/podsecuritypolicies":
return "👮‍♂️"
case "issue_0":
return "👍"
case "issue_1":
@ -504,29 +505,29 @@ func toEmoji(gvr string) string {
func toEmojiXRay(gvr string) string {
switch gvr {
case "containers", "container":
case "containers":
return "🐳"
case "v1/namespaces", "namespaces", "namespace":
case "v1/namespaces":
return "🗂 "
case "v1/pods", "pods", "pod":
case "v1/pods":
return "🚛"
case "v1/services", "services", "service":
case "v1/services":
return "💁‍♀️"
case "v1/serviceaccounts", "serviceaccounts", "serviceaccount":
case "v1/serviceaccounts":
return "💳"
case "v1/persistentvolumes", "persistentvolumes", "persistentvolume":
case "v1/persistentvolumes":
return "📚"
case "v1/persistentvolumeclaims", "persistentvolumeclaims", "persistentvolumeclaim":
case "v1/persistentvolumeclaims":
return "🎟 "
case "v1/secrets", "secrets", "secret":
case "v1/secrets":
return "🔒"
case "v1/configmaps", "configmaps", "configmap":
case "v1/configmaps":
return "🗺 "
case "apps/v1/deployments", "deployments", "deployment":
case "apps/v1/deployments":
return "🪂"
case "apps/v1/statefulsets", "statefulsets", "statefulset":
case "apps/v1/statefulsets":
return "🎎"
case "apps/v1/daemonsets", "daemonsets", "daemonset":
case "apps/v1/daemonsets":
return "😈"
default:
return ""