Rel v0.40.8 (#3198)

* Update deprecated yaml.v2->v3

* [Fix] fix issue with yaml sanitization when [] or [xxx] are present

* Spring cleanup

* fix#3192

* Column Blow Reloaded

* Add ability to use alias when specifying custom views
mine
Fernand Galiana 2025-03-11 18:15:20 -06:00 committed by GitHub
parent 5e05221a26
commit 45c2137df8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 370 additions and 956 deletions

View File

@ -13,7 +13,7 @@ import (
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/k9s/internal/ui"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
func infoCmd() *cobra.Command {

View File

@ -20,7 +20,6 @@ import (
"github.com/derailed/k9s/internal/view"
"github.com/lmittmann/tint"
// "github.com/MatusOllah/slogcolor"
"github.com/mattn/go-colorable"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
@ -134,6 +133,12 @@ func loadConfiguration() (*config.Config, error) {
k9sCfg := config.NewConfig(k8sCfg)
var errs error
conn, err := client.InitConnection(k8sCfg, slog.Default())
if err != nil {
errs = errors.Join(errs, err)
}
k9sCfg.SetConnection(conn)
if err := k9sCfg.Load(config.AppConfigFile, false); err != nil {
errs = errors.Join(errs, err)
}
@ -143,10 +148,6 @@ func loadConfiguration() (*config.Config, error) {
errs = errors.Join(errs, err)
}
conn, err := client.InitConnection(k8sCfg, slog.Default())
if err != nil {
errs = errors.Join(errs, err)
}
// Try to access server version if that fail. Connectivity issue?
if !conn.CheckConnectivity() {
errs = errors.Join(errs, fmt.Errorf("cannot connect to context: %s", k9sCfg.K9s.ActiveContextName()))
@ -157,7 +158,7 @@ func loadConfiguration() (*config.Config, error) {
} else {
slog.Info("✅ Kubernetes connectivity OK")
}
k9sCfg.SetConnection(conn)
if err := k9sCfg.Save(false); err != nil {
slog.Error("K9s config save failed", slogs.Error, err)
errs = errors.Join(errs, err)

3
go.mod
View File

@ -9,7 +9,6 @@ require (
github.com/anchore/syft v1.20.0
github.com/atotto/clipboard v0.1.4
github.com/cenkalti/backoff/v4 v4.3.0
github.com/derailed/popeye v0.11.3
github.com/derailed/tcell/v2 v2.3.1-rc.3
github.com/derailed/tview v0.8.5
github.com/fatih/color v1.18.0
@ -27,7 +26,6 @@ require (
github.com/stretchr/testify v1.10.0
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/text v0.23.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.17.1
k8s.io/api v0.32.2
@ -266,7 +264,6 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/rubenv/sql-migrate v1.7.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/saferwall/pe v1.5.6 // indirect

11
go.sum
View File

@ -418,7 +418,6 @@ github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9Fqctt
github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -432,8 +431,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M=
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk=
github.com/derailed/popeye v0.11.3 h1:gQUp6zuSIRDBdyLS1Ln0nFs8FbQ+KGE+iQxe0w4Ug8M=
github.com/derailed/popeye v0.11.3/go.mod h1:HygqX7A8BwidorJjJUnWDZ5AvbxHIU7uRwXgOtn9GwY=
github.com/derailed/tcell/v2 v2.3.1-rc.3 h1:9s1fmyRcSPRlwr/C9tcpJKCujbrtmPpST6dcMUD2piY=
github.com/derailed/tcell/v2 v2.3.1-rc.3/go.mod h1:nf68BEL8fjmXQHJT3xZjoZFs2uXOzyJcNAQqGUEMrFY=
github.com/derailed/tview v0.8.5 h1:pogM/OnWlgDo6j4zyzdiIXh7E7+eT7D4CPfBnyaETug=
@ -890,7 +887,6 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@ -900,8 +896,6 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
@ -1084,9 +1078,6 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4=
github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@ -1539,11 +1530,9 @@ golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

View File

@ -54,11 +54,9 @@ func (c *Config) CallTimeout() time.Duration {
func (c *Config) RESTConfig() (*restclient.Config, error) {
cfg, err := c.clientConfig().ClientConfig()
if err != nil {
return nil, err
}
if c.proxy != nil {
cfg.Proxy = c.proxy
}
@ -192,15 +190,11 @@ func (c *Config) GetContext(n string) (*api.Context, error) {
return nil, fmt.Errorf("getcontext - invalid context specified: %q", n)
}
// SetProxy sets the proxy function.
func (c *Config) SetProxy(proxy func(*http.Request) (*url.URL, error)) {
c.proxy = proxy
}
func (c *Config) WithProxy(proxy func(*http.Request) (*url.URL, error)) *Config {
c.SetProxy(proxy)
return c
}
// Contexts fetch all available contexts.
func (c *Config) Contexts() (map[string]*api.Context, error) {
cfg, err := c.RawConfig()

View File

@ -13,7 +13,7 @@ import (
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/json"
"github.com/derailed/k9s/internal/slogs"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
// Alias tracks shortname to GVR mappings.
@ -176,8 +176,6 @@ func (a *Aliases) loadDefaultAliases() {
a.declare("help", "h", "?")
a.declare("quit", "q", "q!", "qa", "Q")
a.declare("aliases", "alias", "a")
// !!BOZO!!
// a.declare("popeye", "pop")
a.declare("helm", "charts", "chart", "hm")
a.declare("dir", "d")
a.declare("contexts", "context", "ctx")
@ -202,10 +200,6 @@ func (a *Aliases) SaveAliases(path string) error {
if err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil {
return err
}
cfg, err := yaml.Marshal(a)
if err != nil {
return err
}
return os.WriteFile(path, cfg, data.DefaultFileMod)
return data.SaveYAML(path, a)
}

View File

@ -7,7 +7,7 @@ import (
"net/http"
"os"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
// K9sBench the name of the benchmarks config file.

View File

@ -15,7 +15,7 @@ import (
"github.com/derailed/k9s/internal/config/json"
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/k9s/internal/view/cmd"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
@ -289,14 +289,13 @@ func (c *Config) SaveFile(path string) error {
return err
}
cfg, err := yaml.Marshal(c)
if err != nil {
if err := data.SaveYAML(path, c); err != nil {
slog.Error("Unable to save K9s config file", slogs.Error, err)
return err
}
slog.Info("[CONFIG] Saving K9s config to disk", slogs.Path, path)
return os.WriteFile(path, cfg, data.DefaultFileMod)
return nil
}
// Validate the configuration.

View File

@ -9,7 +9,7 @@ import (
"sync"
"github.com/derailed/k9s/internal/client"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"k8s.io/client-go/tools/clientcmd/api"
)

View File

@ -14,7 +14,7 @@ import (
"github.com/derailed/k9s/internal/config/json"
"github.com/derailed/k9s/internal/slogs"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"k8s.io/client-go/tools/clientcmd/api"
)
@ -74,12 +74,8 @@ func (d *Dir) Save(path string, c *Config) error {
if err := EnsureDirPath(path, DefaultDirMod); err != nil {
return err
}
cfg, err := yaml.Marshal(c)
if err != nil {
return err
}
return os.WriteFile(path, cfg, DefaultFileMod)
return SaveYAML(path, c)
}
func (d *Dir) loadConfig(path string) (*Config, error) {

View File

@ -12,7 +12,7 @@ import (
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/mock"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"k8s.io/cli-runtime/pkg/genericclioptions"
)

View File

@ -4,11 +4,14 @@
package data
import (
"bytes"
"errors"
"io/fs"
"os"
"path/filepath"
"regexp"
"gopkg.in/yaml.v3"
)
const envFGNodeShell = "K9S_FEATURE_GATE_NODE_SHELL"
@ -48,3 +51,28 @@ func EnsureFullPath(path string, mod os.FileMode) error {
return nil
}
// WriteYAML writes a yaml file to bytes.
func WriteYAML(content any) ([]byte, error) {
buff := bytes.NewBuffer(nil)
ec := yaml.NewEncoder(buff)
ec.SetIndent(2)
if err := ec.Encode(content); err != nil {
return nil, err
}
return buff.Bytes(), nil
}
// SaveYAML writes a yaml file to disk.
func SaveYAML(path string, content any) error {
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, DefaultFileMod)
if err != nil {
return err
}
ec := yaml.NewEncoder(f)
ec.SetIndent(2)
return ec.Encode(content)
}

View File

@ -59,15 +59,15 @@ func (n *Namespace) merge(old *Namespace) {
}
// Validate validates a namespace is setup correctly.
func (n *Namespace) Validate(c client.Connection) {
func (n *Namespace) Validate(conn client.Connection) {
n.mx.RLock()
defer n.mx.RUnlock()
if c == nil || !c.IsValidNamespace(n.Active) {
if conn == nil || !conn.IsValidNamespace(n.Active) {
return
}
for _, ns := range n.Favorites {
if !c.IsValidNamespace(ns) {
if !conn.IsValidNamespace(ns) {
slog.Debug("Invalid favorite found",
slogs.Namespace, ns,
slogs.AllNS, n.isAllNamespaces(),

View File

@ -12,7 +12,7 @@ import (
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/json"
"github.com/derailed/k9s/internal/slogs"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
// HotKeys represents a collection of plugins.

View File

@ -36,9 +36,6 @@ type K9s struct {
Logger Logger `json:"logger" yaml:"logger"`
Thresholds Threshold `json:"thresholds" yaml:"thresholds"`
manualRefreshRate int
manualHeadless *bool
manualLogoless *bool
manualCrumbsless *bool
manualReadOnly *bool
manualCommand *string
manualScreenDumpDir *string
@ -293,9 +290,9 @@ func (k *K9s) Override(k9sFlags *Flags) {
k.manualRefreshRate = *k9sFlags.RefreshRate
}
k.manualHeadless = k9sFlags.Headless
k.manualLogoless = k9sFlags.Logoless
k.manualCrumbsless = k9sFlags.Crumbsless
k.UI.manualHeadless = k9sFlags.Headless
k.UI.manualLogoless = k9sFlags.Logoless
k.UI.manualCrumbsless = k9sFlags.Crumbsless
if k9sFlags.ReadOnly != nil && *k9sFlags.ReadOnly {
k.manualReadOnly = k9sFlags.ReadOnly
}
@ -309,7 +306,7 @@ func (k *K9s) Override(k9sFlags *Flags) {
// IsHeadless returns headless setting.
func (k *K9s) IsHeadless() bool {
if IsBoolSet(k.manualHeadless) {
if IsBoolSet(k.UI.manualHeadless) {
return true
}
@ -318,7 +315,7 @@ func (k *K9s) IsHeadless() bool {
// IsLogoless returns logoless setting.
func (k *K9s) IsLogoless() bool {
if IsBoolSet(k.manualLogoless) {
if IsBoolSet(k.UI.manualLogoless) {
return true
}
@ -327,7 +324,7 @@ func (k *K9s) IsLogoless() bool {
// IsCrumbsless returns crumbsless setting.
func (k *K9s) IsCrumbsless() bool {
if IsBoolSet(k.manualCrumbsless) {
if IsBoolSet(k.UI.manualCrumbsless) {
return true
}

View File

@ -66,17 +66,17 @@ func Test_k9sOverrides(t *testing.T) {
ReadOnly: false,
NoExitOnCtrlC: false,
UI: UI{
Headless: false,
Logoless: false,
Crumbsless: false,
Headless: false,
Logoless: false,
Crumbsless: false,
manualHeadless: &true,
manualLogoless: &true,
manualCrumbsless: &true,
},
SkipLatestRevCheck: false,
DisablePodCounting: false,
manualRefreshRate: 100,
manualReadOnly: &true,
manualHeadless: &true,
manualLogoless: &true,
manualCrumbsless: &true,
manualCommand: &cmd,
manualScreenDumpDir: &dir,
},

View File

@ -4,6 +4,7 @@
package config
import (
"bytes"
"errors"
"fmt"
"io/fs"
@ -16,7 +17,7 @@ import (
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/json"
"github.com/derailed/k9s/internal/slogs"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
const k9sPluginsDir = "k9s/plugins"
@ -88,10 +89,14 @@ func (p Plugins) loadPluginDir(dir string) error {
if err != nil {
errs = errors.Join(errs, err)
}
d := yaml.NewDecoder(bytes.NewReader(fileContent))
d.KnownFields(true)
var plugin Plugin
if err = yaml.UnmarshalStrict(fileContent, &plugin); err != nil {
if err = d.Decode(&plugin); err != nil {
var plugins Plugins
if err = yaml.UnmarshalStrict(fileContent, &plugins); err != nil {
if err = d.Decode(&plugins); err != nil {
return fmt.Errorf("cannot parse %s into either a single plugin nor plugins: %w", fileName, err)
}
for name, plugin := range plugins.Plugins {

View File

@ -11,7 +11,7 @@ import (
"github.com/derailed/k9s/internal/config/json"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
// StyleListener represents a skin's listener.

View File

@ -17,3 +17,9 @@ views:
- AGE
- NAME
- IP
bozo:
columns:
- DUH
- BLAH
- BLEE

View File

@ -34,4 +34,8 @@ type UI struct {
// DefaultsToFullScreen toggles fullscreen on views like logs, yaml, details.
DefaultsToFullScreen bool `json:"defaultsToFullScreen" yaml:"defaultsToFullScreen"`
manualHeadless *bool
manualLogoless *bool
manualCrumbsless *bool
}

View File

@ -15,10 +15,11 @@ import (
"slices"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/json"
"github.com/derailed/k9s/internal/slogs"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
// ViewConfigListener represents a view config listener.
@ -118,29 +119,65 @@ func (v *CustomView) Load(path string) error {
return nil
}
// AddListeners registers a new listener for various commands.
func (v *CustomView) AddListeners(l ViewConfigListener, cmds ...string) {
for _, cmd := range cmds {
if cmd != "" {
v.listeners[cmd] = l
}
}
v.fireConfigChanged()
}
// AddListener registers a new listener.
func (v *CustomView) AddListener(gvr string, l ViewConfigListener) {
v.listeners[gvr] = l
func (v *CustomView) AddListener(cmd string, l ViewConfigListener) {
v.listeners[cmd] = l
v.fireConfigChanged()
}
// RemoveListener unregister a listener.
func (v *CustomView) RemoveListener(gvr string) {
delete(v.listeners, gvr)
}
func (v *CustomView) fireConfigChanged() {
for gvr, list := range v.listeners {
if vs := v.getVS(gvr, list.GetNamespace()); vs == nil {
list.ViewSettingsChanged(nil)
} else {
slog.Debug("Reloading custom view settings", slogs.GVR, gvr)
list.ViewSettingsChanged(vs)
func (v *CustomView) RemoveListener(l ViewConfigListener) {
for k, list := range v.listeners {
if list == l {
delete(v.listeners, k)
}
}
}
func (v *CustomView) fireConfigChanged() {
cmds := slices.Collect(maps.Keys(v.listeners))
slices.SortFunc(cmds, func(a, b string) int {
switch {
case strings.Contains(a, "/") && !strings.Contains(b, "/"):
return 1
case !strings.Contains(a, "/") && strings.Contains(b, "/"):
return -1
default:
return strings.Compare(a, b)
}
})
type tuple struct {
cmd string
vs *ViewSetting
}
var victim tuple
for _, cmd := range cmds {
if vs := v.getVS(cmd, v.listeners[cmd].GetNamespace()); vs != nil {
slog.Debug("Reloading custom view settings", slogs.Command, cmd)
victim = tuple{cmd, vs}
break
}
victim = tuple{cmd, nil}
}
if victim.cmd != "" {
v.listeners[victim.cmd].ViewSettingsChanged(victim.vs)
}
}
func (v *CustomView) getVS(gvr, ns string) *ViewSetting {
if client.IsAllNamespaces(ns) {
ns = client.NamespaceAll
}
k := gvr
kk := slices.Collect(maps.Keys(v.Views))
slices.SortFunc(kk, func(s1, s2 string) int {

View File

@ -17,6 +17,10 @@ func TestCustomView_getVS(t *testing.T) {
}{
"empty": {},
"miss": {
gvr: "zorg",
},
"gvr": {
gvr: "v1/pods",
e: &ViewSetting{
@ -40,6 +44,13 @@ func TestCustomView_getVS(t *testing.T) {
},
},
"alias": {
gvr: "bozo",
e: &ViewSetting{
Columns: []string{"DUH", "BLAH", "BLEE"},
},
},
"toast-no-ns": {
gvr: "v1/pods",
ns: "zorg",

View File

@ -10,9 +10,9 @@ import (
"os"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/render/helm"
"github.com/derailed/k9s/internal/slogs"
"gopkg.in/yaml.v2"
"helm.sh/helm/v3/pkg/action"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@ -83,7 +83,7 @@ func (h *HelmChart) GetValues(path string, allValues bool) ([]byte, error) {
return nil, err
}
return yaml.Marshal(resp)
return data.WriteYAML(resp)
}
// Describe returns the chart notes.

View File

@ -9,13 +9,13 @@ import (
"strconv"
"strings"
"gopkg.in/yaml.v2"
"helm.sh/helm/v3/pkg/action"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/render/helm"
)
@ -126,10 +126,14 @@ func (h *HelmHistory) GetValues(path string, allValues bool) ([]byte, error) {
return nil, fmt.Errorf("expected helm.ReleaseRes, but got %T", rel)
}
var content any
if allValues {
return yaml.Marshal(resp.Release.Chart.Values)
content = resp.Release.Chart.Values
} else {
content = resp.Release.Config
}
return yaml.Marshal(resp.Release.Config)
return data.WriteYAML(content)
}
func (h *HelmHistory) Rollback(_ context.Context, path, rev string) error {

View File

@ -1,142 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package dao
// !!BOZO!! Popeye
// import (
// "bytes"
// "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"
// "github.com/derailed/popeye/pkg"
// "github.com/derailed/popeye/pkg/config"
// "github.com/derailed/popeye/types"
// "k8s.io/apimachinery/pkg/runtime"
// )
// var _ Accessor = (*Popeye)(nil)
// // Popeye tracks cluster sanitization.
// type Popeye struct {
// NonResource
// }
// // NewPopeye returns a new set of aliases.
// func NewPopeye(f Factory) *Popeye {
// a := Popeye{}
// a.Init(f, client.NewGVR("popeye"))
// return &a
// }
// type readWriteCloser struct {
// *bytes.Buffer
// }
// // Close close read stream.
// func (readWriteCloser) Close() error {
// return nil
// }
// // List returns a collection of aliases.
// func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// defer func(t time.Time) {
// log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t))
// if err := recover(); err != nil {
// log.Debug().Msgf("POPEYE DIED!")
// }
// }(time.Now())
// flags, js := config.NewFlags(), "json"
// flags.Output = &js
// flags.ActiveNamespace = &ns
// if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" {
// ns, n := client.Namespaced(report)
// sections := []string{n}
// flags.Sections = &sections
// flags.ActiveNamespace = &ns
// }
// spinach := filepath.Join(cfg.AppConfigDir, "spinach.yaml")
// if c, err := p.getFactory().Client().Config().CurrentContextName(); err == nil {
// spinach = filepath.Join(cfg.AppConfigDir, fmt.Sprintf("%s_spinach.yaml", c))
// }
// if _, err := os.Stat(spinach); err == nil {
// flags.Spinach = &spinach
// }
// popeye, err := pkg.NewPopeye(flags, &log.Logger)
// if err != nil {
// return nil, err
// }
// popeye.SetFactory(newPopeyeFactory(p.Factory))
// if err = popeye.Init(); err != nil {
// return nil, err
// }
// buff := readWriteCloser{Buffer: bytes.NewBufferString("")}
// popeye.SetOutputTarget(buff)
// if _, _, err = popeye.Sanitize(); err != nil {
// log.Error().Err(err).Msgf("BOOM %#v", *flags.Sections)
// return nil, err
// }
// var b render.Builder
// if err = json.Unmarshal(buff.Bytes(), &b); err != nil {
// return nil, err
// }
// oo := make([]runtime.Object, 0, len(b.Report.Sections))
// sort.Sort(b.Report.Sections)
// for _, s := range b.Report.Sections {
// s.Tally.Count = len(s.Outcome)
// if s.Tally.Sum() > 0 {
// oo = append(oo, s)
// }
// }
// return oo, nil
// }
// // Get retrieves a resource.
// func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) {
// return nil, errors.New("NYI!!")
// }
// // ----------------------------------------------------------------------------
// // Helpers...
// type popFactory struct {
// Factory
// }
// var _ types.Factory = (*popFactory)(nil)
// func newPopeyeFactory(f Factory) *popFactory {
// return &popFactory{Factory: f}
// }
// func (p *popFactory) Client() types.Connection {
// return &popeyeConnection{Connection: p.Factory.Client()}
// }
// type popeyeConnection struct {
// client.Connection
// }
// var _ types.Connection = (*popeyeConnection)(nil)
// func (c *popeyeConnection) Config() types.Config {
// return c.Connection.Config()
// }

View File

@ -242,21 +242,6 @@ func loadK9s(m ResourceMetas) {
Verbs: []string{},
Categories: []string{k9sCat},
}
m[client.NewGVR("popeye")] = metav1.APIResource{
Name: "popeye",
Kind: "Popeye",
SingularName: "popeye",
Namespaced: true,
Verbs: []string{},
Categories: []string{k9sCat},
}
m[client.NewGVR("sanitizer")] = metav1.APIResource{
Name: "sanitizer",
Kind: "Sanitizer",
SingularName: "sanitizer",
Verbs: []string{},
Categories: []string{k9sCat},
}
m[client.NewGVR("contexts")] = metav1.APIResource{
Name: "contexts",
Kind: "Contexts",

View File

@ -9,6 +9,7 @@ import (
"testing"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"github.com/stretchr/testify/assert"
@ -291,14 +292,15 @@ func makeC(n string) c {
return c{name: n}
}
func (c) InCmdMode() bool { return false }
func (c c) Name() string { return c.name }
func (c c) Hints() model.MenuHints { return nil }
func (c c) HasFocus() bool { return false }
func (c c) ExtraHints() map[string]string { return nil }
func (c c) Draw(tcell.Screen) {}
func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }
func (c c) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
func (c) InCmdMode() bool { return false }
func (c c) Name() string { return c.name }
func (c) SetCommand(*cmd.Interpreter) {}
func (c) Hints() model.MenuHints { return nil }
func (c) HasFocus() bool { return false }
func (c) ExtraHints() map[string]string { return nil }
func (c) Draw(tcell.Screen) {}
func (c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }
func (c) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
return nil
}
func (c c) SetRect(int, int, int, int) {}

View File

@ -10,6 +10,7 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tview"
"github.com/sahilm/fuzzy"
"k8s.io/apimachinery/pkg/runtime"
@ -93,6 +94,13 @@ type Component interface {
Hinter
Commander
Filterer
Viewer
}
// Viewer represents a resource viewer.
type Viewer interface {
// SetCommand sets the current command.
SetCommand(*cmd.Interpreter)
}
type Filterer interface {

View File

@ -48,6 +48,7 @@ type Renderer interface {
// ColorerFunc returns a row colorer function.
ColorerFunc() ColorerFunc
// SetViewSetting sets custom view settings if any.
SetViewSetting(vs *config.ViewSetting)
}

View File

@ -42,6 +42,7 @@ func (b *Base) doHeader(dh model1.Header) model1.Header {
return b.specs.Header(dh)
}
// SetViewSetting sets custom view settings if any.
func (b *Base) SetViewSetting(vs *config.ViewSetting) {
var cols []string
b.vs = vs

View File

@ -1,195 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package render
import "github.com/derailed/popeye/pkg/config"
// !!BOZO!! Popeye
// // Popeye renders a sanitizer to screen.
// type Popeye struct {
// Base
// }
// // ColorerFunc colors a resource row.
// func (Popeye) ColorerFunc() ColorerFunc {
// return func(ns string, h Header, re *model1.RowEvent) tcell.Color {
// c := DefaultColorer(ns, h, re)
// warnCol := h.IndexOf("WARNING", true)
// status, _ := strconv.Atoi(strings.TrimSpace(re.Row.Fields[warnCol]))
// if status > 0 {
// c = tcell.ColorOrange
// }
// errCol := h.IndexOf("ERROR", true)
// status, _ = strconv.Atoi(strings.TrimSpace(re.Row.Fields[errCol]))
// if status > 0 {
// c = ErrColor
// }
// return c
// }
// }
// // Header returns a header row.
// func (Popeye) Header(ns string) model1.Header {
// return model1.Header{
// model1.HeaderColumn{Name: "RESOURCE"},
// model1.HeaderColumn{Name: "SCORE%", Align: tview.AlignRight},
// model1.HeaderColumn{Name: "SCANNED", Align: tview.AlignRight},
// model1.HeaderColumn{Name: "ERROR", Align: tview.AlignRight},
// model1.HeaderColumn{Name: "WARNING", Align: tview.AlignRight},
// model1.HeaderColumn{Name: "INFO", Align: tview.AlignRight},
// model1.HeaderColumn{Name: "OK", Align: tview.AlignRight},
// }
// }
// // Render renders a K8s resource to screen.
// func (Popeye) Render(o interface{}, ns string, r *model1.Row) error {
// s, ok := o.(Section)
// if !ok {
// return fmt.Errorf("expected Section, but got %T", o)
// }
// r.ID = client.FQN(ns, s.Title)
// r.Fields = append(r.Fields,
// s.Title,
// strconv.Itoa(s.Tally.Score()),
// strconv.Itoa(s.Tally.OK+s.Tally.Info+s.Tally.Warning+s.Tally.Error),
// strconv.Itoa(s.Tally.Error),
// strconv.Itoa(s.Tally.Warning),
// strconv.Itoa(s.Tally.Info),
// strconv.Itoa(s.Tally.OK),
// )
// return nil
// }
// // ----------------------------------------------------------------------------
// // Helpers...
type (
// // Builder represents a popeye report.
// Builder struct {
// Report Report `json:"popeye" yaml:"popeye"`
// }
// // Report represents the output of a sanitization pass.
// Report struct {
// Score int `json:"score" yaml:"score"`
// Grade string `json:"grade" yaml:"grade"`
// Sections Sections `json:"sanitizers,omitempty" yaml:"sanitizers,omitempty"`
// }
// Sections represents a collection of sections.
Sections []Section
// 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"`
}
// Outcome represents a classification of reports outcome.
Outcome map[string]Issues
// Issues represents a collection of issues.
Issues []Issue
// Issue represents a sanitization issue.
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"`
}
// Tally tracks a section scores.
Tally struct {
OK, Info, Warning, Error int
Count int
}
)
// // Sum sums up tally counts.
// func (t *Tally) Sum() int {
// return t.OK + t.Info + t.Warning + t.Error
// }
// // Score returns the overall sections score in percent.
// func (t *Tally) Score() int {
// oks := t.OK + t.Info
// return toPerc(float64(oks), float64(oks+t.Warning+t.Error))
// }
// func toPerc(v1, v2 float64) int {
// if v2 == 0 {
// return 0
// }
// return int(math.Floor((v1 / v2) * 100))
// }
// // Len returns a section length.
// func (s Sections) Len() int {
// return len(s)
// }
// // Swap swaps values.
// func (s Sections) Swap(i, j int) {
// s[i], s[j] = s[j], s[i]
// }
// // Less compares section scores.
// func (s Sections) Less(i, j int) bool {
// t1, t2 := s[i].Tally, s[j].Tally
// return t1.Score() < t2.Score()
// }
// // GetObjectKind returns a schema object.
// func (Section) GetObjectKind() schema.ObjectKind {
// return nil
// }
// // DeepCopyObject returns a container copy.
// func (s Section) DeepCopyObject() runtime.Object {
// return s
// }
// // MaxSeverity gather the max severity in a collection of issues.
// func (s Section) MaxSeverity() config.Level {
// max := config.OkLevel
// for _, issues := range s.Outcome {
// m := issues.MaxSeverity()
// if m > max {
// max = m
// }
// }
// return max
// }
// // MaxSeverity gather the max severity in a collection of issues.
// func (i Issues) MaxSeverity() config.Level {
// max := config.OkLevel
// for _, is := range i {
// if is.Level > max {
// max = is.Level
// }
// }
// return max
// }
// // CountSeverity counts severity level instances.
// func (i Issues) CountSeverity(l config.Level) int {
// var count int
// for _, is := range i {
// if is.Level == l {
// count++
// }
// }
// return count
// }

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package render
// Level tracks lint check level.
type Level int
const (
// OkLevel denotes no linting issues.
OkLevel Level = iota
// InfoLevel denotes FIY linting issues.
InfoLevel
// WarnLevel denotes a warning issue.
WarnLevel
// ErrorLevel denotes a serious issue.
ErrorLevel
)
type (
// Sections represents a collection of sections.
Sections []Section
// Section represents a sanitizer pass.
Section struct {
Title string `json:"sanitizer" yaml:"sanitizer"`
GVR string `yaml:"gvr" json:"gvr"`
Outcome Outcome `json:"issues,omitempty" yaml:"issues,omitempty"`
}
// Outcome represents a classification of reports outcome.
Outcome map[string]Issues
// Issues represents a collection of issues.
Issues []Issue
// Issue represents a sanitization issue.
Issue struct {
Group string `yaml:"group" json:"group"`
GVR string `yaml:"gvr" json:"gvr"`
Level Level `yaml:"level" json:"level"`
Message string `yaml:"message" json:"message"`
}
)

View File

@ -32,11 +32,19 @@ type synchronizer interface {
type Configurator struct {
Config *config.Config
Styles *config.Styles
CustomView *config.CustomView
customView *config.CustomView
BenchFile string
skinFile string
}
func (c *Configurator) CustomView() *config.CustomView {
if c.customView == nil {
c.customView = config.NewCustomView()
}
return c.customView
}
// HasSkin returns true if a skin file was located.
func (c *Configurator) HasSkin() bool {
return c.skinFile != ""
@ -82,13 +90,9 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e
// RefreshCustomViews load view configuration changes.
func (c *Configurator) RefreshCustomViews() error {
if c.CustomView == nil {
c.CustomView = config.NewCustomView()
} else {
c.CustomView.Reset()
}
c.CustomView().Reset()
return c.CustomView.Load(config.AppViewsFile)
return c.CustomView().Load(config.AppViewsFile)
}
// SkinsDirWatcher watches for skin directory file changes.

View File

@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"github.com/stretchr/testify/assert"
@ -39,23 +40,24 @@ func makeComponent(n string) c {
return c{name: n}
}
func (c) InCmdMode() bool { return false }
func (c c) HasFocus() bool { return true }
func (c c) Hints() model.MenuHints { return nil }
func (c c) ExtraHints() map[string]string { return nil }
func (c c) Name() string { return c.name }
func (c c) Draw(tcell.Screen) {}
func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }
func (c c) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
func (c) SetCommand(*cmd.Interpreter) {}
func (c) InCmdMode() bool { return false }
func (c) HasFocus() bool { return true }
func (c) Hints() model.MenuHints { return nil }
func (c) ExtraHints() map[string]string { return nil }
func (c c) Name() string { return c.name }
func (c) Draw(tcell.Screen) {}
func (c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }
func (c) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {
return nil
}
func (c c) SetRect(int, int, int, int) {}
func (c c) GetRect() (int, int, int, int) { return 0, 0, 0, 0 }
func (c c) GetFocusable() tview.Focusable { return c }
func (c c) Focus(func(tview.Primitive)) {}
func (c c) Blur() {}
func (c c) Start() {}
func (c c) Stop() {}
func (c c) Init(context.Context) error { return nil }
func (c c) SetFilter(string) {}
func (c c) SetLabelFilter(map[string]string) {}
func (c) SetRect(int, int, int, int) {}
func (c) GetRect() (int, int, int, int) { return 0, 0, 0, 0 }
func (c c) GetFocusable() tview.Focusable { return c }
func (c) Focus(func(tview.Primitive)) {}
func (c) Blur() {}
func (c) Start() {}
func (c) Stop() {}
func (c) Init(context.Context) error { return nil }
func (c) SetFilter(string) {}
func (c) SetLabelFilter(map[string]string) {}

View File

@ -54,6 +54,7 @@ type Table struct {
ctx context.Context
mx sync.RWMutex
readOnly bool
noIcon bool
}
// NewTable returns a new table view.
@ -72,6 +73,15 @@ func NewTable(gvr client.GVR) *Table {
}
}
// SetNoIcon toggles no icon mode.
func (t *Table) SetNoIcon(b bool) {
t.mx.Lock()
defer t.mx.Unlock()
t.noIcon = b
}
// SetReadOnly toggles read-only mode.
func (t *Table) SetReadOnly(ro bool) {
t.mx.Lock()
defer t.mx.Unlock()
@ -114,7 +124,8 @@ func (t *Table) getMSort() bool {
return t.manualSort
}
func (t *Table) setViewSetting(vs *config.ViewSetting) bool {
// SetViewSetting sets custom view config is present.
func (t *Table) SetViewSetting(vs *config.ViewSetting) bool {
t.mx.Lock()
defer t.mx.Unlock()
@ -127,7 +138,8 @@ func (t *Table) setViewSetting(vs *config.ViewSetting) bool {
return false
}
func (t *Table) getViewSetting() *config.ViewSetting {
// GetViewSetting return current view settings if any.
func (t *Table) GetViewSetting() *config.ViewSetting {
t.mx.RLock()
defer t.mx.RUnlock()
@ -161,7 +173,7 @@ func (t *Table) GVR() client.GVR { return t.gvr }
// ViewSettingsChanged notifies listener the view configuration changed.
func (t *Table) ViewSettingsChanged(vs *config.ViewSetting) {
if t.setViewSetting(vs) {
if t.SetViewSetting(vs) {
if vs == nil {
if !t.getMSort() && !t.sortCol.IsSet() {
t.setSortCol(model1.SortColumn{})
@ -294,7 +306,7 @@ func (t *Table) doUpdate(data *model1.TableData) *model1.TableData {
t.actions.Delete(KeyShiftP)
}
t.setSortCol(data.ComputeSortCol(t.getViewSetting(), t.getSortCol(), t.getMSort()))
t.setSortCol(data.ComputeSortCol(t.GetViewSetting(), t.getSortCol(), t.getMSort()))
return data
}
@ -537,9 +549,12 @@ func (t *Table) styleTitle() string {
var title string
if ns == client.ClusterScope {
title = SkinTitle(fmt.Sprintf(TitleFmt, ROIndicator(t.readOnly), t.gvr, render.AsThousands(rc)), t.styles.Frame())
title = SkinTitle(fmt.Sprintf(TitleFmt, t.gvr, render.AsThousands(rc)), t.styles.Frame())
} else {
title = SkinTitle(fmt.Sprintf(NSTitleFmt, ROIndicator(t.readOnly), t.gvr, ns, render.AsThousands(rc)), t.styles.Frame())
title = SkinTitle(fmt.Sprintf(NSTitleFmt, t.gvr, ns, render.AsThousands(rc)), t.styles.Frame())
}
if ic := ROIndicator(t.readOnly, t.noIcon); ic != "" {
title = " " + ic + title
}
buff := t.cmdBuff.GetText()
@ -557,10 +572,14 @@ func (t *Table) styleTitle() string {
}
// ROIndicator returns an icon showing whether the session is in readonly mode or not.
func ROIndicator(ro bool) string {
if ro {
return LockedIC
func ROIndicator(ro, noIC bool) string {
if noIC {
return ""
}
return UnlockedIC
if ro {
return lockedIC
}
return unlockedIC
}

View File

@ -23,10 +23,10 @@ const (
SearchFmt = "<[filter:bg:r]/%s[fg:bg:-]> "
// NSTitleFmt represents a namespaced view title.
NSTitleFmt = " %s [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] "
NSTitleFmt = " [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] "
// TitleFmt represents a standard view title.
TitleFmt = " %s [fg:bg:b]%s[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] "
TitleFmt = " [fg:bg:b]%s[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] "
descIndicator = "↓"
ascIndicator = "↑"

View File

@ -16,11 +16,8 @@ import (
)
const (
// UnlockedIC represents an unlocked icon.
UnlockedIC = "🔓"
// LockedIC represents a locked icon.
LockedIC = "🔒"
unlockedIC = "🔓"
lockedIC = "🔒"
)
// Namespaceable represents a namespaceable model.

View File

@ -22,6 +22,7 @@ import (
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -58,6 +59,12 @@ func (b *Browser) getUpdating() bool {
return b.updating
}
// SetCommand sets the current command.
func (b *Browser) SetCommand(cmd *cmd.Interpreter) {
b.GetTable().SetCommand(cmd)
//b.Table.SetViewSetting(b.app.CustomView().VSFor(cmd)
}
// Init watches all running pods in given namespace.
func (b *Browser) Init(ctx context.Context) error {
var err error
@ -85,6 +92,7 @@ func (b *Browser) Init(ctx context.Context) error {
b.app.CmdBuff().Reset()
}
b.Table.SetReadOnly(b.app.Config.IsReadOnly())
b.Table.SetNoIcon(b.app.Config.K9s.UI.NoIcons)
b.bindKeys(b.Actions())
for _, f := range b.bindKeysFn {

View File

@ -5,6 +5,8 @@ package cmd
import (
"strings"
"github.com/derailed/k9s/internal/client"
)
// Interpreter tracks user prompt input.
@ -218,7 +220,7 @@ func (c *Interpreter) FuzzyArg() (string, bool) {
func (c *Interpreter) NSArg() (string, bool) {
ns, ok := c.args[nsKey]
return ns, ok && ns != ""
return ns, ok && ns != client.BlankNamespace
}
// HasContext returns the current context if any.

View File

@ -329,7 +329,7 @@ func (c *Command) exec(p *cmd.Interpreter, gvr client.GVR, comp model.Component,
slog.Error("Failure detected during command exec", slogs.Error, e)
c.app.Content.Dump()
slog.Debug("Dumping history buffer", slogs.CmdHist, c.app.cmdHistory.List())
slog.Error("Dumping stack", slogs.Stack, debug.Stack())
slog.Error("Dumping stack", slogs.Stack, string(debug.Stack()))
p := cmd.NewInterpreter("pod")
cmds := c.app.cmdHistory.List()
@ -344,6 +344,8 @@ func (c *Command) exec(p *cmd.Interpreter, gvr client.GVR, comp model.Component,
if comp == nil {
return fmt.Errorf("no component found for %s", gvr)
}
comp.SetCommand(p)
c.app.Flash().Infof("Viewing %s...", gvr)
if clearStack {
cmd := contextRX.ReplaceAllString(p.GetLine(), "")
@ -352,10 +354,10 @@ func (c *Command) exec(p *cmd.Interpreter, gvr client.GVR, comp model.Component,
if err := c.app.inject(comp, clearStack); err != nil {
return err
}
if pushCmd {
c.app.cmdHistory.Push(p.GetLine())
}
slog.Debug("History", slogs.Stack, c.app.cmdHistory.List())
return
}

View File

@ -12,6 +12,7 @@ import (
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"github.com/sahilm/fuzzy"
@ -58,6 +59,7 @@ func NewDetails(app *App, title, subject, contentType string, searchable bool) *
return &d
}
func (d *Details) SetCommand(*cmd.Interpreter) {}
func (d *Details) SetFilter(string) {}
func (d *Details) SetLabelFilter(map[string]string) {}

View File

@ -14,6 +14,7 @@ import (
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
)
@ -43,8 +44,9 @@ func NewHelp(app *App) *Help {
}
}
func (h *Help) SetFilter(string) {}
func (h *Help) SetLabelFilter(map[string]string) {}
func (*Help) SetCommand(*cmd.Interpreter) {}
func (*Help) SetFilter(string) {}
func (*Help) SetLabelFilter(map[string]string) {}
// Init initializes the component.
func (h *Help) Init(ctx context.Context) error {

View File

@ -9,6 +9,7 @@ import (
"fmt"
"log/slog"
"os"
"regexp"
"strconv"
"strings"
@ -55,8 +56,10 @@ func clipboardWrite(text string) error {
return nil
}
var bracketRX = regexp.MustCompile(`\[(.+)\[\]`)
func sanitizeEsc(s string) string {
return strings.ReplaceAll(s, "[]", "]")
return bracketRX.ReplaceAllString(s, `[$1]`)
}
func cpCmd(flash *model.Flash, v *tview.TextView) func(*tcell.EventKey) *tcell.EventKey {

View File

@ -327,3 +327,26 @@ func Test_linesWithRegions(t *testing.T) {
})
}
}
func Test_sanitizeEsc(t *testing.T) {
uu := map[string]struct {
s string
e string
}{
"empty": {},
"empty-brackets": {
s: "[]",
e: "[]",
},
"tag": {
s: "[fred[]",
e: "[fred]",
},
}
for k, u := range uu {
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, sanitizeEsc(u.s))
})
}
}

View File

@ -15,6 +15,7 @@ import (
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"github.com/sahilm/fuzzy"
@ -61,6 +62,7 @@ func NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView {
return &v
}
func (v *LiveView) SetCommand(*cmd.Interpreter) {}
func (v *LiveView) SetFilter(string) {}
func (v *LiveView) SetLabelFilter(map[string]string) {}

View File

@ -22,6 +22,7 @@ import (
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
)
@ -63,8 +64,9 @@ func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log {
return &l
}
func (l *Log) SetFilter(string) {}
func (l *Log) SetLabelFilter(map[string]string) {}
func (*Log) SetCommand(*cmd.Interpreter) {}
func (*Log) SetFilter(string) {}
func (*Log) SetLabelFilter(map[string]string) {}
// Init initializes the viewer.
func (l *Log) Init(ctx context.Context) (err error) {

View File

@ -8,6 +8,7 @@ import (
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
)
@ -27,6 +28,7 @@ func NewPicker() *Picker {
}
}
func (p *Picker) SetCommand(*cmd.Interpreter) {}
func (p *Picker) SetFilter(string) {}
func (p *Picker) SetLabelFilter(map[string]string) {}

View File

@ -21,6 +21,7 @@ import (
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/tchart"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
)
// Graphable represents a graphic component.
@ -77,6 +78,7 @@ func NewPulse(gvr client.GVR) ResourceViewer {
}
}
func (p *Pulse) SetCommand(*cmd.Interpreter) {}
func (p *Pulse) SetFilter(string) {}
func (p *Pulse) SetLabelFilter(map[string]string) {}

View File

@ -90,9 +90,6 @@ func miscViewers(vv MetaViewers) {
vv[client.NewGVR("pulses")] = MetaViewer{
viewerFn: NewPulse,
}
vv[client.NewGVR("sanitizer")] = MetaViewer{
viewerFn: NewSanitizer,
}
}
func appsViewers(vv MetaViewers) {

View File

@ -1,436 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package view
import (
"context"
"fmt"
"log/slog"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/xray"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"golang.org/x/text/cases"
"golang.org/x/text/language"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var _ ResourceViewer = (*Sanitizer)(nil)
// Sanitizer represents a sanitizer tree view.
type Sanitizer struct {
*ui.Tree
app *App
gvr client.GVR
meta metav1.APIResource
model *model.Tree
cancelFn context.CancelFunc
envFn EnvFunc
contextFn ContextFunc
}
// NewSanitizer returns a new view.
func NewSanitizer(gvr client.GVR) ResourceViewer {
return &Sanitizer{
gvr: gvr,
Tree: ui.NewTree(),
model: model.NewTree(gvr),
}
}
func (s *Sanitizer) SetFilter(string) {}
func (s *Sanitizer) SetLabelFilter(map[string]string) {}
// Init initializes the view.
func (s *Sanitizer) Init(ctx context.Context) error {
s.envFn = s.k9sEnv
if err := s.Tree.Init(ctx); err != nil {
return err
}
s.SetKeyListenerFn(s.keyEntered)
var err error
s.meta, err = dao.MetaAccess.MetaFor(s.gvr)
if err != nil {
return err
}
if s.app, err = extractApp(ctx); err != nil {
return err
}
s.bindKeys()
s.SetBackgroundColor(s.app.Styles.Xray().BgColor.Color())
s.SetBorderColor(s.app.Styles.Frame().Border.FgColor.Color())
s.SetBorderFocusColor(s.app.Styles.Frame().Border.FocusColor.Color())
s.SetGraphicsColor(s.app.Styles.Xray().GraphicColor.Color())
s.SetTitle(cases.Title(language.Und, cases.NoLower).String(s.gvr.R()))
s.model.SetNamespace(client.CleanseNamespace(s.app.Config.ActiveNamespace()))
s.model.AddListener(s)
s.SetChangedFunc(func(n *tview.TreeNode) {
spec, ok := n.GetReference().(xray.NodeSpec)
if !ok {
slog.Error("No ref field found on node", slogs.FQN, n.GetText())
return
}
s.SetSelectedItem(spec.AsPath())
s.refreshActions()
})
s.refreshActions()
return nil
}
// InCmdMode checks if prompt is active.
func (*Sanitizer) InCmdMode() bool {
return false
}
// ExtraHints returns additional hints.
func (s *Sanitizer) ExtraHints() map[string]string {
if s.app.Config.K9s.UI.NoIcons {
return nil
}
return xray.EmojiInfo()
}
// SetInstance sets specific resource instance.
func (s *Sanitizer) SetInstance(string) {}
func (s *Sanitizer) bindKeys() {
s.Actions().Bulk(ui.KeyMap{
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", s.activateCmd, false),
tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", s.resetCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Goto", s.gotoCmd, true),
})
}
func (s *Sanitizer) keyEntered() {
s.ClearSelection()
s.update(s.filter(s.model.Peek()))
}
func (s *Sanitizer) refreshActions() {}
// GetSelectedPath returns the current selection as string.
func (s *Sanitizer) GetSelectedPath() string {
spec := s.selectedSpec()
if spec == nil {
return ""
}
return spec.Path()
}
func (s *Sanitizer) selectedSpec() *xray.NodeSpec {
node := s.GetCurrentNode()
if node == nil {
return nil
}
ref, ok := node.GetReference().(xray.NodeSpec)
if !ok {
slog.Error("Expecting a NodeSpec", slogs.RefType, fmt.Sprintf("%T", node.GetReference()))
return nil
}
return &ref
}
// EnvFn returns an plugin env function if available.
func (s *Sanitizer) EnvFn() EnvFunc {
return s.envFn
}
func (s *Sanitizer) k9sEnv() Env {
env := k8sEnv(s.app.Conn().Config())
spec := s.selectedSpec()
if spec == nil {
return env
}
env["FILTER"] = s.CmdBuff().GetText()
if env["FILTER"] == "" {
ns, n := client.Namespaced(spec.Path())
env["NAMESPACE"], env["FILTER"] = ns, n
}
switch spec.GVR() {
case "containers":
_, co := client.Namespaced(spec.Path())
env["CONTAINER"] = co
ns, n := client.Namespaced(*spec.ParentPath())
env["NAMESPACE"], env["POD"], env["NAME"] = ns, n, co
default:
ns, n := client.Namespaced(spec.Path())
env["NAMESPACE"], env["NAME"] = ns, n
}
return env
}
// Aliases returns all available aliases.
func (s *Sanitizer) Aliases() []string {
return append(s.meta.ShortNames, s.meta.SingularName, s.meta.Name)
}
func (s *Sanitizer) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
if s.app.InCmdMode() {
return evt
}
s.app.ResetPrompt(s.CmdBuff())
return nil
}
func (s *Sanitizer) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !s.CmdBuff().InCmdMode() {
s.CmdBuff().Reset()
return s.app.PrevCmd(evt)
}
s.CmdBuff().Reset()
s.model.ClearFilter()
s.Start()
return nil
}
func (s *Sanitizer) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if s.CmdBuff().IsActive() {
if internal.IsLabelSelector(s.CmdBuff().GetText()) {
s.Start()
}
s.CmdBuff().SetActive(false)
s.GetRoot().ExpandAll()
return nil
}
spec := s.selectedSpec()
if spec == nil {
return nil
}
if len(spec.GVRs) <= 2 {
return nil
}
path := strings.Replace(spec.Path(), "::", "/", 1)
if strings.Contains(path, "[") {
return nil
}
if len(strings.Split(path, "/")) == 1 && spec.GVR() != "node" {
path = "-/" + path
}
s.app.gotoResource(client.NewGVR(spec.GVR()).R(), path, false, true)
return nil
}
func (s *Sanitizer) filter(root *xray.TreeNode) *xray.TreeNode {
q := s.CmdBuff().GetText()
if s.CmdBuff().Empty() || internal.IsLabelSelector(q) {
return root
}
s.UpdateTitle()
if f, ok := internal.IsFuzzySelector(q); ok {
return root.Filter(f, fuzzyFilter)
}
if internal.IsInverseSelector(q) {
return root.Filter(q, rxInverseFilter)
}
return root.Filter(q, rxFilter)
}
// TreeNodeSelected callback for node selection.
func (s *Sanitizer) TreeNodeSelected() {
s.app.QueueUpdateDraw(func() {
n := s.GetCurrentNode()
if n != nil {
n.SetColor(s.app.Styles.Xray().CursorColor.Color())
}
})
}
// TreeLoadFailed notifies the load failed.
func (s *Sanitizer) TreeLoadFailed(err error) {
s.app.Flash().Err(err)
}
func (s *Sanitizer) update(node *xray.TreeNode) {
root := makeTreeNode(node, s.ExpandNodes(), s.app.Config.K9s.UI.NoIcons, s.app.Styles)
if node == nil {
s.app.QueueUpdateDraw(func() {
s.SetRoot(root)
})
return
}
for _, c := range node.Children {
s.hydrate(root, c)
}
if s.GetSelectedItem() == "" {
s.SetSelectedItem(node.Spec().Path())
}
s.app.QueueUpdateDraw(func() {
s.SetRoot(root)
root.Walk(func(node, parent *tview.TreeNode) bool {
spec, ok := node.GetReference().(xray.NodeSpec)
if !ok {
slog.Error("Expecting a NodeSpec", slogs.RefType, fmt.Sprintf("%T", node.GetReference()))
return false
}
// BOZO!! Figure this out expand/collapse but the root
if parent != nil {
node.SetExpanded(s.ExpandNodes())
} else {
node.SetExpanded(true)
}
if spec.AsPath() == s.GetSelectedItem() {
node.SetExpanded(true).SetSelectable(true)
s.SetCurrentNode(node)
}
return true
})
})
}
// TreeChanged notifies the model data changed.
func (s *Sanitizer) TreeChanged(node *xray.TreeNode) {
s.Count = node.Count(s.gvr.String())
s.update(s.filter(node))
s.UpdateTitle()
}
func (s *Sanitizer) hydrate(parent *tview.TreeNode, n *xray.TreeNode) {
node := makeTreeNode(n, s.ExpandNodes(), s.app.Config.K9s.UI.NoIcons, s.app.Styles)
for _, c := range n.Children {
s.hydrate(node, c)
}
parent.AddChild(node)
}
// SetEnvFn sets the custom environment function.
func (s *Sanitizer) SetEnvFn(EnvFunc) {}
// Refresh updates the view.
func (s *Sanitizer) Refresh() {}
// BufferChanged indicates the buffer was changed.
func (s *Sanitizer) BufferChanged(_, _ string) {}
// BufferCompleted indicates input was accepted.
func (s *Sanitizer) BufferCompleted(_, _ string) {
s.update(s.filter(s.model.Peek()))
}
// BufferActive indicates the buff activity changed.
func (s *Sanitizer) BufferActive(state bool, k model.BufferKind) {
s.app.BufferActive(state, k)
}
func (s *Sanitizer) defaultContext() context.Context {
ctx := context.WithValue(context.Background(), internal.KeyFactory, s.app.factory)
ctx = context.WithValue(ctx, internal.KeyFields, "")
if s.CmdBuff().Empty() {
ctx = context.WithValue(ctx, internal.KeyLabels, "")
} else {
ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(s.CmdBuff().GetText()))
}
return ctx
}
// Start initializes resource watch loop.
func (s *Sanitizer) Start() {
s.Stop()
s.CmdBuff().AddListener(s)
ctx := s.defaultContext()
ctx, s.cancelFn = context.WithCancel(ctx)
if s.contextFn != nil {
ctx = s.contextFn(ctx)
}
s.model.Refresh(ctx)
s.UpdateTitle()
}
// Stop terminates watch loop.
func (s *Sanitizer) Stop() {
if s.cancelFn == nil {
return
}
s.cancelFn()
s.cancelFn = nil
s.CmdBuff().RemoveListener(s)
}
// AddBindKeysFn sets up extra key bindings.
func (s *Sanitizer) AddBindKeysFn(BindKeysFunc) {}
// SetContextFn sets custom context.
func (s *Sanitizer) SetContextFn(f ContextFunc) {
s.contextFn = f
}
// Name returns the component name.
func (s *Sanitizer) Name() string { return "report" }
// GetTable returns the underlying table.
func (s *Sanitizer) GetTable() *Table { return nil }
// GVR returns a resource descriptor.
func (s *Sanitizer) GVR() client.GVR { return s.gvr }
// App returns the current app handle.
func (s *Sanitizer) App() *App {
return s.app
}
// UpdateTitle updates the view title.
func (s *Sanitizer) UpdateTitle() {
t := s.styleTitle()
s.app.QueueUpdateDraw(func() {
s.SetTitle(t)
})
}
func (s *Sanitizer) styleTitle() string {
base := cases.Title(language.Und, cases.NoLower).String(s.gvr.R())
ns := s.model.GetNamespace()
if client.IsAllNamespaces(ns) {
ns = client.NamespaceAll
}
var title string
if ns == client.ClusterScope {
title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, ui.ROIndicator(s.app.Config.IsReadOnly()), base, render.AsThousands(int64(s.Count))), s.app.Styles.Frame())
} else {
title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, ui.ROIndicator(s.app.Config.IsReadOnly()), base, ns, render.AsThousands(int64(s.Count))), s.app.Styles.Frame())
}
buff := s.CmdBuff().GetText()
if buff == "" {
return title
}
if internal.IsLabelSelector(buff) {
buff = ui.TrimLabelSelector(buff)
}
return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), s.app.Styles.Frame())
}

View File

@ -5,12 +5,12 @@ package view
import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tcell/v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/yaml"
)
// Secret presents a secret viewer.
@ -57,7 +57,7 @@ func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
raw, err := yaml.Marshal(d)
raw, err := data.WriteYAML(d)
if err != nil {
s.App().Flash().Errf("Error decoding secret %s", err)
return nil

View File

@ -12,11 +12,11 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/tcell/v2"
)
@ -28,6 +28,7 @@ type Table struct {
enterFn EnterFunc
envFn EnvFunc
bindKeysFn []BindKeysFunc
command *cmd.Interpreter
}
// NewTable returns a new viewer.
@ -48,11 +49,8 @@ func (t *Table) Init(ctx context.Context) (err error) {
if t.app.Conn() != nil {
ctx = context.WithValue(ctx, internal.KeyHasMetrics, t.app.Conn().HasMetrics())
}
if t.app.CustomView == nil {
t.app.CustomView = config.NewCustomView()
}
ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles)
ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView)
ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView())
t.Table.Init(ctx)
if !t.app.Config.K9s.UI.Reactive {
if err := t.app.RefreshCustomViews(); err != nil {
@ -68,6 +66,11 @@ func (t *Table) Init(ctx context.Context) (err error) {
return nil
}
// SetCommand sets the current command.
func (t *Table) SetCommand(cmd *cmd.Interpreter) {
t.command = cmd
}
// HeaderIndex returns index of a given column or false if not found.
func (t *Table) HeaderIndex(colName string) (int, bool) {
for i := 0; i < t.GetColumnCount(); i++ {
@ -145,14 +148,18 @@ func (t *Table) Start() {
t.Stop()
t.CmdBuff().AddListener(t)
t.Styles().AddListener(t.Table)
t.App().CustomView.AddListener(t.Table.GVR().String(), t.Table)
cmds := []string{t.Table.GVR().String()}
if t.command != nil {
cmds = append(cmds, t.command.GetLine())
}
t.App().CustomView().AddListeners(t.Table, cmds...)
}
// Stop terminates the component.
func (t *Table) Stop() {
t.CmdBuff().RemoveListener(t)
t.Styles().RemoveListener(t.Table)
t.App().CustomView.RemoveListener(t.GVR().String())
t.App().CustomView().RemoveListener(t.Table)
}
// SetEnterFn specifies the default enter behavior.

View File

@ -10,6 +10,7 @@ import (
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
)
const (
@ -95,6 +96,9 @@ type ResourceViewer interface {
// SetInstance sets a parent FQN
SetInstance(string)
// SetCommand sets the current command.
SetCommand(*cmd.Interpreter)
}
// LogViewer represents a log viewer.

View File

@ -20,6 +20,7 @@ import (
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/derailed/k9s/internal/xray"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
@ -54,6 +55,7 @@ func NewXray(gvr client.GVR) ResourceViewer {
}
}
func (x *Xray) SetCommand(*cmd.Interpreter) {}
func (x *Xray) SetFilter(string) {}
func (x *Xray) SetLabelFilter(map[string]string) {}
@ -671,9 +673,12 @@ func (x *Xray) styleTitle() string {
var title string
if ns == client.ClusterScope {
title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, ui.ROIndicator(x.app.Config.IsReadOnly()), base, render.AsThousands(int64(x.Count))), x.app.Styles.Frame())
title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(x.Count))), x.app.Styles.Frame())
} else {
title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, ui.ROIndicator(x.app.Config.IsReadOnly()), base, ns, render.AsThousands(int64(x.Count))), x.app.Styles.Frame())
title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(x.Count))), x.app.Styles.Frame())
}
if ic := ui.ROIndicator(x.app.Config.IsReadOnly(), x.app.Config.K9s.UI.NoIcons); ic != "" {
title = " " + ic + title
}
buff := x.CmdBuff().GetText()

View File

@ -9,7 +9,6 @@ import (
"strings"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/popeye/pkg/config"
)
// Section represents an xray renderer.
@ -59,15 +58,15 @@ func (*Section) outcomeRefs(parent *TreeNode, section render.Section) {
// ----------------------------------------------------------------------------
// Helpers...
func colorize(s string, l config.Level) string {
func colorize(s string, l render.Level) string {
c := "green"
// nolint:exhaustive
switch l {
case config.ErrorLevel:
case render.ErrorLevel:
c = "red"
case config.WarnLevel:
case render.WarnLevel:
c = "orange"
case config.InfoLevel:
case render.InfoLevel:
c = "blue"
}
return fmt.Sprintf("[%s::]%s", c, s)