checkpoint
parent
402b0e440e
commit
e211611d4e
|
|
@ -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
|
||||
|
|
|
|||
83
README.md
83
README.md
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
4
go.mod
|
|
@ -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
2
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = §ions
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = §ions
|
||||
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!!")
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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{},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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 '🐩'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
Loading…
Reference in New Issue