From 45c2137df89e5b3d3bc6f9e6b04503dded5f2176 Mon Sep 17 00:00:00 2001 From: Fernand Galiana Date: Tue, 11 Mar 2025 18:15:20 -0600 Subject: [PATCH] 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 --- cmd/info.go | 2 +- cmd/root.go | 13 +- go.mod | 3 - go.sum | 11 - internal/client/config.go | 8 +- internal/config/alias.go | 10 +- internal/config/benchmark.go | 2 +- internal/config/config.go | 7 +- internal/config/data/config.go | 2 +- internal/config/data/dir.go | 8 +- internal/config/data/dir_test.go | 2 +- internal/config/data/helpers.go | 28 ++ internal/config/data/ns.go | 6 +- internal/config/hotkey.go | 2 +- internal/config/k9s.go | 15 +- internal/config/k9s_int_test.go | 12 +- internal/config/plugin.go | 11 +- internal/config/styles.go | 2 +- internal/config/testdata/views/views.yaml | 6 + internal/config/types.go | 4 + internal/config/views.go | 65 +++- internal/config/views_int_test.go | 11 + internal/dao/helm_chart.go | 4 +- internal/dao/helm_history.go | 10 +- internal/dao/popeye.go | 142 ------- internal/dao/registry.go | 15 - internal/model/stack_test.go | 18 +- internal/model/types.go | 8 + internal/model1/types.go | 1 + internal/render/base.go | 1 + internal/render/popeye.go | 195 ---------- internal/render/section.go | 44 +++ internal/ui/config.go | 18 +- internal/ui/crumbs_test.go | 38 +- internal/ui/table.go | 39 +- internal/ui/table_helper.go | 4 +- internal/ui/types.go | 7 +- internal/view/browser.go | 8 + internal/view/cmd/interpreter.go | 4 +- internal/view/command.go | 6 +- internal/view/details.go | 2 + internal/view/help.go | 6 +- internal/view/helpers.go | 5 +- internal/view/helpers_test.go | 23 ++ internal/view/live_view.go | 2 + internal/view/log.go | 6 +- internal/view/picker.go | 2 + internal/view/pulse.go | 2 + internal/view/registrar.go | 3 - internal/view/sanitizer.go | 436 ---------------------- internal/view/secret.go | 4 +- internal/view/table.go | 21 +- internal/view/types.go | 4 + internal/view/xray.go | 9 +- internal/xray/section.go | 9 +- 55 files changed, 370 insertions(+), 956 deletions(-) delete mode 100644 internal/dao/popeye.go delete mode 100644 internal/render/popeye.go create mode 100644 internal/render/section.go delete mode 100644 internal/view/sanitizer.go diff --git a/cmd/info.go b/cmd/info.go index 8110bbb1..e6aba152 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -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 { diff --git a/cmd/root.go b/cmd/root.go index 9cc30161..baed2df0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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) diff --git a/go.mod b/go.mod index 58797797..8de2d5ef 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 27ad0126..16aeb094 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/client/config.go b/internal/client/config.go index e34d0e3c..6814b5a7 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -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() diff --git a/internal/config/alias.go b/internal/config/alias.go index 8172ba97..6503c493 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -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) } diff --git a/internal/config/benchmark.go b/internal/config/benchmark.go index 329c0940..0e5fca8b 100644 --- a/internal/config/benchmark.go +++ b/internal/config/benchmark.go @@ -7,7 +7,7 @@ import ( "net/http" "os" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) // K9sBench the name of the benchmarks config file. diff --git a/internal/config/config.go b/internal/config/config.go index e4ced887..64fe162f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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. diff --git a/internal/config/data/config.go b/internal/config/data/config.go index bc894406..c2142ea8 100644 --- a/internal/config/data/config.go +++ b/internal/config/data/config.go @@ -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" ) diff --git a/internal/config/data/dir.go b/internal/config/data/dir.go index 6165e5ad..e22bfa92 100644 --- a/internal/config/data/dir.go +++ b/internal/config/data/dir.go @@ -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) { diff --git a/internal/config/data/dir_test.go b/internal/config/data/dir_test.go index e4e5118c..4d29943b 100644 --- a/internal/config/data/dir_test.go +++ b/internal/config/data/dir_test.go @@ -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" ) diff --git a/internal/config/data/helpers.go b/internal/config/data/helpers.go index af086e5b..beae284a 100644 --- a/internal/config/data/helpers.go +++ b/internal/config/data/helpers.go @@ -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) +} diff --git a/internal/config/data/ns.go b/internal/config/data/ns.go index cf9aba0f..f37488f1 100644 --- a/internal/config/data/ns.go +++ b/internal/config/data/ns.go @@ -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(), diff --git a/internal/config/hotkey.go b/internal/config/hotkey.go index b1695085..b83bc186 100644 --- a/internal/config/hotkey.go +++ b/internal/config/hotkey.go @@ -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. diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 8579b682..a376acd0 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -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 } diff --git a/internal/config/k9s_int_test.go b/internal/config/k9s_int_test.go index f950502e..c9144302 100644 --- a/internal/config/k9s_int_test.go +++ b/internal/config/k9s_int_test.go @@ -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, }, diff --git a/internal/config/plugin.go b/internal/config/plugin.go index 2f6773e2..11b7e02d 100644 --- a/internal/config/plugin.go +++ b/internal/config/plugin.go @@ -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 { diff --git a/internal/config/styles.go b/internal/config/styles.go index 3e3b4e82..bdbce4d3 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -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. diff --git a/internal/config/testdata/views/views.yaml b/internal/config/testdata/views/views.yaml index 5ce6a64d..f2f391f7 100644 --- a/internal/config/testdata/views/views.yaml +++ b/internal/config/testdata/views/views.yaml @@ -17,3 +17,9 @@ views: - AGE - NAME - IP + + bozo: + columns: + - DUH + - BLAH + - BLEE diff --git a/internal/config/types.go b/internal/config/types.go index 6938e555..29080f11 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -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 } diff --git a/internal/config/views.go b/internal/config/views.go index 03c87298..de3ad028 100644 --- a/internal/config/views.go +++ b/internal/config/views.go @@ -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 { diff --git a/internal/config/views_int_test.go b/internal/config/views_int_test.go index e113fe71..e9eb0e11 100644 --- a/internal/config/views_int_test.go +++ b/internal/config/views_int_test.go @@ -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", diff --git a/internal/dao/helm_chart.go b/internal/dao/helm_chart.go index 3a20d918..43c712b9 100644 --- a/internal/dao/helm_chart.go +++ b/internal/dao/helm_chart.go @@ -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. diff --git a/internal/dao/helm_history.go b/internal/dao/helm_history.go index 3dd30dea..1d31c6da 100644 --- a/internal/dao/helm_history.go +++ b/internal/dao/helm_history.go @@ -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 { diff --git a/internal/dao/popeye.go b/internal/dao/popeye.go deleted file mode 100644 index f23476d0..00000000 --- a/internal/dao/popeye.go +++ /dev/null @@ -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 = §ions -// 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() -// } diff --git a/internal/dao/registry.go b/internal/dao/registry.go index ef04efb3..a906b6fe 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -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", diff --git a/internal/model/stack_test.go b/internal/model/stack_test.go index 6d8c756e..4f4f73e3 100644 --- a/internal/model/stack_test.go +++ b/internal/model/stack_test.go @@ -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) {} diff --git a/internal/model/types.go b/internal/model/types.go index e729b322..d175a0c2 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -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 { diff --git a/internal/model1/types.go b/internal/model1/types.go index bd40b49f..8235ca3c 100644 --- a/internal/model1/types.go +++ b/internal/model1/types.go @@ -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) } diff --git a/internal/render/base.go b/internal/render/base.go index 091db7ba..c86c17db 100644 --- a/internal/render/base.go +++ b/internal/render/base.go @@ -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 diff --git a/internal/render/popeye.go b/internal/render/popeye.go deleted file mode 100644 index e43df218..00000000 --- a/internal/render/popeye.go +++ /dev/null @@ -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 -// } diff --git a/internal/render/section.go b/internal/render/section.go new file mode 100644 index 00000000..ea295832 --- /dev/null +++ b/internal/render/section.go @@ -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"` + } +) diff --git a/internal/ui/config.go b/internal/ui/config.go index 98019b19..fc664bc0 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -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. diff --git a/internal/ui/crumbs_test.go b/internal/ui/crumbs_test.go index dc43b5ba..57daa103 100644 --- a/internal/ui/crumbs_test.go +++ b/internal/ui/crumbs_test.go @@ -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) {} diff --git a/internal/ui/table.go b/internal/ui/table.go index ad2c488e..c3981d37 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -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 } diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index 535afa0b..8db289b8 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -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 = "↑" diff --git a/internal/ui/types.go b/internal/ui/types.go index 5f8c2b97..3fb98de7 100644 --- a/internal/ui/types.go +++ b/internal/ui/types.go @@ -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. diff --git a/internal/view/browser.go b/internal/view/browser.go index 3cf8533a..0a02bed9 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -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 { diff --git a/internal/view/cmd/interpreter.go b/internal/view/cmd/interpreter.go index 82cbbe97..fade256e 100644 --- a/internal/view/cmd/interpreter.go +++ b/internal/view/cmd/interpreter.go @@ -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. diff --git a/internal/view/command.go b/internal/view/command.go index e3695da7..be074f3e 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -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 } diff --git a/internal/view/details.go b/internal/view/details.go index 235b5d2b..0a95dafc 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -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) {} diff --git a/internal/view/help.go b/internal/view/help.go index c434b96b..dd3609f1 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -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 { diff --git a/internal/view/helpers.go b/internal/view/helpers.go index e533c9ee..11df3c02 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -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 { diff --git a/internal/view/helpers_test.go b/internal/view/helpers_test.go index f7eefcdb..87500f92 100644 --- a/internal/view/helpers_test.go +++ b/internal/view/helpers_test.go @@ -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)) + }) + } +} diff --git a/internal/view/live_view.go b/internal/view/live_view.go index 94b29578..0aa63ecb 100644 --- a/internal/view/live_view.go +++ b/internal/view/live_view.go @@ -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) {} diff --git a/internal/view/log.go b/internal/view/log.go index e56d94ef..884ba0ee 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -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) { diff --git a/internal/view/picker.go b/internal/view/picker.go index ce39b6b9..aa2e5b46 100644 --- a/internal/view/picker.go +++ b/internal/view/picker.go @@ -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) {} diff --git a/internal/view/pulse.go b/internal/view/pulse.go index ac757148..2b38ef2f 100644 --- a/internal/view/pulse.go +++ b/internal/view/pulse.go @@ -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) {} diff --git a/internal/view/registrar.go b/internal/view/registrar.go index daee0944..e40e627a 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -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) { diff --git a/internal/view/sanitizer.go b/internal/view/sanitizer.go deleted file mode 100644 index 2c7ef263..00000000 --- a/internal/view/sanitizer.go +++ /dev/null @@ -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()) -} diff --git a/internal/view/secret.go b/internal/view/secret.go index 1db27526..141c68f3 100644 --- a/internal/view/secret.go +++ b/internal/view/secret.go @@ -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 diff --git a/internal/view/table.go b/internal/view/table.go index 9900df8d..f3e42679 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -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. diff --git a/internal/view/types.go b/internal/view/types.go index 76a4c9a6..46485ba0 100644 --- a/internal/view/types.go +++ b/internal/view/types.go @@ -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. diff --git a/internal/view/xray.go b/internal/view/xray.go index aa96b8af..2027099b 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -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() diff --git a/internal/xray/section.go b/internal/xray/section.go index 7e171004..f9ce0fa0 100644 --- a/internal/xray/section.go +++ b/internal/xray/section.go @@ -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)