diff --git a/Makefile b/Makefile
index 2382ab33..36c29177 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
else
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
endif
-VERSION ?= v0.31.9
+VERSION ?= v0.32.0
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}
diff --git a/change_logs/release_v0.32.0.md b/change_logs/release_v0.32.0.md
new file mode 100644
index 00000000..e35c3b61
--- /dev/null
+++ b/change_logs/release_v0.32.0.md
@@ -0,0 +1,73 @@
+
+
+# Release v0.32.0
+
+## Notes
+
+Thank you to all that contributed with flushing out issues and enhancements for K9s!
+I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev
+and see if we're happier with some of the fixes!
+If you've filed an issue please help me verify and close.
+
+Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!
+Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!
+
+As you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,
+please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
+
+On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
+
+## Maintenance Release!
+
+A lot of refactors, perf improvements (crossing fingers+toes!) and general spring cleaning items in this release.
+Thus I expect a bit of `disturbance in the farce` given the major code churns, so please beware!
+
+---
+
+## Videos Are In The Can!
+
+Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...
+
+* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)
+* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)
+* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)
+
+---
+
+## A Word From Our Sponsors...
+
+To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!
+
+* [Justin Reid](https://github.com/jmreid)
+* [Danni](https://github.com/danninov)
+* [Robert Krahn](https://github.com/rksm)
+* [Hao Ke](https://github.com/kehao95)
+* [PH](https://github.com/raphael-com-ph)
+
+> Sponsorship cancellations since the last release: **9!!** 🥹
+
+---
+
+## Resolved Issues
+
+* [#2569](https://github.com/derailed/k9s/issues/2569) k9s panics on start if the main config file (config.yml) is owned by root
+* [#2568](https://github.com/derailed/k9s/issues/2568) kube context in running k9s is no longer sticky, during kubectx context switch
+* [#2560](https://github.com/derailed/k9s/issues/2560) Namespace/Settings keeps resetting
+* [#2557](https://github.com/derailed/k9s/issues/2557) [Feature]: Sort CRDs by their group
+* [#1462](https://github.com/derailed/k9s/issues/1462) k9s running very slowly when opening namespace with 13k pods (maybe??)
+
+---
+
+## Contributed PRs
+
+Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!
+
+* [#2564](https://github.com/derailed/k9s/pull/2564) Add everforest skins
+* [#2558](https://github.com/derailed/k9s/pull/2558) feat: sort by role in node list view
+* [#2554](https://github.com/derailed/k9s/pull/2554) Added context to the debug command for debug-container plugin
+* [#2554](https://github.com/derailed/k9s/pull/2554) Correctly respect the KUBECACHEDIR env var
+* [#2546](https://github.com/derailed/k9s/pull/2546) Use configured log fgColor to print log markers
+
+---
+
+
© 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
\ No newline at end of file
diff --git a/cmd/root.go b/cmd/root.go
index 2ffc3ea1..5181c8bd 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -134,7 +134,7 @@ func loadConfiguration() (*config.Config, error) {
errs = errors.Join(errs, err)
}
- if err := k9sCfg.Load(config.AppConfigFile); err != nil {
+ if err := k9sCfg.Load(config.AppConfigFile, false); err != nil {
errs = errors.Join(errs, err)
}
k9sCfg.K9s.Override(k9sFlags)
@@ -151,7 +151,7 @@ func loadConfiguration() (*config.Config, error) {
}
log.Info().Msg("✅ Kubernetes connectivity")
- if err := k9sCfg.Save(); err != nil {
+ if err := k9sCfg.Save(false); err != nil {
log.Error().Err(err).Msg("Config save")
errs = errors.Join(errs, err)
}
diff --git a/internal/client/client.go b/internal/client/client.go
index f76467a2..e12f9005 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -212,64 +212,26 @@ func (a *APIClient) ServerVersion() (*version.Info, error) {
return info, nil
}
-func (a *APIClient) IsValidNamespace(ns string) bool {
- if IsClusterWide(ns) || ns == NotNamespaced {
- return true
+func (a *APIClient) IsValidNamespace(n string) bool {
+ ok, err := a.isValidNamespace(n)
+ if err != nil {
+ log.Warn().Err(err).Msgf("namespace validation failed for: %q", n)
}
- ok, err := a.CanI(ClusterScope, "v1/namespaces", "", []string{ListVerb})
- if ok && err == nil {
- nn, _ := a.ValidNamespaceNames()
- _, ok = nn[ns]
- return ok
- }
-
- ok, err = a.isValidNamespace(ns)
- if ok && err == nil {
- return ok
- }
- log.Warn().Err(err).Msgf("namespace validation failed for: %q", ns)
-
- return false
-}
-
-func (a *APIClient) cachedNamespaceNames() NamespaceNames {
- cns, ok := a.cache.Get(cacheNSKey)
- if !ok {
- return make(NamespaceNames)
- }
-
- return cns.(NamespaceNames)
+ return ok
}
func (a *APIClient) isValidNamespace(n string) (bool, error) {
if IsClusterWide(n) || n == NotNamespaced {
return true, nil
}
-
- if a == nil {
- return false, errors.New("invalid client")
- }
-
- cnss := a.cachedNamespaceNames()
- if _, ok := cnss[n]; ok {
- return true, nil
- }
-
- dial, err := a.Dial()
+ nn, err := a.ValidNamespaceNames()
if err != nil {
return false, err
}
- ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout())
- defer cancel()
- _, err = dial.CoreV1().Namespaces().Get(ctx, n, metav1.GetOptions{})
- if err != nil {
- return false, err
- }
- cnss[n] = struct{}{}
- a.cache.Add(cacheNSKey, cnss, cacheExpiry)
+ _, ok := nn[n]
- return true, nil
+ return ok, nil
}
// ValidNamespaceNames returns all available namespaces.
@@ -283,6 +245,12 @@ func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {
return nss, nil
}
}
+
+ ok, err := a.CanI(ClusterScope, "v1/namespaces", "", []string{ListVerb})
+ if !ok || err != nil {
+ return nil, fmt.Errorf("user not authorized to list all namespaces")
+ }
+
dial, err := a.Dial()
if err != nil {
return nil, err
diff --git a/internal/client/config.go b/internal/client/config.go
index 0dafc59e..65c3d6d7 100644
--- a/internal/client/config.go
+++ b/internal/client/config.go
@@ -20,7 +20,7 @@ const (
defaultCallTimeoutDuration time.Duration = 15 * time.Second
// UsePersistentConfig caches client config to avoid reloads.
- UsePersistentConfig = false
+ UsePersistentConfig = true
)
// Config tracks a kubernetes configuration.
diff --git a/internal/config/alias.go b/internal/config/alias.go
index 35cd8e78..be53f402 100644
--- a/internal/config/alias.go
+++ b/internal/config/alias.go
@@ -4,7 +4,9 @@
package config
import (
+ "errors"
"fmt"
+ "io/fs"
"os"
"sync"
@@ -136,7 +138,7 @@ func (a *Aliases) LoadFile(path string) error {
if path == "" {
return nil
}
- if _, err := os.Stat(path); os.IsNotExist(err) {
+ if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return nil
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 0390bf5e..5f7c2471 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -6,6 +6,7 @@ package config
import (
"errors"
"fmt"
+ "io/fs"
"os"
"github.com/derailed/k9s/internal/client"
@@ -52,14 +53,13 @@ func (c *Config) ContextAliasesPath() string {
}
// ContextPluginsPath returns a context specific plugins file spec.
-func (c *Config) ContextPluginsPath() string {
+func (c *Config) ContextPluginsPath() (string, error) {
ct, err := c.K9s.ActiveContext()
if err != nil {
- log.Error().Err(err).Msgf("active context load failed")
- return ""
+ return "", err
}
- return AppContextPluginsFile(ct.GetClusterName(), c.K9s.activeContextName)
+ return AppContextPluginsFile(ct.GetClusterName(), c.K9s.activeContextName), nil
}
// Refine the configuration based on cli args.
@@ -209,9 +209,9 @@ func (c *Config) Merge(c1 *Config) {
}
// Load loads K9s configuration from file.
-func (c *Config) Load(path string) error {
- if _, err := os.Stat(path); os.IsNotExist(err) {
- if err := c.Save(); err != nil {
+func (c *Config) Load(path string, force bool) error {
+ if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
+ if err := c.Save(force); err != nil {
return err
}
}
@@ -234,12 +234,12 @@ func (c *Config) Load(path string) error {
}
// Save configuration to disk.
-func (c *Config) Save() error {
+func (c *Config) Save(force bool) error {
c.Validate()
- if err := c.K9s.Save(); err != nil {
+ if err := c.K9s.Save(force); err != nil {
return err
}
- if _, err := os.Stat(AppConfigFile); os.IsNotExist(err) {
+ if _, err := os.Stat(AppConfigFile); errors.Is(err, fs.ErrNotExist) {
return c.SaveFile(AppConfigFile)
}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 10c66cb6..3910eb5c 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -4,6 +4,7 @@
package config_test
import (
+ "errors"
"fmt"
"os"
"path/filepath"
@@ -58,7 +59,7 @@ func TestConfigSave(t *testing.T) {
c.K9s.Override(u.k9sFlags)
assert.NoError(t, c.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags)))
}
- assert.NoError(t, c.Save())
+ assert.NoError(t, c.Save(true))
bb, err := os.ReadFile(config.AppConfigFile)
assert.NoError(t, err)
ee, err := os.ReadFile("testdata/configs/default.yaml")
@@ -265,16 +266,19 @@ func TestContextAliasesPath(t *testing.T) {
func TestContextPluginsPath(t *testing.T) {
uu := map[string]struct {
- ct string
- e string
+ ct, e string
+ err error
}{
- "empty": {},
+ "empty": {
+ err: errors.New(`no context found for: ""`),
+ },
"happy": {
ct: "ct-1-1",
e: "/tmp/test/cl-1/ct-1-1/plugins.yaml",
},
"not-exists": {
- ct: "fred",
+ ct: "fred",
+ err: errors.New(`no context found for: "fred"`),
},
}
@@ -283,7 +287,11 @@ func TestContextPluginsPath(t *testing.T) {
t.Run(k, func(t *testing.T) {
c := mock.NewMockConfig()
_, _ = c.K9s.ActivateContext(u.ct)
- assert.Equal(t, u.e, c.ContextPluginsPath())
+ s, err := c.ContextPluginsPath()
+ if err != nil {
+ assert.Equal(t, u.err, err)
+ }
+ assert.Equal(t, u.e, s)
})
}
}
@@ -309,7 +317,7 @@ Invalid type. Expected: boolean, given: string`,
u := uu[k]
t.Run(k, func(t *testing.T) {
cfg := config.NewConfig(nil)
- if err := cfg.Load(u.f); err != nil {
+ if err := cfg.Load(u.f, true); err != nil {
assert.Equal(t, u.err, err.Error())
}
})
@@ -520,14 +528,14 @@ func TestConfigValidate(t *testing.T) {
cfg := mock.NewMockConfig()
cfg.SetConnection(mock.NewMockConnection())
- assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
+ assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true))
cfg.Validate()
}
func TestConfigLoad(t *testing.T) {
cfg := mock.NewMockConfig()
- assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
+ assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true))
assert.Equal(t, 2, cfg.K9s.RefreshRate)
assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount)
assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize)
@@ -536,13 +544,13 @@ func TestConfigLoad(t *testing.T) {
func TestConfigLoadCrap(t *testing.T) {
cfg := mock.NewMockConfig()
- assert.NotNil(t, cfg.Load("testdata/configs/k9s_not_there.yaml"))
+ assert.NotNil(t, cfg.Load("testdata/configs/k9s_not_there.yaml", true))
}
func TestConfigSaveFile(t *testing.T) {
cfg := mock.NewMockConfig()
- assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
+ assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true))
cfg.K9s.RefreshRate = 100
cfg.K9s.ReadOnly = true
@@ -561,7 +569,7 @@ func TestConfigSaveFile(t *testing.T) {
func TestConfigReset(t *testing.T) {
cfg := mock.NewMockConfig()
- assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
+ assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true))
cfg.Reset()
cfg.Validate()
diff --git a/internal/config/data/config.go b/internal/config/data/config.go
index 130bf15b..a03020f8 100644
--- a/internal/config/data/config.go
+++ b/internal/config/data/config.go
@@ -26,6 +26,15 @@ func NewConfig(ct *api.Context) *Config {
}
}
+func (c *Config) Merge(c1 *Config) {
+ if c1 == nil {
+ return
+ }
+ if c.Context != nil && c1.Context != nil {
+ c.Context.merge(c1.Context)
+ }
+}
+
// Validate ensures config is in norms.
func (c *Config) Validate(conn client.Connection, ks KubeSettings) {
c.mx.Lock()
diff --git a/internal/config/data/context.go b/internal/config/data/context.go
index 32a77791..591f072f 100644
--- a/internal/config/data/context.go
+++ b/internal/config/data/context.go
@@ -56,6 +56,13 @@ func NewContextFromKubeConfig(ks KubeSettings) (*Context, error) {
return NewContextFromConfig(ct), nil
}
+func (c *Context) merge(old *Context) {
+ if old == nil {
+ return
+ }
+ c.Namespace.merge(old.Namespace)
+
+}
func (c *Context) GetClusterName() string {
c.mx.RLock()
defer c.mx.RUnlock()
diff --git a/internal/config/data/context_int_test.go b/internal/config/data/context_int_test.go
new file mode 100644
index 00000000..1a37ff99
--- /dev/null
+++ b/internal/config/data/context_int_test.go
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package data
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_contextMerge(t *testing.T) {
+ uu := map[string]struct {
+ c1, c2, e *Context
+ }{
+ "empty": {},
+ "nil": {
+ c1: &Context{
+ Namespace: &Namespace{
+ Active: "ns1",
+ Favorites: []string{"ns1", "ns2", "ns3"},
+ },
+ },
+ e: &Context{
+ Namespace: &Namespace{
+ Active: "ns1",
+ Favorites: []string{"ns1", "ns2", "ns3"},
+ },
+ },
+ },
+ "deltas": {
+ c1: &Context{
+ Namespace: &Namespace{
+ Active: "ns1",
+ Favorites: []string{"ns1", "ns2", "ns3"},
+ },
+ },
+ c2: &Context{
+ Namespace: &Namespace{
+ Active: "ns10",
+ Favorites: []string{"ns10", "ns11", "ns12"},
+ },
+ },
+ e: &Context{
+ Namespace: &Namespace{
+ Active: "ns1",
+ Favorites: []string{"ns1", "ns2", "ns3", "ns10", "ns11", "ns12"},
+ },
+ },
+ },
+ "deltas-locked": {
+ c1: &Context{
+ Namespace: &Namespace{
+ Active: "ns1",
+ LockFavorites: true,
+ Favorites: []string{"ns1", "ns2", "ns3"},
+ },
+ },
+ c2: &Context{
+ Namespace: &Namespace{
+ Active: "ns10",
+ Favorites: []string{"ns10", "ns11", "ns12"},
+ },
+ },
+ e: &Context{
+ Namespace: &Namespace{
+ Active: "ns1",
+ LockFavorites: true,
+ Favorites: []string{"ns1", "ns2", "ns3"},
+ },
+ },
+ },
+ }
+
+ for k, u := range uu {
+ t.Run(k, func(t *testing.T) {
+ u.c1.merge(u.c2)
+ assert.Equal(t, u.e, u.c1)
+ })
+ }
+}
diff --git a/internal/config/data/dir.go b/internal/config/data/dir.go
index b945706f..3b045578 100644
--- a/internal/config/data/dir.go
+++ b/internal/config/data/dir.go
@@ -6,6 +6,7 @@ package data
import (
"errors"
"fmt"
+ "io/fs"
"os"
"path/filepath"
"sync"
@@ -34,20 +35,21 @@ func (d *Dir) Load(n string, ct *api.Context) (*Config, error) {
if ct == nil {
return nil, errors.New("api.Context must not be nil")
}
- var (
- path = filepath.Join(d.root, SanitizeContextSubpath(ct.Cluster, n), MainConfigFile)
- cfg *Config
- err error
- )
- if f, e := os.Stat(path); os.IsNotExist(e) || f.Size() == 0 {
+ var path = filepath.Join(d.root, SanitizeContextSubpath(ct.Cluster, n), MainConfigFile)
+
+ f, err := os.Stat(path)
+ if errors.Is(err, fs.ErrPermission) {
+ return nil, err
+ }
+ if errors.Is(err, fs.ErrNotExist) || (f != nil && f.Size() == 0) {
log.Debug().Msgf("Context config not found! Generating... %q", path)
- cfg, err = d.genConfig(path, ct)
- } else {
- log.Debug().Msgf("Found existing context config: %q", path)
- cfg, err = d.loadConfig(path)
+ return d.genConfig(path, ct)
+ }
+ if err != nil {
+ return nil, err
}
- return cfg, err
+ return d.loadConfig(path)
}
func (d *Dir) genConfig(path string, ct *api.Context) (*Config, error) {
@@ -60,6 +62,10 @@ func (d *Dir) genConfig(path string, ct *api.Context) (*Config, error) {
}
func (d *Dir) Save(path string, c *Config) error {
+ if cfg, err := d.loadConfig(path); err == nil {
+ c.Merge(cfg)
+ }
+
d.mx.Lock()
defer d.mx.Unlock()
diff --git a/internal/config/data/helpers.go b/internal/config/data/helpers.go
index 59f04387..ae6cc6e9 100644
--- a/internal/config/data/helpers.go
+++ b/internal/config/data/helpers.go
@@ -4,6 +4,8 @@
package data
import (
+ "errors"
+ "io/fs"
"os"
"path/filepath"
"regexp"
@@ -38,7 +40,7 @@ func EnsureDirPath(path string, mod os.FileMode) error {
// EnsureFullPath ensures a directory exist from the given path.
func EnsureFullPath(path string, mod os.FileMode) error {
- if _, err := os.Stat(path); os.IsNotExist(err) {
+ if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
if err = os.MkdirAll(path, mod); err != nil {
return err
}
diff --git a/internal/config/data/ns.go b/internal/config/data/ns.go
index 19262894..57f9d3e6 100644
--- a/internal/config/data/ns.go
+++ b/internal/config/data/ns.go
@@ -38,6 +38,21 @@ func NewActiveNamespace(n string) *Namespace {
}
}
+func (n *Namespace) merge(old *Namespace) {
+ n.mx.Lock()
+ defer n.mx.Unlock()
+
+ if n.LockFavorites {
+ return
+ }
+ for _, fav := range old.Favorites {
+ if InList(n.Favorites, fav) {
+ continue
+ }
+ n.Favorites = append(n.Favorites, fav)
+ }
+}
+
// Validate validates a namespace is setup correctly.
func (n *Namespace) Validate(c client.Connection) {
n.mx.RLock()
diff --git a/internal/config/files.go b/internal/config/files.go
index b4b1e02b..2b246d51 100644
--- a/internal/config/files.go
+++ b/internal/config/files.go
@@ -5,6 +5,8 @@ package config
import (
_ "embed"
+ "errors"
+ "io/fs"
"os"
"path/filepath"
@@ -238,7 +240,7 @@ func EnsureBenchmarksCfgFile(cluster, context string) (string, error) {
if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil {
return "", err
}
- if _, err := os.Stat(f); os.IsNotExist(err) {
+ if _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) {
return f, os.WriteFile(f, benchmarkTpl, data.DefaultFileMod)
}
@@ -251,7 +253,7 @@ func EnsureAliasesCfgFile() (string, error) {
if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil {
return "", err
}
- if _, err := os.Stat(f); os.IsNotExist(err) {
+ if _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) {
return f, os.WriteFile(f, aliasesTpl, data.DefaultFileMod)
}
@@ -264,7 +266,7 @@ func EnsureHotkeysCfgFile() (string, error) {
if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil {
return "", err
}
- if _, err := os.Stat(f); os.IsNotExist(err) {
+ if _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) {
return f, os.WriteFile(f, hotkeysTpl, data.DefaultFileMod)
}
diff --git a/internal/config/hotkey.go b/internal/config/hotkey.go
index 91801eb8..65a651e4 100644
--- a/internal/config/hotkey.go
+++ b/internal/config/hotkey.go
@@ -4,7 +4,9 @@
package config
import (
+ "errors"
"fmt"
+ "io/fs"
"os"
"github.com/derailed/k9s/internal/config/data"
@@ -38,7 +40,7 @@ func (h HotKeys) Load(path string) error {
if err := h.LoadHotKeys(AppHotKeysFile); err != nil {
return err
}
- if _, err := os.Stat(path); os.IsNotExist(err) {
+ if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return nil
}
@@ -47,7 +49,7 @@ func (h HotKeys) Load(path string) error {
// LoadHotKeys loads plugins from a given file.
func (h HotKeys) LoadHotKeys(path string) error {
- if _, err := os.Stat(path); os.IsNotExist(err) {
+ if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return nil
}
bb, err := os.ReadFile(path)
diff --git a/internal/config/k9s.go b/internal/config/k9s.go
index a1b45558..953fb7ca 100644
--- a/internal/config/k9s.go
+++ b/internal/config/k9s.go
@@ -4,7 +4,10 @@
package config
import (
+ "errors"
"fmt"
+ "io/fs"
+ "os"
"path/filepath"
"sync"
@@ -67,7 +70,7 @@ func (k *K9s) resetConnection(conn client.Connection) {
}
// Save saves the k9s config to disk.
-func (k *K9s) Save() error {
+func (k *K9s) Save(force bool) error {
if k.getActiveConfig() == nil {
log.Warn().Msgf("Save failed. no active config detected")
return nil
@@ -77,8 +80,11 @@ func (k *K9s) Save() error {
data.SanitizeContextSubpath(k.activeConfig.Context.GetClusterName(), k.getActiveContextName()),
data.MainConfigFile,
)
+ if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) || force {
+ return k.dir.Save(path, k.getActiveConfig())
+ }
- return k.dir.Save(path, k.getActiveConfig())
+ return nil
}
// Merge merges k9s configs.
diff --git a/internal/config/k9s_int_test.go b/internal/config/k9s_int_test.go
index 0bee7fcc..f950502e 100644
--- a/internal/config/k9s_int_test.go
+++ b/internal/config/k9s_int_test.go
@@ -119,7 +119,7 @@ func Test_screenDumpDirOverride(t *testing.T) {
u := uu[k]
t.Run(k, func(t *testing.T) {
cfg := NewConfig(nil)
- assert.NoError(t, cfg.Load("testdata/configs/k9s.yaml"))
+ assert.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
cfg.K9s.manualScreenDumpDir = &u.dir
assert.Equal(t, u.e, cfg.K9s.AppScreenDumpDir())
diff --git a/internal/config/k9s_test.go b/internal/config/k9s_test.go
index 69e76189..d74c59c3 100644
--- a/internal/config/k9s_test.go
+++ b/internal/config/k9s_test.go
@@ -136,13 +136,13 @@ func TestContextScreenDumpDir(t *testing.T) {
_, err := cfg.K9s.ActivateContext("ct-1-1")
assert.NoError(t, err)
- assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
+ assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true))
assert.Equal(t, "/tmp/k9s-test/screen-dumps/cl-1/ct-1-1", cfg.K9s.ContextScreenDumpDir())
}
func TestAppScreenDumpDir(t *testing.T) {
cfg := mock.NewMockConfig()
- assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
+ assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true))
assert.Equal(t, "/tmp/k9s-test/screen-dumps", cfg.K9s.AppScreenDumpDir())
}
diff --git a/internal/config/mock/test_helpers.go b/internal/config/mock/test_helpers.go
index c5a3b69a..eae5709d 100644
--- a/internal/config/mock/test_helpers.go
+++ b/internal/config/mock/test_helpers.go
@@ -4,7 +4,9 @@
package mock
import (
+ "errors"
"fmt"
+ "io/fs"
"os"
"strings"
@@ -21,7 +23,7 @@ import (
)
func EnsureDir(d string) error {
- if _, err := os.Stat(d); os.IsNotExist(err) {
+ if _, err := os.Stat(d); errors.Is(err, fs.ErrNotExist) {
return os.MkdirAll(d, 0700)
}
if err := os.RemoveAll(d); err != nil {
diff --git a/internal/config/plugin.go b/internal/config/plugin.go
index e0992fe5..e57aa53a 100644
--- a/internal/config/plugin.go
+++ b/internal/config/plugin.go
@@ -6,6 +6,7 @@ package config
import (
"errors"
"fmt"
+ "io/fs"
"os"
"path/filepath"
"strings"
@@ -94,7 +95,7 @@ func (p Plugins) loadPluginDir(dir string) error {
}
func (p *Plugins) load(path string) error {
- if _, err := os.Stat(path); os.IsNotExist(err) {
+ if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return nil
}
bb, err := os.ReadFile(path)
diff --git a/internal/config/views.go b/internal/config/views.go
index 1bab44c8..cfa1b7ca 100644
--- a/internal/config/views.go
+++ b/internal/config/views.go
@@ -4,8 +4,11 @@
package config
import (
+ "errors"
"fmt"
+ "io/fs"
"os"
+ "strings"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/json"
@@ -25,6 +28,26 @@ type ViewSetting struct {
SortColumn string `yaml:"sortColumn"`
}
+func (v *ViewSetting) HasCols() bool {
+ return len(v.Columns) > 0
+}
+
+func (v *ViewSetting) IsBlank() bool {
+ return v == nil || len(v.Columns) == 0
+}
+
+func (v *ViewSetting) SortCol() (string, bool, error) {
+ if v == nil || v.SortColumn == "" {
+ return "", false, fmt.Errorf("no sort column specified")
+ }
+ tt := strings.Split(v.SortColumn, ":")
+ if len(tt) < 2 {
+ return "", false, fmt.Errorf("invalid sort column spec: %q. must be col-name:asc|desc", v.SortColumn)
+ }
+
+ return tt[0], tt[1] == "desc", nil
+}
+
// CustomView represents a collection of view customization.
type CustomView struct {
Views map[string]ViewSetting `yaml:"views"`
@@ -48,7 +71,7 @@ func (v *CustomView) Reset() {
// Load loads view configurations.
func (v *CustomView) Load(path string) error {
- if _, err := os.Stat(path); os.IsNotExist(err) {
+ if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return nil
}
bb, err := os.ReadFile(path)
diff --git a/internal/dao/cluster.go b/internal/dao/cluster.go
index 6dcc8d25..dab4fbf1 100644
--- a/internal/dao/cluster.go
+++ b/internal/dao/cluster.go
@@ -38,7 +38,7 @@ var (
_ RefScanner = (*DaemonSet)(nil)
_ RefScanner = (*Job)(nil)
_ RefScanner = (*CronJob)(nil)
- _ RefScanner = (*Pod)(nil)
+ // _ RefScanner = (*Pod)(nil)
)
func scanners() map[string]RefScanner {
@@ -48,7 +48,7 @@ func scanners() map[string]RefScanner {
"apps/v1/daemonsets": &DaemonSet{},
"batch/v1/jobs": &Job{},
"batch/v1/cronjobs": &CronJob{},
- "v1/pods": &Pod{},
+ // "v1/pods": &Pod{},
}
}
diff --git a/internal/dao/cm.go b/internal/dao/cm.go
new file mode 100644
index 00000000..0910d704
--- /dev/null
+++ b/internal/dao/cm.go
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package dao
+
+var (
+ _ Accessor = (*ConfigMap)(nil)
+)
+
+// ConfigMap represents a configmap resource.
+type ConfigMap struct {
+ Resource
+}
diff --git a/internal/dao/crd.go b/internal/dao/crd.go
index 16f06076..5f8455a2 100644
--- a/internal/dao/crd.go
+++ b/internal/dao/crd.go
@@ -3,15 +3,6 @@
package dao
-import (
- "context"
-
- "github.com/derailed/k9s/internal"
- v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
- "k8s.io/apimachinery/pkg/labels"
- "k8s.io/apimachinery/pkg/runtime"
-)
-
var (
_ Accessor = (*CustomResourceDefinition)(nil)
_ Nuker = (*CustomResourceDefinition)(nil)
@@ -21,28 +12,3 @@ var (
type CustomResourceDefinition struct {
Resource
}
-
-// IsHappy check for happy deployments.
-func (c *CustomResourceDefinition) IsHappy(crd v1.CustomResourceDefinition) bool {
- versions := make([]string, 0, 3)
- for _, v := range crd.Spec.Versions {
- if v.Served && !v.Deprecated {
- versions = append(versions, v.Name)
- break
- }
- }
-
- return len(versions) > 0
-}
-
-// List returns a collection of nodes.
-func (c *CustomResourceDefinition) List(ctx context.Context, _ string) ([]runtime.Object, error) {
- strLabel, ok := ctx.Value(internal.KeyLabels).(string)
- labelSel := labels.Everything()
- if sel, e := labels.ConvertSelectorToLabelsMap(strLabel); ok && e == nil {
- labelSel = sel.AsSelector()
- }
-
- const gvr = "apiextensions.k8s.io/v1/customresourcedefinitions"
- return c.getFactory().List(gvr, "-", false, labelSel)
-}
diff --git a/internal/dao/dp.go b/internal/dao/dp.go
index 4f88d4a0..f3273399 100644
--- a/internal/dao/dp.go
+++ b/internal/dao/dp.go
@@ -49,11 +49,6 @@ func (d *Deployment) ListImages(ctx context.Context, fqn string) ([]string, erro
return render.ExtractImages(&dp.Spec.Template.Spec), nil
}
-// IsHappy check for happy deployments.
-func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
- return dp.Status.Replicas == dp.Status.AvailableReplicas
-}
-
// Scale a Deployment.
func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error {
ns, n := client.Namespaced(path)
diff --git a/internal/dao/ds.go b/internal/dao/ds.go
index 0e2d9430..b0bf8c0b 100644
--- a/internal/dao/ds.go
+++ b/internal/dao/ds.go
@@ -51,11 +51,6 @@ func (d *DaemonSet) ListImages(ctx context.Context, fqn string) ([]string, error
return render.ExtractImages(&ds.Spec.Template.Spec), nil
}
-// IsHappy check for happy deployments.
-func (d *DaemonSet) IsHappy(ds appsv1.DaemonSet) bool {
- return ds.Status.DesiredNumberScheduled == ds.Status.CurrentNumberScheduled
-}
-
// Restart a DaemonSet rollout.
func (d *DaemonSet) Restart(ctx context.Context, path string) error {
o, err := d.getFactory().Get("apps/v1/daemonsets", path, true, labels.Everything())
@@ -140,7 +135,7 @@ func podLogs(ctx context.Context, sel map[string]string, opts *LogOptions) ([]Lo
}
opts.MultiPods = true
- po := Pod{}
+ var po Pod
po.Init(f, client.NewGVR("v1/pods"))
outs := make([]LogChan, 0, len(oo))
diff --git a/internal/dao/helpers.go b/internal/dao/helpers.go
index f4818f48..552d3137 100644
--- a/internal/dao/helpers.go
+++ b/internal/dao/helpers.go
@@ -6,22 +6,58 @@ package dao
import (
"bytes"
"errors"
+ "fmt"
"math"
- "regexp"
+ "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
+ v1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/printers"
)
-const defaultServiceAccount = "default"
-
-var (
- inverseRx = regexp.MustCompile(`\A\!`)
- fuzzyRx = regexp.MustCompile(`\A-f\s?([\w-]+)\b`)
+const (
+ defaultServiceAccount = "default"
+ defaultContainerAnnotation = "kubectl.kubernetes.io/default-container"
)
+// GetDefaultContainer returns a container name if specified in an annotation.
+func GetDefaultContainer(m metav1.ObjectMeta, spec v1.PodSpec) (string, bool) {
+ defaultContainer, ok := m.Annotations[defaultContainerAnnotation]
+ if !ok {
+ return "", false
+ }
+
+ for _, container := range spec.Containers {
+ if container.Name == defaultContainer {
+ return defaultContainer, true
+ }
+ }
+ log.Warn().Msg(defaultContainer + " container not found. " + defaultContainerAnnotation + " annotation will be ignored")
+
+ return "", false
+}
+
+func extractFQN(o runtime.Object) string {
+ u, ok := o.(*unstructured.Unstructured)
+ if !ok {
+ log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o))
+ return client.NA
+ }
+
+ return FQN(u.GetNamespace(), u.GetName())
+}
+
+// FQN returns a fully qualified resource name.
+func FQN(ns, n string) string {
+ if ns == "" {
+ return n
+ }
+ return ns + "/" + n
+}
+
func inList(ll []string, s string) bool {
for _, l := range ll {
if l == s {
@@ -31,24 +67,6 @@ func inList(ll []string, s string) bool {
return false
}
-// IsInverseSelector checks if inverse char has been provided.
-func IsInverseSelector(s string) bool {
- if s == "" {
- return false
- }
- return inverseRx.MatchString(s)
-}
-
-// HasFuzzySelector checks if query is fuzzy.
-func HasFuzzySelector(s string) (string, bool) {
- mm := fuzzyRx.FindStringSubmatch(s)
- if len(mm) != 2 {
- return "", false
- }
-
- return mm[1], true
-}
-
func toPerc(v1, v2 float64) float64 {
if v2 == 0 {
return 0
diff --git a/internal/dao/log_items.go b/internal/dao/log_items.go
index d8cd1707..898c0c84 100644
--- a/internal/dao/log_items.go
+++ b/internal/dao/log_items.go
@@ -10,6 +10,7 @@ import (
"strings"
"sync"
+ "github.com/derailed/k9s/internal"
"github.com/sahilm/fuzzy"
)
@@ -174,7 +175,7 @@ func (l *LogItems) Filter(index int, q string, showTime bool) ([]int, [][]int, e
if q == "" {
return nil, nil, nil
}
- if f, ok := HasFuzzySelector(q); ok {
+ if f, ok := internal.IsFuzzySelector(q); ok {
mm, ii := l.fuzzyFilter(index, f, showTime)
return mm, ii, nil
}
@@ -200,7 +201,7 @@ func (l *LogItems) fuzzyFilter(index int, q string, showTime bool) ([]int, [][]i
func (l *LogItems) filterLogs(index int, q string, showTime bool) ([]int, [][]int, error) {
var invert bool
- if IsInverseSelector(q) {
+ if internal.IsInverseSelector(q) {
invert = true
q = q[1:]
}
diff --git a/internal/dao/ns.go b/internal/dao/ns.go
index 4daec5f9..33a63dce 100644
--- a/internal/dao/ns.go
+++ b/internal/dao/ns.go
@@ -3,27 +3,11 @@
package dao
-import (
- "context"
-
- "k8s.io/apimachinery/pkg/runtime"
-)
-
var (
- _ Accessor = (*Pod)(nil)
+ _ Accessor = (*Namespace)(nil)
)
// Namespace represents a namespace resource.
type Namespace struct {
- Generic
-}
-
-// List returns a collection of namespaces.
-func (n *Namespace) List(ctx context.Context, ns string) ([]runtime.Object, error) {
- oo, err := n.Generic.List(ctx, ns)
- if err != nil {
- return nil, err
- }
-
- return oo, nil
+ Resource
}
diff --git a/internal/dao/pod.go b/internal/dao/pod.go
index e9c6e09f..4b03d66e 100644
--- a/internal/dao/pod.go
+++ b/internal/dao/pod.go
@@ -37,9 +37,8 @@ var (
)
const (
- logRetryCount = 20
- logRetryWait = 1 * time.Second
- defaultContainerAnnotation = "kubectl.kubernetes.io/default-container"
+ logRetryCount = 20
+ logRetryWait = 1 * time.Second
)
// Pod represents a pod resource.
@@ -47,17 +46,6 @@ type Pod struct {
Resource
}
-// IsHappy check for happy deployments.
-func (p *Pod) IsHappy(po v1.Pod) bool {
- for _, c := range po.Status.Conditions {
- if c.Status == v1.ConditionFalse {
- return false
- }
- }
-
- return true
-}
-
// Get returns a resource instance if found, else an error.
func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
o, err := p.Resource.Get(ctx, path)
@@ -425,24 +413,6 @@ func MetaFQN(m metav1.ObjectMeta) string {
return FQN(m.Namespace, m.Name)
}
-// FQN returns a fully qualified resource name.
-func FQN(ns, n string) string {
- if ns == "" {
- return n
- }
- return ns + "/" + n
-}
-
-func extractFQN(o runtime.Object) string {
- u, ok := o.(*unstructured.Unstructured)
- if !ok {
- log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o))
- return client.NA
- }
-
- return FQN(u.GetNamespace(), u.GetName())
-}
-
// GetPodSpec returns a pod spec given a resource.
func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) {
pod, err := p.GetInstance(path)
@@ -500,21 +470,6 @@ func (p *Pod) isControlled(path string) (string, bool, error) {
return "", false, nil
}
-// GetDefaultContainer returns a container name if specified in an annotation.
-func GetDefaultContainer(m metav1.ObjectMeta, spec v1.PodSpec) (string, bool) {
- defaultContainer, ok := m.Annotations[defaultContainerAnnotation]
- if ok {
- for _, container := range spec.Containers {
- if container.Name == defaultContainer {
- return defaultContainer, true
- }
- }
- log.Warn().Msg(defaultContainer + " container not found. " + defaultContainerAnnotation + " annotation will be ignored")
- }
-
- return "", false
-}
-
func (p *Pod) Sanitize(ctx context.Context, ns string) (int, error) {
oo, err := p.Resource.List(ctx, ns)
if err != nil {
diff --git a/internal/dao/popeye.go b/internal/dao/popeye.go
index ec1aa801..7cb08e9a 100644
--- a/internal/dao/popeye.go
+++ b/internal/dao/popeye.go
@@ -3,7 +3,7 @@
package dao
-// !!BOZO!!
+// !!BOZO!! Popeye
// import (
// "bytes"
// "context"
diff --git a/internal/dao/registry.go b/internal/dao/registry.go
index 4060d4e9..3aaf1bf5 100644
--- a/internal/dao/registry.go
+++ b/internal/dao/registry.go
@@ -89,29 +89,32 @@ func NewMeta() *Meta {
// Customize here for non resource types or types with metrics or logs.
func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
m := Accessors{
- client.NewGVR("workloads"): &Workload{},
- client.NewGVR("contexts"): &Context{},
- client.NewGVR("containers"): &Container{},
- client.NewGVR("scans"): &ImageScan{},
- client.NewGVR("screendumps"): &ScreenDump{},
- client.NewGVR("benchmarks"): &Benchmark{},
- client.NewGVR("portforwards"): &PortForward{},
- client.NewGVR("v1/services"): &Service{},
- client.NewGVR("v1/pods"): &Pod{},
- client.NewGVR("v1/nodes"): &Node{},
- client.NewGVR("apps/v1/deployments"): &Deployment{},
- client.NewGVR("apps/v1/daemonsets"): &DaemonSet{},
- client.NewGVR("apps/v1/statefulsets"): &StatefulSet{},
- client.NewGVR("apps/v1/replicasets"): &ReplicaSet{},
- client.NewGVR("batch/v1/cronjobs"): &CronJob{},
- client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
- client.NewGVR("batch/v1/jobs"): &Job{},
- client.NewGVR("v1/namespaces"): &Namespace{},
- // !!BOZO!!
+ client.NewGVR("workloads"): &Workload{},
+ client.NewGVR("contexts"): &Context{},
+ client.NewGVR("containers"): &Container{},
+ client.NewGVR("scans"): &ImageScan{},
+ client.NewGVR("screendumps"): &ScreenDump{},
+ client.NewGVR("benchmarks"): &Benchmark{},
+ client.NewGVR("portforwards"): &PortForward{},
+ client.NewGVR("dir"): &Dir{},
+ client.NewGVR("v1/services"): &Service{},
+ client.NewGVR("v1/pods"): &Pod{},
+ client.NewGVR("v1/nodes"): &Node{},
+ client.NewGVR("v1/namespaces"): &Namespace{},
+ client.NewGVR("v1/configmap"): &ConfigMap{},
+ client.NewGVR("v1/secrets"): &Secret{},
+ client.NewGVR("apps/v1/deployments"): &Deployment{},
+ client.NewGVR("apps/v1/daemonsets"): &DaemonSet{},
+ client.NewGVR("apps/v1/statefulsets"): &StatefulSet{},
+ client.NewGVR("apps/v1/replicasets"): &ReplicaSet{},
+ client.NewGVR("batch/v1/cronjobs"): &CronJob{},
+ client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
+ client.NewGVR("batch/v1/jobs"): &Job{},
+ client.NewGVR("helm"): &HelmChart{},
+ client.NewGVR("helm-history"): &HelmHistory{},
+ client.NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions"): &CustomResourceDefinition{},
+ // !!BOZO!! Popeye
//client.NewGVR("popeye"): &Popeye{},
- client.NewGVR("helm"): &HelmChart{},
- client.NewGVR("helm-history"): &HelmHistory{},
- client.NewGVR("dir"): &Dir{},
}
r, ok := m[gvr]
@@ -369,6 +372,7 @@ func loadPreferred(f Factory, m ResourceMetas) error {
if err != nil {
return err
}
+ dial.Invalidate()
rr, err := dial.ServerPreferredResources()
if err != nil {
log.Debug().Err(err).Msgf("Failed to load preferred resources")
diff --git a/internal/dao/secret.go b/internal/dao/secret.go
index 8cc47868..49dcaa30 100644
--- a/internal/dao/secret.go
+++ b/internal/dao/secret.go
@@ -15,13 +15,13 @@ import (
// Secret represents a secret K8s resource.
type Secret struct {
- Table
+ Resource
decode bool
}
// Describe describes a secret that can be encoded or decoded.
func (s *Secret) Describe(path string) (string, error) {
- encodedDescription, err := s.Table.Describe(path)
+ encodedDescription, err := s.Generic.Describe(path)
if err != nil {
return "", err
@@ -51,13 +51,13 @@ func (s *Secret) Decode(encodedDescription, path string) (string, error) {
dataEndIndex := strings.Index(encodedDescription, "====")
if dataEndIndex == -1 {
- return "", fmt.Errorf("Unable to find data section in secret description")
+ return "", fmt.Errorf("unable to find data section in secret description")
}
dataEndIndex += 4
if dataEndIndex >= len(encodedDescription) {
- return "", fmt.Errorf("Data section in secret description is invalid")
+ return "", fmt.Errorf("data section in secret description is invalid")
}
// Remove the encoded part from k8s's describe API
diff --git a/internal/dao/sts.go b/internal/dao/sts.go
index f562adef..01376409 100644
--- a/internal/dao/sts.go
+++ b/internal/dao/sts.go
@@ -50,11 +50,6 @@ func (s *StatefulSet) ListImages(ctx context.Context, fqn string) ([]string, err
return render.ExtractImages(&sts.Spec.Template.Spec), nil
}
-// IsHappy check for happy sts.
-func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
- return sts.Status.Replicas == sts.Status.ReadyReplicas
-}
-
// Scale a StatefulSet.
func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error {
ns, n := client.Namespaced(path)
diff --git a/internal/dao/table.go b/internal/dao/table.go
index c2569da1..6936772f 100644
--- a/internal/dao/table.go
+++ b/internal/dao/table.go
@@ -16,7 +16,9 @@ import (
"k8s.io/client-go/rest"
)
-// BOZO!! Figure out how to convert to table def and use factory.
+const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json"
+
+var genScheme = runtime.NewScheme()
// Table retrieves K8s resources as tabular data.
type Table struct {
@@ -25,19 +27,19 @@ type Table struct {
// Get returns a given resource.
func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
- a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName)
- _, codec := t.codec()
-
- c, err := t.getClient()
+ f, p := t.codec()
+ c, err := t.getClient(f)
if err != nil {
return nil, err
}
+
ns, n := client.Namespaced(path)
+ a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName)
req := c.Get().
SetHeader("Accept", a).
Name(n).
Resource(t.gvr.R()).
- VersionedParams(&metav1.TableOptions{}, codec)
+ VersionedParams(&metav1.TableOptions{}, p)
if ns != client.ClusterScope {
req = req.Namespace(ns)
}
@@ -48,18 +50,24 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
// List all Resources in a given namespace.
func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
labelSel, _ := ctx.Value(internal.KeyLabels).(string)
- a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName)
- _, codec := t.codec()
+ fieldSel, _ := ctx.Value(internal.KeyFields).(string)
- c, err := t.getClient()
+ f, p := t.codec()
+ c, err := t.getClient(f)
if err != nil {
return nil, err
}
+ a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName)
o, err := c.Get().
SetHeader("Accept", a).
Namespace(ns).
Resource(t.gvr.R()).
- VersionedParams(&metav1.ListOptions{LabelSelector: labelSel}, codec).
+ VersionedParams(&metav1.ListOptions{
+ LabelSelector: labelSel,
+ FieldSelector: fieldSel,
+ ResourceVersion: "0",
+ ResourceVersionMatch: v1.ResourceVersionMatchNotOlderThan,
+ }, p).
Do(ctx).Get()
if err != nil {
return nil, err
@@ -71,9 +79,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// ----------------------------------------------------------------------------
// Helpers...
-const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json"
-
-func (t *Table) getClient() (*rest.RESTClient, error) {
+func (t *Table) getClient(f serializer.CodecFactory) (*rest.RESTClient, error) {
cfg, err := t.Client().RestConfig()
if err != nil {
return nil, err
@@ -84,8 +90,7 @@ func (t *Table) getClient() (*rest.RESTClient, error) {
if t.gvr.G() == "" {
cfg.APIPath = "/api"
}
- codec, _ := t.codec()
- cfg.NegotiatedSerializer = codec.WithoutConversion()
+ cfg.NegotiatedSerializer = f.WithoutConversion()
crRestClient, err := rest.RESTClientFor(cfg)
if err != nil {
@@ -96,11 +101,12 @@ func (t *Table) getClient() (*rest.RESTClient, error) {
}
func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) {
- scheme := runtime.NewScheme()
+ var tt metav1.Table
+ opts := metav1.TableOptions{IncludeObject: v1.IncludeObject}
gv := t.gvr.GV()
- metav1.AddToGroupVersion(scheme, gv)
- scheme.AddKnownTypes(gv, &metav1.Table{}, &metav1.TableOptions{IncludeObject: v1.IncludeObject})
- scheme.AddKnownTypes(metav1.SchemeGroupVersion, &metav1.Table{}, &metav1.TableOptions{IncludeObject: v1.IncludeObject})
+ metav1.AddToGroupVersion(genScheme, gv)
+ genScheme.AddKnownTypes(gv, &tt, &opts)
+ genScheme.AddKnownTypes(metav1.SchemeGroupVersion, &tt, &opts)
- return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme)
+ return serializer.NewCodecFactory(genScheme), runtime.NewParameterCodec(genScheme)
}
diff --git a/internal/helpers.go b/internal/helpers.go
new file mode 100644
index 00000000..aa1d43e5
--- /dev/null
+++ b/internal/helpers.go
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package internal
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/derailed/k9s/internal/view/cmd"
+)
+
+var (
+ inverseRx = regexp.MustCompile(`\A\!`)
+ fuzzyRx = regexp.MustCompile(`\A-f\s?([\w-]+)\b`)
+ labelRx = regexp.MustCompile(`\A\-l`)
+)
+
+// Helpers...
+
+// IsInverseSelector checks if inverse char has been provided.
+func IsInverseSelector(s string) bool {
+ if s == "" {
+ return false
+ }
+ return inverseRx.MatchString(s)
+}
+
+// IsLabelSelector checks if query is a label query.
+func IsLabelSelector(s string) bool {
+ if labelRx.MatchString(s) {
+ return true
+ }
+
+ return !strings.Contains(s, " ") && cmd.ToLabels(s) != nil
+}
+
+// IsFuzzySelector checks if query is fuzzy.
+func IsFuzzySelector(s string) (string, bool) {
+ mm := fuzzyRx.FindStringSubmatch(s)
+ if len(mm) != 2 {
+ return "", false
+ }
+
+ return mm[1], true
+}
diff --git a/internal/helpers_test.go b/internal/helpers_test.go
new file mode 100644
index 00000000..7ac6bc99
--- /dev/null
+++ b/internal/helpers_test.go
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package internal_test
+
+import (
+ "testing"
+
+ "github.com/derailed/k9s/internal"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestIsLabelSelector(t *testing.T) {
+ uu := map[string]struct {
+ s string
+ ok bool
+ }{
+ "empty": {s: ""},
+ "cool": {s: "-l app=fred,env=blee", ok: true},
+ "no-flag": {s: "app=fred,env=blee", ok: true},
+ "no-space": {s: "-lapp=fred,env=blee", ok: true},
+ "wrong-flag": {s: "-f app=fred,env=blee"},
+ "missing-key": {s: "=fred"},
+ "missing-val": {s: "fred="},
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.ok, internal.IsLabelSelector(u.s))
+ })
+ }
+}
diff --git a/internal/model/cluster.go b/internal/model/cluster.go
index 3182a0e1..e598217e 100644
--- a/internal/model/cluster.go
+++ b/internal/model/cluster.go
@@ -108,7 +108,7 @@ func (c *Cluster) Metrics(ctx context.Context, mx *client.ClusterMetrics) error
}
}
if nn == nil {
- return errors.New("Unable to fetch nodes list")
+ return errors.New("unable to fetch nodes list")
}
if len(nn.Items) > 0 {
c.cache.Add(clusterNodesKey, nn, clusterCacheExpiry)
diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go
index 5f1653be..62f6e7f1 100644
--- a/internal/model/cluster_info.go
+++ b/internal/model/cluster_info.go
@@ -143,8 +143,6 @@ func (c *ClusterInfo) Refresh() {
var mx client.ClusterMetrics
if err := c.cluster.Metrics(ctx, &mx); err == nil {
data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral
- } else {
- log.Warn().Err(err).Msgf("Cluster metrics failed")
}
}
data.K9sVer = c.version
diff --git a/internal/model/describe.go b/internal/model/describe.go
index 55b525bf..2d5d845a 100644
--- a/internal/model/describe.go
+++ b/internal/model/describe.go
@@ -12,6 +12,7 @@ import (
"time"
backoff "github.com/cenkalti/backoff/v4"
+ "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/rs/zerolog/log"
@@ -66,7 +67,7 @@ func (d *Describe) filter(q string, lines []string) fuzzy.Matches {
if q == "" {
return nil
}
- if f, ok := dao.HasFuzzySelector(q); ok {
+ if f, ok := internal.IsFuzzySelector(q); ok {
return d.fuzzyFilter(strings.TrimSpace(f), lines)
}
return rxFilter(q, lines)
diff --git a/internal/model/pulse_health.go b/internal/model/pulse_health.go
index 3cf71c23..0f1891bb 100644
--- a/internal/model/pulse_health.go
+++ b/internal/model/pulse_health.go
@@ -10,6 +10,7 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/health"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -123,14 +124,14 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check,
if !ok {
return nil, fmt.Errorf("expecting a meta table but got %T", oo[0])
}
- rows := make(render.Rows, len(table.Rows))
- re, _ := meta.Renderer.(Generic)
+ rows := make(model1.Rows, len(table.Rows))
+ re, _ := meta.Renderer.(model1.Generic)
re.SetTable(ns, table)
for i, row := range table.Rows {
if err := re.Render(row, ns, &rows[i]); err != nil {
return nil, err
}
- if !render.Happy(ns, re.Header(ns), rows[i]) {
+ if !model1.IsValid(ns, re.Header(ns), rows[i]) {
c.Inc(health.S2)
continue
}
@@ -140,12 +141,12 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check,
return c, nil
}
c.Total(int64(len(oo)))
- rr, re := make(render.Rows, len(oo)), meta.Renderer
+ rr, re := make(model1.Rows, len(oo)), meta.Renderer
for i, o := range oo {
if err := re.Render(o, ns, &rr[i]); err != nil {
return nil, err
}
- if !render.Happy(ns, re.Header(ns), rr[i]) {
+ if !model1.IsValid(ns, re.Header(ns), rr[i]) {
c.Inc(health.S2)
continue
}
diff --git a/internal/model/registry.go b/internal/model/registry.go
index 55db15f8..4ad382da 100644
--- a/internal/model/registry.go
+++ b/internal/model/registry.go
@@ -82,7 +82,7 @@ var Registry = map[string]ResourceMeta{
DAO: &dao.Alias{},
Renderer: &render.Alias{},
},
- // !!BOZO!!
+ // !!BOZO!! Popeye
//"popeye": {
// DAO: &dao.Popeye{},
// Renderer: &render.Popeye{},
@@ -102,8 +102,17 @@ var Registry = map[string]ResourceMeta{
TreeRenderer: &xray.Pod{},
},
"v1/namespaces": {
+ DAO: &dao.Namespace{},
Renderer: &render.Namespace{},
},
+ "v1/secrets": {
+ DAO: &dao.Secret{},
+ Renderer: &render.Secret{},
+ },
+ "v1/configmaps": {
+ DAO: &dao.ConfigMap{},
+ Renderer: &render.ConfigMap{},
+ },
"v1/nodes": {
DAO: &dao.Node{},
Renderer: &render.Node{},
@@ -113,6 +122,10 @@ var Registry = map[string]ResourceMeta{
Renderer: &render.Service{},
TreeRenderer: &xray.Service{},
},
+ "v1/events": {
+ DAO: &dao.Table{},
+ Renderer: &render.Event{},
+ },
"v1/serviceaccounts": {
Renderer: &render.ServiceAccount{},
},
@@ -122,14 +135,6 @@ var Registry = map[string]ResourceMeta{
"v1/persistentvolumeclaims": {
Renderer: &render.PersistentVolumeClaim{},
},
- "v1/events": {
- DAO: &dao.Table{},
- Renderer: &render.Event{},
- },
- "v1/secrets": {
- DAO: &dao.Secret{},
- Renderer: &render.Generic{},
- },
// Apps...
"apps/v1/deployments": {
@@ -169,6 +174,7 @@ var Registry = map[string]ResourceMeta{
// CRDs...
"apiextensions.k8s.io/v1/customresourcedefinitions": {
+ DAO: &dao.CustomResourceDefinition{},
Renderer: &render.CustomResourceDefinition{},
},
diff --git a/internal/model/rev_values.go b/internal/model/rev_values.go
index 08badab6..a0b8cb98 100644
--- a/internal/model/rev_values.go
+++ b/internal/model/rev_values.go
@@ -10,6 +10,7 @@ import (
"time"
backoff "github.com/cenkalti/backoff/v4"
+ "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/rs/zerolog/log"
@@ -84,7 +85,7 @@ func (v *RevValues) filter(q string, lines []string) fuzzy.Matches {
if q == "" {
return nil
}
- if f, ok := dao.HasFuzzySelector(q); ok {
+ if f, ok := internal.IsFuzzySelector(q); ok {
return v.fuzzyFilter(strings.TrimSpace(f), lines)
}
return rxFilter(q, lines)
diff --git a/internal/model/stack.go b/internal/model/stack.go
index b660a1ab..55221b85 100644
--- a/internal/model/stack.go
+++ b/internal/model/stack.go
@@ -112,6 +112,7 @@ func (s *Stack) Pop() (Component, bool) {
s.mx.Lock()
{
c = s.components[len(s.components)-1]
+ c.Stop()
s.components = s.components[:len(s.components)-1]
}
s.mx.Unlock()
diff --git a/internal/model/table.go b/internal/model/table.go
index 62593827..ce848724 100644
--- a/internal/model/table.go
+++ b/internal/model/table.go
@@ -14,7 +14,7 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -25,7 +25,7 @@ const initRefreshRate = 300 * time.Millisecond
// TableListener represents a table model listener.
type TableListener interface {
// TableDataChanged notifies the model data changed.
- TableDataChanged(*render.TableData)
+ TableDataChanged(*model1.TableData)
// TableLoadFailed notifies the load failed.
TableLoadFailed(error)
@@ -34,21 +34,20 @@ type TableListener interface {
// Table represents a table model.
type Table struct {
gvr client.GVR
- namespace string
- data *render.TableData
+ data *model1.TableData
listeners []TableListener
inUpdate int32
refreshRate time.Duration
instance string
- mx sync.RWMutex
labelFilter string
+ mx sync.RWMutex
}
// NewTable returns a new table model.
func NewTable(gvr client.GVR) *Table {
return &Table{
gvr: gvr,
- data: render.NewTableData(),
+ data: model1.NewTableData(gvr),
refreshRate: 2 * time.Second,
}
}
@@ -141,18 +140,17 @@ func (t *Table) Delete(ctx context.Context, path string, propagation *metav1.Del
// GetNamespace returns the model namespace.
func (t *Table) GetNamespace() string {
- return t.namespace
+ return t.data.GetNamespace()
}
// SetNamespace sets up model namespace.
func (t *Table) SetNamespace(ns string) {
- t.namespace = ns
- t.data.Clear()
+ t.data.Reset(ns)
}
// InNamespace checks if current namespace matches desired namespace.
func (t *Table) InNamespace(ns string) bool {
- return len(t.data.RowEvents) > 0 && t.namespace == ns
+ return t.data.GetNamespace() == ns && !t.data.Empty()
}
// SetRefreshRate sets model refresh duration.
@@ -162,7 +160,7 @@ func (t *Table) SetRefreshRate(d time.Duration) {
// ClusterWide checks if resource is scope for all namespaces.
func (t *Table) ClusterWide() bool {
- return client.IsClusterWide(t.namespace)
+ return client.IsClusterWide(t.data.GetNamespace())
}
// Empty returns true if no model data.
@@ -170,13 +168,13 @@ func (t *Table) Empty() bool {
return t.data.Empty()
}
-// Count returns the row count.
-func (t *Table) Count() int {
- return t.data.Count()
+// RowCount returns the row count.
+func (t *Table) RowCount() int {
+ return t.data.RowCount()
}
// Peek returns model data.
-func (t *Table) Peek() *render.TableData {
+func (t *Table) Peek() *model1.TableData {
t.mx.RLock()
defer t.mx.RUnlock()
@@ -184,8 +182,6 @@ func (t *Table) Peek() *render.TableData {
}
func (t *Table) updater(ctx context.Context) {
- defer log.Debug().Msgf("TABLE-UPDATER canceled -- %q", t.gvr)
-
bf := backoff.NewExponentialBackOff()
bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval
rate := initRefreshRate
@@ -199,7 +195,7 @@ func (t *Table) updater(ctx context.Context) {
return t.refresh(ctx)
}, backoff.WithContext(bf, ctx))
if err != nil {
- log.Error().Err(err).Msgf("Retry failed")
+ log.Warn().Err(err).Msgf("reconciler exited")
t.fireTableLoadFailed(err)
return
}
@@ -229,24 +225,25 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err
}
a.Init(factory, t.gvr)
- ns := client.CleanseNamespace(t.namespace)
- if client.IsClusterScoped(t.namespace) {
+ t.mx.RLock()
+ ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter)
+ t.mx.RUnlock()
+
+ ns := client.CleanseNamespace(t.data.GetNamespace())
+ if client.IsClusterScoped(ns) {
ns = client.BlankNamespace
}
- ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter)
return a.List(ctx, ns)
}
func (t *Table) reconcile(ctx context.Context) error {
- t.mx.Lock()
- defer t.mx.Unlock()
- meta := resourceMeta(t.gvr)
- ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter)
var (
oo []runtime.Object
err error
)
+ meta := resourceMeta(t.gvr)
+ ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter)
if t.instance == "" {
oo, err = t.list(ctx, meta.DAO)
} else {
@@ -257,41 +254,10 @@ func (t *Table) reconcile(ctx context.Context) error {
return err
}
- var rows render.Rows
- if len(oo) > 0 {
- if meta.Renderer.IsGeneric() {
- table, ok := oo[0].(*metav1.Table)
- if !ok {
- return fmt.Errorf("expecting a meta table but got %T", oo[0])
- }
- rows = make(render.Rows, len(table.Rows))
- if err := genericHydrate(t.namespace, table, rows, meta.Renderer); err != nil {
- return err
- }
- } else {
- rows = make(render.Rows, len(oo))
- if err := hydrate(t.namespace, oo, rows, meta.Renderer); err != nil {
- return err
- }
- }
- }
-
- // if labelSelector in place might as well clear the model data.
- sel, ok := ctx.Value(internal.KeyLabels).(string)
- if ok && sel != "" {
- t.data.Clear()
- }
- t.data.Update(rows)
- t.data.SetHeader(t.namespace, meta.Renderer.Header(t.namespace))
-
- if len(t.data.Header) == 0 {
- return fmt.Errorf("fail to list resource %s", t.gvr)
- }
-
- return nil
+ return t.data.Reconcile(ctx, meta.Renderer, oo)
}
-func (t *Table) fireTableChanged(data *render.TableData) {
+func (t *Table) fireTableChanged(data *model1.TableData) {
var ll []TableListener
t.mx.RLock()
ll = t.listeners
@@ -312,43 +278,3 @@ func (t *Table) fireTableLoadFailed(err error) {
l.TableLoadFailed(err)
}
}
-
-// ----------------------------------------------------------------------------
-// Helpers...
-
-func hydrate(ns string, oo []runtime.Object, rr render.Rows, re Renderer) error {
- for i, o := range oo {
- if err := re.Render(o, ns, &rr[i]); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// Generic represents a generic resource.
-type Generic interface {
- // SetTable sets up the resource tabular definition.
- SetTable(ns string, table *metav1.Table)
-
- // Header returns a resource header.
- Header(ns string) render.Header
-
- // Render renders the resource.
- Render(o interface{}, ns string, row *render.Row) error
-}
-
-func genericHydrate(ns string, table *metav1.Table, rr render.Rows, re Renderer) error {
- gr, ok := re.(Generic)
- if !ok {
- return fmt.Errorf("expecting generic renderer but got %T", re)
- }
- gr.SetTable(ns, table)
- for i, row := range table.Rows {
- if err := gr.Render(row, ns, &rr[i]); err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/internal/model/table_int_test.go b/internal/model/table_int_test.go
index 522cd18c..ea7c0390 100644
--- a/internal/model/table_int_test.go
+++ b/internal/model/table_int_test.go
@@ -13,11 +13,11 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/watch"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
@@ -35,9 +35,9 @@ func TestTableReconcile(t *testing.T) {
err := ta.reconcile(ctx)
assert.Nil(t, err)
data := ta.Peek()
- assert.Equal(t, 23, len(data.Header))
- assert.Equal(t, 1, len(data.RowEvents))
- assert.Equal(t, client.NamespaceAll, data.Namespace)
+ assert.Equal(t, 23, data.HeaderCount())
+ assert.Equal(t, 1, data.RowCount())
+ assert.Equal(t, client.NamespaceAll, data.GetNamespace())
}
func TestTableList(t *testing.T) {
@@ -66,12 +66,10 @@ func TestTableGet(t *testing.T) {
}
func TestTableMeta(t *testing.T) {
- pd := dao.Pod{}
- pd.Init(makeFactory(), client.NewGVR("v1/pods"))
uu := map[string]struct {
gvr string
accessor dao.Accessor
- renderer Renderer
+ renderer model1.Renderer
}{
"generic": {
gvr: "containers",
@@ -84,9 +82,9 @@ func TestTableMeta(t *testing.T) {
renderer: &render.Node{},
},
"table": {
- gvr: "v1/configmaps",
+ gvr: "v1/events",
accessor: &dao.Table{},
- renderer: &render.Generic{},
+ renderer: &render.Event{},
},
}
@@ -100,44 +98,6 @@ func TestTableMeta(t *testing.T) {
}
}
-func TestTableHydrate(t *testing.T) {
- oo := []runtime.Object{
- &render.PodWithMetrics{Raw: load(t, "p1")},
- }
- rr := make([]render.Row, 1)
-
- assert.Nil(t, hydrate("blee", oo, rr, render.Pod{}))
- assert.Equal(t, 1, len(rr))
- assert.Equal(t, 23, len(rr[0].Fields))
-}
-
-func TestTableGenericHydrate(t *testing.T) {
- raw := raw(t, "p1")
- tt := metav1beta1.Table{
- ColumnDefinitions: []metav1beta1.TableColumnDefinition{
- {Name: "c1"},
- {Name: "c2"},
- },
- Rows: []metav1beta1.TableRow{
- {
- Cells: []interface{}{"fred", 10},
- Object: runtime.RawExtension{Raw: raw},
- },
- {
- Cells: []interface{}{"blee", 20},
- Object: runtime.RawExtension{Raw: raw},
- },
- },
- }
- rr := make([]render.Row, 2)
- re := render.Generic{}
- re.SetTable("blee", &tt)
-
- assert.Nil(t, genericHydrate("blee", &tt, rr, &re))
- assert.Equal(t, 2, len(rr))
- assert.Equal(t, 3, len(rr[0].Fields))
-}
-
// ----------------------------------------------------------------------------
// Helpers...
@@ -162,12 +122,6 @@ func load(t *testing.T, n string) *unstructured.Unstructured {
return &o
}
-func raw(t *testing.T, n string) []byte {
- raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
- assert.Nil(t, err)
- return raw
-}
-
// ----------------------------------------------------------------------------
func makeFactory() testFactory {
diff --git a/internal/model/table_test.go b/internal/model/table_test.go
index ec636b9e..54171017 100644
--- a/internal/model/table_test.go
+++ b/internal/model/table_test.go
@@ -14,7 +14,7 @@ import (
"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/model1"
"github.com/derailed/k9s/internal/watch"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -36,9 +36,9 @@ func TestTableRefresh(t *testing.T) {
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
assert.NoError(t, ta.Refresh(ctx))
data := ta.Peek()
- assert.Equal(t, 23, len(data.Header))
- assert.Equal(t, 1, len(data.RowEvents))
- assert.Equal(t, client.NamespaceAll, data.Namespace)
+ assert.Equal(t, 23, data.HeaderCount())
+ assert.Equal(t, 1, data.RowCount())
+ assert.Equal(t, client.NamespaceAll, data.GetNamespace())
assert.Equal(t, 1, l.count)
assert.Equal(t, 0, l.errs)
}
@@ -75,7 +75,7 @@ type tableListener struct {
count, errs int
}
-func (l *tableListener) TableDataChanged(*render.TableData) {
+func (l *tableListener) TableDataChanged(*model1.TableData) {
l.count++
}
diff --git a/internal/model/text.go b/internal/model/text.go
index 64e4d4f9..b50c8680 100644
--- a/internal/model/text.go
+++ b/internal/model/text.go
@@ -6,7 +6,7 @@ package model
import (
"strings"
- "github.com/derailed/k9s/internal/dao"
+ "github.com/derailed/k9s/internal"
"github.com/sahilm/fuzzy"
)
@@ -111,7 +111,7 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches {
if q == "" {
return nil
}
- if f, ok := dao.HasFuzzySelector(q); ok {
+ if f, ok := internal.IsFuzzySelector(q); ok {
return t.fuzzyFilter(strings.TrimSpace(f), lines)
}
return rxFilter(q, lines)
diff --git a/internal/model/types.go b/internal/model/types.go
index b8ec0b6c..e729b322 100644
--- a/internal/model/types.go
+++ b/internal/model/types.go
@@ -9,7 +9,7 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tview"
"github.com/sahilm/fuzzy"
"k8s.io/apimachinery/pkg/runtime"
@@ -100,21 +100,6 @@ type Filterer interface {
SetLabelFilter(map[string]string)
}
-// Renderer represents a resource renderer.
-type Renderer interface {
- // IsGeneric identifies a generic handler.
- IsGeneric() bool
-
- // Render converts raw resources to tabular data.
- Render(o interface{}, ns string, row *render.Row) error
-
- // Header returns the resource header.
- Header(ns string) render.Header
-
- // ColorerFunc returns a row colorer function.
- ColorerFunc() render.ColorerFunc
-}
-
// Cruder performs crud operations.
type Cruder interface {
// List returns a collection of resources.
@@ -149,6 +134,6 @@ type TreeRenderer interface {
// ResourceMeta represents model info about a resource.
type ResourceMeta struct {
DAO dao.Accessor
- Renderer Renderer
+ Renderer model1.Renderer
TreeRenderer TreeRenderer
}
diff --git a/internal/model/values.go b/internal/model/values.go
index 25870d5a..eafe098d 100644
--- a/internal/model/values.go
+++ b/internal/model/values.go
@@ -11,6 +11,7 @@ import (
"time"
backoff "github.com/cenkalti/backoff/v4"
+ "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/rs/zerolog/log"
@@ -113,7 +114,7 @@ func (v *Values) filter(q string, lines []string) fuzzy.Matches {
if q == "" {
return nil
}
- if f, ok := dao.HasFuzzySelector(q); ok {
+ if f, ok := internal.IsFuzzySelector(q); ok {
return v.fuzzyFilter(strings.TrimSpace(f), lines)
}
return rxFilter(q, lines)
diff --git a/internal/model/yaml.go b/internal/model/yaml.go
index 7e7dd1a2..e476e272 100644
--- a/internal/model/yaml.go
+++ b/internal/model/yaml.go
@@ -74,7 +74,7 @@ func (y *YAML) filter(q string, lines []string) fuzzy.Matches {
if q == "" {
return nil
}
- if f, ok := dao.HasFuzzySelector(q); ok {
+ if f, ok := internal.IsFuzzySelector(q); ok {
return y.fuzzyFilter(strings.TrimSpace(f), lines)
}
return rxFilter(q, lines)
diff --git a/internal/render/color.go b/internal/model1/color.go
similarity index 74%
rename from internal/render/color.go
rename to internal/model1/color.go
index 816c1e39..f570a040 100644
--- a/internal/render/color.go
+++ b/internal/model1/color.go
@@ -1,11 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
-package render
+package model1
-import (
- "github.com/derailed/tcell/v2"
-)
+import "github.com/derailed/tcell/v2"
var (
// ModColor row modified color.
@@ -33,12 +31,9 @@ var (
CompletedColor tcell.Color
)
-// ColorerFunc represents a resource row colorer.
-type ColorerFunc func(ns string, h Header, re RowEvent) tcell.Color
-
// DefaultColorer set the default table row colors.
-func DefaultColorer(ns string, h Header, re RowEvent) tcell.Color {
- if !Happy(ns, h, re.Row) {
+func DefaultColorer(ns string, h Header, re *RowEvent) tcell.Color {
+ if !IsValid(ns, h, re.Row) {
return ErrColor
}
diff --git a/internal/model1/color_test.go b/internal/model1/color_test.go
new file mode 100644
index 00000000..985bab95
--- /dev/null
+++ b/internal/model1/color_test.go
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1_test
+
+import (
+ "testing"
+
+ "github.com/derailed/k9s/internal/model1"
+ "github.com/derailed/tcell/v2"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDefaultColorer(t *testing.T) {
+ uu := map[string]struct {
+ re model1.RowEvent
+ e tcell.Color
+ }{
+ "add": {
+ model1.RowEvent{
+ Kind: model1.EventAdd,
+ },
+ model1.AddColor,
+ },
+ "update": {
+ model1.RowEvent{
+ Kind: model1.EventUpdate,
+ },
+ model1.ModColor,
+ },
+ "delete": {
+ model1.RowEvent{
+ Kind: model1.EventDelete,
+ },
+ model1.KillColor,
+ },
+ "no-change": {
+ model1.RowEvent{
+ Kind: model1.EventUnchanged,
+ },
+ model1.StdColor,
+ },
+ "invalid": {
+ model1.RowEvent{
+ Kind: model1.EventUnchanged,
+ Row: model1.Row{
+ Fields: model1.Fields{"", "", "blah"},
+ },
+ },
+ model1.ErrColor,
+ },
+ }
+
+ h := model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "VALID"},
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, model1.DefaultColorer("", h, &u.re))
+ })
+ }
+}
diff --git a/internal/render/delta.go b/internal/model1/delta.go
similarity index 97%
rename from internal/render/delta.go
rename to internal/model1/delta.go
index bdc95aa1..3d3e32dc 100644
--- a/internal/render/delta.go
+++ b/internal/model1/delta.go
@@ -1,11 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
-package render
+package model1
-import (
- "reflect"
-)
+import "reflect"
// DeltaRow represents a collection of row deltas between old and new row.
type DeltaRow []string
diff --git a/internal/model1/delta_test.go b/internal/model1/delta_test.go
new file mode 100644
index 00000000..a809660c
--- /dev/null
+++ b/internal/model1/delta_test.go
@@ -0,0 +1,266 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1_test
+
+import (
+ "testing"
+
+ "github.com/derailed/k9s/internal/model1"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDeltaLabelize(t *testing.T) {
+ uu := map[string]struct {
+ o model1.Row
+ n model1.Row
+ e model1.DeltaRow
+ }{
+ "same": {
+ o: model1.Row{
+ Fields: model1.Fields{"a", "b", "blee=fred,doh=zorg"},
+ },
+ n: model1.Row{
+ Fields: model1.Fields{"a", "b", "blee=fred1,doh=zorg"},
+ },
+ e: model1.DeltaRow{"", "", "fred", "zorg"},
+ },
+ }
+
+ hh := model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "C"},
+ }
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ d := model1.NewDeltaRow(u.o, u.n, hh)
+ d = d.Labelize([]int{0, 1}, 2)
+ assert.Equal(t, u.e, d)
+ })
+ }
+}
+
+func TestDeltaCustomize(t *testing.T) {
+ uu := map[string]struct {
+ r1, r2 model1.Row
+ cols []int
+ e model1.DeltaRow
+ }{
+ "same": {
+ r1: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ r2: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ cols: []int{0, 1, 2},
+ e: model1.DeltaRow{"", "", ""},
+ },
+ "empty": {
+ r1: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ r2: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ e: model1.DeltaRow{},
+ },
+ "diff-full": {
+ r1: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ r2: model1.Row{
+ Fields: model1.Fields{"a1", "b1", "c1"},
+ },
+ cols: []int{0, 1, 2},
+ e: model1.DeltaRow{"a", "b", "c"},
+ },
+ "diff-reverse": {
+ r1: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ r2: model1.Row{
+ Fields: model1.Fields{"a1", "b1", "c1"},
+ },
+ cols: []int{2, 1, 0},
+ e: model1.DeltaRow{"c", "b", "a"},
+ },
+ "diff-skip": {
+ r1: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ r2: model1.Row{
+ Fields: model1.Fields{"a1", "b1", "c1"},
+ },
+ cols: []int{2, 0},
+ e: model1.DeltaRow{"c", "a"},
+ },
+ "diff-missing": {
+ r1: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ r2: model1.Row{
+ Fields: model1.Fields{"a1", "b1", "c1"},
+ },
+ cols: []int{2, 10, 0},
+ e: model1.DeltaRow{"c", "", "a"},
+ },
+ "diff-negative": {
+ r1: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ r2: model1.Row{
+ Fields: model1.Fields{"a1", "b1", "c1"},
+ },
+ cols: []int{2, -1, 0},
+ e: model1.DeltaRow{"c", "", "a"},
+ },
+ }
+
+ hh := model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "C"},
+ }
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ d := model1.NewDeltaRow(u.r1, u.r2, hh)
+ out := make(model1.DeltaRow, len(u.cols))
+ d.Customize(u.cols, out)
+ assert.Equal(t, u.e, out)
+ })
+ }
+}
+
+func TestDeltaNew(t *testing.T) {
+ uu := map[string]struct {
+ o model1.Row
+ n model1.Row
+ blank bool
+ e model1.DeltaRow
+ }{
+ "same": {
+ o: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ n: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ blank: true,
+ e: model1.DeltaRow{"", "", ""},
+ },
+ "diff": {
+ o: model1.Row{
+ Fields: model1.Fields{"a1", "b", "c"},
+ },
+ n: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ e: model1.DeltaRow{"a1", "", ""},
+ },
+ "diff2": {
+ o: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ n: model1.Row{
+ Fields: model1.Fields{"a", "b1", "c"},
+ },
+ e: model1.DeltaRow{"", "b", ""},
+ },
+ "diffLast": {
+ o: model1.Row{
+ Fields: model1.Fields{"a", "b", "c"},
+ },
+ n: model1.Row{
+ Fields: model1.Fields{"a", "b", "c1"},
+ },
+ e: model1.DeltaRow{"", "", "c"},
+ },
+ }
+
+ hh := model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "C"},
+ }
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ d := model1.NewDeltaRow(u.o, u.n, hh)
+ assert.Equal(t, u.e, d)
+ assert.Equal(t, u.blank, d.IsBlank())
+ })
+ }
+}
+
+func TestDeltaBlank(t *testing.T) {
+ uu := map[string]struct {
+ r model1.DeltaRow
+ e bool
+ }{
+ "empty": {
+ r: model1.DeltaRow{},
+ e: true,
+ },
+ "blank": {
+ r: model1.DeltaRow{"", "", ""},
+ e: true,
+ },
+ "notblank": {
+ r: model1.DeltaRow{"", "", "z"},
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, u.r.IsBlank())
+ })
+ }
+}
+
+func TestDeltaDiff(t *testing.T) {
+ uu := map[string]struct {
+ d1, d2 model1.DeltaRow
+ ageCol int
+ e bool
+ }{
+ "empty": {
+ d1: model1.DeltaRow{"f1", "f2", "f3"},
+ ageCol: 2,
+ e: true,
+ },
+ "same": {
+ d1: model1.DeltaRow{"f1", "f2", "f3"},
+ d2: model1.DeltaRow{"f1", "f2", "f3"},
+ ageCol: -1,
+ },
+ "diff": {
+ d1: model1.DeltaRow{"f1", "f2", "f3"},
+ d2: model1.DeltaRow{"f1", "f2", "f13"},
+ ageCol: -1,
+ e: true,
+ },
+ "diff-age-first": {
+ d1: model1.DeltaRow{"f1", "f2", "f3"},
+ d2: model1.DeltaRow{"f1", "f2", "f13"},
+ ageCol: 0,
+ e: true,
+ },
+ "diff-age-last": {
+ d1: model1.DeltaRow{"f1", "f2", "f3"},
+ d2: model1.DeltaRow{"f1", "f2", "f13"},
+ ageCol: 2,
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, u.d1.Diff(u.d2, u.ageCol))
+ })
+ }
+}
diff --git a/internal/model1/fields.go b/internal/model1/fields.go
new file mode 100644
index 00000000..9242d54f
--- /dev/null
+++ b/internal/model1/fields.go
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1
+
+import "reflect"
+
+// Fields represents a collection of row fields.
+type Fields []string
+
+// Customize returns a subset of fields.
+func (f Fields) Customize(cols []int, out Fields) {
+ for i, c := range cols {
+ if c < 0 {
+ out[i] = NAValue
+ continue
+ }
+ if c < len(f) {
+ out[i] = f[c]
+ }
+ }
+}
+
+// Diff returns true if fields differ or false otherwise.
+func (f Fields) Diff(ff Fields, ageCol int) bool {
+ if ageCol < 0 {
+ return !reflect.DeepEqual(f[:len(f)-1], ff[:len(ff)-1])
+ }
+ if !reflect.DeepEqual(f[:ageCol], ff[:ageCol]) {
+ return true
+ }
+ return !reflect.DeepEqual(f[ageCol+1:], ff[ageCol+1:])
+}
+
+// Clone returns a copy of the fields.
+func (f Fields) Clone() Fields {
+ cp := make(Fields, len(f))
+ copy(cp, f)
+
+ return cp
+}
diff --git a/internal/render/header.go b/internal/model1/header.go
similarity index 82%
rename from internal/render/header.go
rename to internal/model1/header.go
index 5ac9bcd2..798f1d92 100644
--- a/internal/render/header.go
+++ b/internal/model1/header.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
-package render
+package model1
import (
"reflect"
@@ -34,18 +34,24 @@ func (h HeaderColumn) Clone() HeaderColumn {
// Header represents a table header.
type Header []HeaderColumn
+func (h Header) Clear() Header {
+ h = h[:0]
+
+ return h
+}
+
// Clone duplicates a header.
func (h Header) Clone() Header {
- header := make(Header, len(h))
- for i, c := range h {
- header[i] = c.Clone()
+ he := make(Header, 0, len(h))
+ for _, h := range h {
+ he = append(he, h.Clone())
}
- return header
+ return he
}
// Labelize returns a new Header based on labels.
-func (h Header) Labelize(cols []int, labelCol int, rr RowEvents) Header {
+func (h Header) Labelize(cols []int, labelCol int, rr *RowEvents) Header {
header := make(Header, 0, len(cols)+1)
for _, c := range cols {
header = append(header, h[c])
@@ -63,8 +69,8 @@ func (h Header) MapIndices(cols []string, wide bool) []int {
ii := make([]int, 0, len(cols))
cc := make(map[int]struct{}, len(cols))
for _, col := range cols {
- idx := h.IndexOf(col, true)
- if idx < 0 {
+ idx, ok := h.IndexOf(col, true)
+ if !ok {
log.Warn().Msgf("Column %q not found on resource", col)
}
ii, cc[idx] = append(ii, idx), struct{}{}
@@ -90,13 +96,10 @@ func (h Header) Customize(cols []string, wide bool) Header {
cc := make(Header, 0, len(h))
xx := make(map[int]struct{}, len(h))
for _, c := range cols {
- idx := h.IndexOf(c, true)
- if idx == -1 {
+ idx, ok := h.IndexOf(c, true)
+ if !ok {
log.Warn().Msgf("Column %s is not available on this resource", c)
- col := HeaderColumn{
- Name: c,
- }
- cc = append(cc, col)
+ cc = append(cc, HeaderColumn{Name: c})
continue
}
xx[idx] = struct{}{}
@@ -129,8 +132,8 @@ func (h Header) Diff(header Header) bool {
return !reflect.DeepEqual(h, header)
}
-// Columns return header as a collection of strings.
-func (h Header) Columns(wide bool) []string {
+// ColumnNames return header col names
+func (h Header) ColumnNames(wide bool) []string {
if len(h) == 0 {
return nil
}
@@ -147,7 +150,9 @@ func (h Header) Columns(wide bool) []string {
// HasAge returns true if table has an age column.
func (h Header) HasAge() bool {
- return h.IndexOf(ageCol, true) != -1
+ _, ok := h.IndexOf(ageCol, true)
+
+ return ok
}
// IsMetricsCol checks if given column index represents metrics.
@@ -177,22 +182,17 @@ func (h Header) IsCapacityCol(col int) bool {
return h[col].Capacity
}
-// ValidColIndex returns the valid col index or -1 if none.
-func (h Header) ValidColIndex() int {
- return h.IndexOf("VALID", true)
-}
-
// IndexOf returns the col index or -1 if none.
-func (h Header) IndexOf(colName string, includeWide bool) int {
+func (h Header) IndexOf(colName string, includeWide bool) (int, bool) {
for i, c := range h {
if c.Wide && !includeWide {
continue
}
if c.Name == colName {
- return i
+ return i, true
}
}
- return -1
+ return -1, false
}
// Dump for debugging.
diff --git a/internal/render/header_test.go b/internal/model1/header_test.go
similarity index 53%
rename from internal/render/header_test.go
rename to internal/model1/header_test.go
index 8c82a141..3d1d62f3 100644
--- a/internal/render/header_test.go
+++ b/internal/model1/header_test.go
@@ -1,18 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
-package render_test
+package model1_test
import (
"testing"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
"github.com/stretchr/testify/assert"
)
func TestHeaderMapIndices(t *testing.T) {
uu := map[string]struct {
- h1 render.Header
+ h1 model1.Header
cols []string
wide bool
e []int
@@ -50,15 +50,16 @@ func TestHeaderMapIndices(t *testing.T) {
func TestHeaderIndexOf(t *testing.T) {
uu := map[string]struct {
- h render.Header
- name string
- wide bool
- e int
+ h model1.Header
+ name string
+ wide, ok bool
+ e int
}{
"shown": {
h: makeHeader(),
name: "A",
e: 0,
+ ok: true,
},
"hidden": {
h: makeHeader(),
@@ -70,23 +71,26 @@ func TestHeaderIndexOf(t *testing.T) {
name: "B",
wide: true,
e: 1,
+ ok: true,
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.h.IndexOf(u.name, u.wide))
+ idx, ok := u.h.IndexOf(u.name, u.wide)
+ assert.Equal(t, u.ok, ok)
+ assert.Equal(t, u.e, idx)
})
}
}
func TestHeaderCustomize(t *testing.T) {
uu := map[string]struct {
- h render.Header
+ h model1.Header
cols []string
wide bool
- e render.Header
+ e model1.Header
}{
"default": {
h: makeHeader(),
@@ -98,58 +102,58 @@ func TestHeaderCustomize(t *testing.T) {
e: makeHeader(),
},
"reverse": {
- h: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "C"},
+ h: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B", Wide: true},
+ model1.HeaderColumn{Name: "C"},
},
cols: []string{"C", "A"},
- e: render.Header{
- render.HeaderColumn{Name: "C"},
- render.HeaderColumn{Name: "A"},
+ e: model1.Header{
+ model1.HeaderColumn{Name: "C"},
+ model1.HeaderColumn{Name: "A"},
},
},
"reverse-wide": {
- h: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "C"},
+ h: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B", Wide: true},
+ model1.HeaderColumn{Name: "C"},
},
cols: []string{"C", "A"},
wide: true,
- e: render.Header{
- render.HeaderColumn{Name: "C"},
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
+ e: model1.Header{
+ model1.HeaderColumn{Name: "C"},
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B", Wide: true},
},
},
"toggle-wide": {
- h: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "C"},
+ h: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B", Wide: true},
+ model1.HeaderColumn{Name: "C"},
},
cols: []string{"C", "B"},
wide: true,
- e: render.Header{
- render.HeaderColumn{Name: "C"},
- render.HeaderColumn{Name: "B", Wide: false},
- render.HeaderColumn{Name: "A", Wide: true},
+ e: model1.Header{
+ model1.HeaderColumn{Name: "C"},
+ model1.HeaderColumn{Name: "B", Wide: false},
+ model1.HeaderColumn{Name: "A", Wide: true},
},
},
"missing": {
- h: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "C"},
+ h: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B", Wide: true},
+ model1.HeaderColumn{Name: "C"},
},
cols: []string{"BLEE", "A"},
wide: true,
- e: render.Header{
- render.HeaderColumn{Name: "BLEE"},
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "C", Wide: true},
+ e: model1.Header{
+ model1.HeaderColumn{Name: "BLEE"},
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B", Wide: true},
+ model1.HeaderColumn{Name: "C", Wide: true},
},
},
}
@@ -164,7 +168,7 @@ func TestHeaderCustomize(t *testing.T) {
func TestHeaderDiff(t *testing.T) {
uu := map[string]struct {
- h1, h2 render.Header
+ h1, h2 model1.Header
e bool
}{
"same": {
@@ -177,37 +181,37 @@ func TestHeaderDiff(t *testing.T) {
e: true,
},
"differ-wide": {
- h1: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "C"},
+ h1: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B", Wide: true},
+ model1.HeaderColumn{Name: "C"},
},
- h2: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
+ h2: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "C"},
},
e: true,
},
"differ-order": {
- h1: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "C"},
+ h1: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B", Wide: true},
+ model1.HeaderColumn{Name: "C"},
},
- h2: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "C"},
- render.HeaderColumn{Name: "B", Wide: true},
+ h2: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "C"},
+ model1.HeaderColumn{Name: "B", Wide: true},
},
e: true,
},
"differ-name": {
- h1: render.Header{
- render.HeaderColumn{Name: "A"},
+ h1: model1.Header{
+ model1.HeaderColumn{Name: "A"},
},
- h2: render.Header{
- render.HeaderColumn{Name: "B"},
+ h2: model1.Header{
+ model1.HeaderColumn{Name: "B"},
},
e: true,
},
@@ -223,17 +227,17 @@ func TestHeaderDiff(t *testing.T) {
func TestHeaderHasAge(t *testing.T) {
uu := map[string]struct {
- h render.Header
+ h model1.Header
age, e bool
}{
"no-age": {
- h: render.Header{},
+ h: model1.Header{},
},
"age": {
- h: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "AGE", Time: true},
+ h: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
},
e: true,
age: true,
@@ -249,41 +253,14 @@ func TestHeaderHasAge(t *testing.T) {
}
}
-func TestHeaderValidColIndex(t *testing.T) {
- uu := map[string]struct {
- h render.Header
- e int
- }{
- "none": {
- h: render.Header{},
- e: -1,
- },
- "valid": {
- h: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "VALID", Wide: true},
- },
- e: 2,
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.h.ValidColIndex())
- })
- }
-}
-
func TestHeaderColumns(t *testing.T) {
uu := map[string]struct {
- h render.Header
+ h model1.Header
wide bool
e []string
}{
"empty": {
- h: render.Header{},
+ h: model1.Header{},
},
"regular": {
h: makeHeader(),
@@ -299,17 +276,17 @@ func TestHeaderColumns(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.h.Columns(u.wide))
+ assert.Equal(t, u.e, u.h.ColumnNames(u.wide))
})
}
}
func TestHeaderClone(t *testing.T) {
uu := map[string]struct {
- h render.Header
+ h model1.Header
}{
"empty": {
- h: render.Header{},
+ h: model1.Header{},
},
"full": {
h: makeHeader(),
@@ -332,10 +309,10 @@ func TestHeaderClone(t *testing.T) {
// ----------------------------------------------------------------------------
// Helpers...
-func makeHeader() render.Header {
- return render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "C"},
+func makeHeader() model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B", Wide: true},
+ model1.HeaderColumn{Name: "C"},
}
}
diff --git a/internal/model1/helpers.go b/internal/model1/helpers.go
new file mode 100644
index 00000000..fb0ac597
--- /dev/null
+++ b/internal/model1/helpers.go
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1
+
+import (
+ "fmt"
+ "math"
+ "sort"
+ "strings"
+
+ "github.com/fvbommel/sortorder"
+ "k8s.io/apimachinery/pkg/api/resource"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+)
+
+func Hydrate(ns string, oo []runtime.Object, rr Rows, re Renderer) error {
+ for i, o := range oo {
+ if err := re.Render(o, ns, &rr[i]); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func GenericHydrate(ns string, table *metav1.Table, rr Rows, re Renderer) error {
+ gr, ok := re.(Generic)
+ if !ok {
+ return fmt.Errorf("expecting generic renderer but got %T", re)
+ }
+ gr.SetTable(ns, table)
+ for i, row := range table.Rows {
+ if err := gr.Render(row, ns, &rr[i]); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// IsValid returns true if resource is valid, false otherwise.
+func IsValid(ns string, h Header, r Row) bool {
+ if len(r.Fields) == 0 {
+ return true
+ }
+ idx, ok := h.IndexOf("VALID", true)
+ if !ok || idx >= len(r.Fields) {
+ return true
+ }
+
+ return strings.TrimSpace(r.Fields[idx]) == ""
+}
+
+func sortLabels(m map[string]string) (keys, vals []string) {
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ vals = append(vals, m[k])
+ }
+
+ return
+}
+
+// Converts labels string to map.
+func labelize(labels string) map[string]string {
+ ll := strings.Split(labels, ",")
+ data := make(map[string]string, len(ll))
+ for _, l := range ll {
+ tokens := strings.Split(l, "=")
+ if len(tokens) == 2 {
+ data[tokens[0]] = tokens[1]
+ }
+ }
+
+ return data
+}
+
+func durationToSeconds(duration string) int64 {
+ if len(duration) == 0 {
+ return 0
+ }
+ if duration == NAValue {
+ return math.MaxInt64
+ }
+
+ num := make([]rune, 0, 5)
+ var n, m int64
+ for _, r := range duration {
+ switch r {
+ case 'y':
+ m = 365 * 24 * 60 * 60
+ case 'd':
+ m = 24 * 60 * 60
+ case 'h':
+ m = 60 * 60
+ case 'm':
+ m = 60
+ case 's':
+ m = 1
+ default:
+ num = append(num, r)
+ continue
+ }
+ n, num = n+runesToNum(num)*m, num[:0]
+ }
+
+ return n
+}
+
+func runesToNum(rr []rune) int64 {
+ var r int64
+ var m int64 = 1
+ for i := len(rr) - 1; i >= 0; i-- {
+ v := int64(rr[i] - '0')
+ r += v * m
+ m *= 10
+ }
+
+ return r
+}
+
+func capacityToNumber(capacity string) int64 {
+ quantity := resource.MustParse(capacity)
+ return quantity.Value()
+}
+
+// Less return true if c1 <= c2.
+func Less(isNumber, isDuration, isCapacity bool, id1, id2, v1, v2 string) bool {
+ var less bool
+ switch {
+ case isNumber:
+ less = lessNumber(v1, v2)
+ case isDuration:
+ less = lessDuration(v1, v2)
+ case isCapacity:
+ less = lessCapacity(v1, v2)
+ default:
+ less = sortorder.NaturalLess(v1, v2)
+ }
+ if v1 == v2 {
+ return sortorder.NaturalLess(id1, id2)
+ }
+
+ return less
+}
+
+func lessDuration(s1, s2 string) bool {
+ d1, d2 := durationToSeconds(s1), durationToSeconds(s2)
+ return d1 <= d2
+}
+
+func lessCapacity(s1, s2 string) bool {
+ c1, c2 := capacityToNumber(s1), capacityToNumber(s2)
+
+ return c1 <= c2
+}
+
+func lessNumber(s1, s2 string) bool {
+ v1, v2 := strings.Replace(s1, ",", "", -1), strings.Replace(s2, ",", "", -1)
+
+ return sortorder.NaturalLess(v1, v2)
+}
diff --git a/internal/model1/helpers_test.go b/internal/model1/helpers_test.go
new file mode 100644
index 00000000..3e6a0427
--- /dev/null
+++ b/internal/model1/helpers_test.go
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1
+
+import (
+ "math"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSortLabels(t *testing.T) {
+ uu := map[string]struct {
+ labels string
+ e [][]string
+ }{
+ "simple": {
+ labels: "a=b,c=d",
+ e: [][]string{
+ {"a", "c"},
+ {"b", "d"},
+ },
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ hh, vv := sortLabels(labelize(u.labels))
+ assert.Equal(t, u.e[0], hh)
+ assert.Equal(t, u.e[1], vv)
+ })
+ }
+}
+
+func TestLabelize(t *testing.T) {
+ uu := map[string]struct {
+ labels string
+ e map[string]string
+ }{
+ "simple": {
+ labels: "a=b,c=d",
+ e: map[string]string{"a": "b", "c": "d"},
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, labelize(u.labels))
+ })
+ }
+}
+
+func TestDurationToSecond(t *testing.T) {
+ uu := map[string]struct {
+ s string
+ e int64
+ }{
+ "seconds": {s: "22s", e: 22},
+ "minutes": {s: "22m", e: 1320},
+ "hours": {s: "12h", e: 43200},
+ "days": {s: "3d", e: 259200},
+ "day_hour": {s: "3d9h", e: 291600},
+ "day_hour_minute": {s: "2d22h3m", e: 252180},
+ "day_hour_minute_seconds": {s: "2d22h3m50s", e: 252230},
+ "year": {s: "3y", e: 94608000},
+ "year_day": {s: "1y2d", e: 31708800},
+ "n/a": {s: NAValue, e: math.MaxInt64},
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, durationToSeconds(u.s))
+ })
+ }
+}
+
+func BenchmarkDurationToSecond(b *testing.B) {
+ t := "2d22h3m50s"
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for n := 0; n < b.N; n++ {
+ durationToSeconds(t)
+ }
+}
diff --git a/internal/model1/row.go b/internal/model1/row.go
new file mode 100644
index 00000000..bda6a860
--- /dev/null
+++ b/internal/model1/row.go
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1
+
+// Row represents a collection of columns.
+type Row struct {
+ ID string
+ Fields Fields
+}
+
+// NewRow returns a new row with initialized fields.
+func NewRow(size int) Row {
+ return Row{Fields: make([]string, size)}
+}
+
+// Labelize returns a new row based on labels.
+func (r Row) Labelize(cols []int, labelCol int, labels []string) Row {
+ out := NewRow(len(cols) + len(labels))
+ for _, col := range cols {
+ out.Fields = append(out.Fields, r.Fields[col])
+ }
+ m := labelize(r.Fields[labelCol])
+ for _, label := range labels {
+ out.Fields = append(out.Fields, m[label])
+ }
+
+ return out
+}
+
+// Customize returns a row subset based on given col indices.
+func (r Row) Customize(cols []int) Row {
+ out := NewRow(len(cols))
+ r.Fields.Customize(cols, out.Fields)
+ out.ID = r.ID
+
+ return out
+}
+
+// Diff returns true if row differ or false otherwise.
+func (r Row) Diff(ro Row, ageCol int) bool {
+ if r.ID != ro.ID {
+ return true
+ }
+ return r.Fields.Diff(ro.Fields, ageCol)
+}
+
+// Clone copies a row.
+func (r Row) Clone() Row {
+ return Row{
+ ID: r.ID,
+ Fields: r.Fields.Clone(),
+ }
+}
+
+// Len returns the length of the row.
+func (r Row) Len() int {
+ return len(r.Fields)
+}
+
+// ----------------------------------------------------------------------------
+
+// RowSorter sorts rows.
+type RowSorter struct {
+ Rows Rows
+ Index int
+ IsNumber bool
+ IsDuration bool
+ IsCapacity bool
+ Asc bool
+}
+
+func (s RowSorter) Len() int {
+ return len(s.Rows)
+}
+
+func (s RowSorter) Swap(i, j int) {
+ s.Rows[i], s.Rows[j] = s.Rows[j], s.Rows[i]
+}
+
+func (s RowSorter) Less(i, j int) bool {
+ v1, v2 := s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index]
+ id1, id2 := s.Rows[i].ID, s.Rows[j].ID
+ less := Less(s.IsNumber, s.IsDuration, s.IsCapacity, id1, id2, v1, v2)
+ if s.Asc {
+ return less
+ }
+ return !less
+}
+
+// ----------------------------------------------------------------------------
+// Helpers...
diff --git a/internal/render/row_event.go b/internal/model1/row_event.go
similarity index 54%
rename from internal/render/row_event.go
rename to internal/model1/row_event.go
index 7248371d..628ddab0 100644
--- a/internal/render/row_event.go
+++ b/internal/model1/row_event.go
@@ -1,28 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
-package render
+package model1
import (
+ "fmt"
"sort"
)
-const (
- // EventUnchanged notifies listener resource has not changed.
- EventUnchanged ResEvent = 1 << iota
-
- // EventAdd notifies listener of a resource was added.
- EventAdd
-
- // EventUpdate notifies listener of a resource updated.
- EventUpdate
-
- // EventDelete notifies listener of a resource was deleted.
- EventDelete
-
- // EventClear the stack was reset.
- EventClear
-)
+type ReRangeFn func(int, RowEvent) bool
// ResEvent represents a resource event.
type ResEvent int
@@ -103,13 +89,58 @@ func (r RowEvent) Diff(re RowEvent, ageCol int) bool {
// ----------------------------------------------------------------------------
+type reIndex map[string]int
+
// RowEvents a collection of row events.
-type RowEvents []RowEvent
+type RowEvents struct {
+ events []RowEvent
+ index reIndex
+}
+
+func NewRowEvents(size int) *RowEvents {
+ return &RowEvents{
+ events: make([]RowEvent, 0, size),
+ index: make(reIndex, size),
+ }
+}
+
+func NewRowEventsWithEvts(ee ...RowEvent) *RowEvents {
+ re := NewRowEvents(len(ee))
+ for _, e := range ee {
+ re.Add(e)
+ }
+
+ return re
+}
+
+func (r *RowEvents) reindex() {
+ for i, e := range r.events {
+ r.index[e.Row.ID] = i
+ }
+}
+
+func (r *RowEvents) At(i int) (RowEvent, bool) {
+ if i < 0 || i > len(r.events) {
+ return RowEvent{}, false
+ }
+
+ return r.events[i], true
+}
+
+func (r *RowEvents) Set(i int, re RowEvent) {
+ r.events[i] = re
+ r.index[re.Row.ID] = i
+}
+
+func (r *RowEvents) Add(re RowEvent) {
+ r.events = append(r.events, re)
+ r.index[re.Row.ID] = len(r.events) - 1
+}
// ExtractHeaderLabels extract header labels.
-func (r RowEvents) ExtractHeaderLabels(labelCol int) []string {
+func (r *RowEvents) ExtractHeaderLabels(labelCol int) []string {
ll := make([]string, 0, 10)
- for _, re := range r {
+ for _, re := range r.events {
ll = append(ll, re.ExtractHeaderLabels(labelCol)...)
}
@@ -117,32 +148,32 @@ func (r RowEvents) ExtractHeaderLabels(labelCol int) []string {
}
// Labelize converts labels into a row event.
-func (r RowEvents) Labelize(cols []int, labelCol int, labels []string) RowEvents {
- out := make(RowEvents, 0, len(r))
- for _, re := range r {
+func (r *RowEvents) Labelize(cols []int, labelCol int, labels []string) *RowEvents {
+ out := make([]RowEvent, 0, len(r.events))
+ for _, re := range r.events {
out = append(out, re.Labelize(cols, labelCol, labels))
}
- return out
+ return NewRowEventsWithEvts(out...)
}
// Customize returns custom row events based on columns layout.
-func (r RowEvents) Customize(cols []int) RowEvents {
- ee := make(RowEvents, 0, len(cols))
- for _, re := range r {
+func (r *RowEvents) Customize(cols []int) *RowEvents {
+ ee := make([]RowEvent, 0, len(cols))
+ for _, re := range r.events {
ee = append(ee, re.Customize(cols))
}
- return ee
+
+ return NewRowEventsWithEvts(ee...)
}
// Diff returns true if the event changed.
-func (r RowEvents) Diff(re RowEvents, ageCol int) bool {
- if len(r) != len(re) {
+func (r *RowEvents) Diff(re *RowEvents, ageCol int) bool {
+ if len(r.events) != len(re.events) {
return true
}
-
- for i := range r {
- if r[i].Diff(re[i], ageCol) {
+ for i := range r.events {
+ if r.events[i].Diff(re.events[i], ageCol) {
return true
}
}
@@ -150,53 +181,80 @@ func (r RowEvents) Diff(re RowEvents, ageCol int) bool {
return false
}
-// Clone returns a rowevents deep copy.
-func (r RowEvents) Clone() RowEvents {
- res := make(RowEvents, len(r))
- for i, re := range r {
- res[i] = re.Clone()
+// Clone returns a deep copy.
+func (r *RowEvents) Clone() *RowEvents {
+ re := make([]RowEvent, 0, len(r.events))
+ for _, e := range r.events {
+ re = append(re, e.Clone())
}
- return res
+ return NewRowEventsWithEvts(re...)
}
// Upsert add or update a row if it exists.
-func (r RowEvents) Upsert(re RowEvent) RowEvents {
+func (r *RowEvents) Upsert(re RowEvent) {
if idx, ok := r.FindIndex(re.Row.ID); ok {
- r[idx] = re
+ r.events[idx] = re
} else {
- r = append(r, re)
+ r.Add(re)
}
- return r
}
// Delete removes an element by id.
-func (r RowEvents) Delete(id string) RowEvents {
- victim, ok := r.FindIndex(id)
+func (r *RowEvents) Delete(fqn string) error {
+ victim, ok := r.FindIndex(fqn)
if !ok {
- return r
+ return fmt.Errorf("unable to delete row with fqn: %q", fqn)
}
- return append(r[0:victim], r[victim+1:]...)
+ r.events = append(r.events[0:victim], r.events[victim+1:]...)
+ delete(r.index, fqn)
+ r.reindex()
+
+ return nil
+}
+
+func (r *RowEvents) Len() int {
+ return len(r.events)
+}
+
+func (r *RowEvents) Empty() bool {
+ return len(r.events) == 0
}
// Clear delete all row events.
-func (r RowEvents) Clear() RowEvents {
- return RowEvents{}
+func (r *RowEvents) Clear() {
+ r.events = r.events[:0]
+ for k := range r.index {
+ delete(r.index, k)
+ }
+}
+
+func (r *RowEvents) Range(f ReRangeFn) {
+ for i, e := range r.events {
+ if !f(i, e) {
+ return
+ }
+ }
+}
+
+func (r *RowEvents) Get(id string) (RowEvent, bool) {
+ i, ok := r.index[id]
+ if !ok {
+ return RowEvent{}, false
+ }
+
+ return r.At(i)
}
// FindIndex locates a row index by id. Returns false is not found.
-func (r RowEvents) FindIndex(id string) (int, bool) {
- for i, re := range r {
- if re.Row.ID == id {
- return i, true
- }
- }
+func (r *RowEvents) FindIndex(id string) (int, bool) {
+ i, ok := r.index[id]
- return 0, false
+ return i, ok
}
// Sort rows based on column index and order.
-func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, asc bool) {
+func (r *RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, asc bool) {
if sortCol == -1 {
return
}
@@ -211,13 +269,14 @@ func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity,
IsCapacity: isCapacity,
}
sort.Sort(t)
+ r.reindex()
}
// ----------------------------------------------------------------------------
// RowEventSorter sorts row events by a given colon.
type RowEventSorter struct {
- Events RowEvents
+ Events *RowEvents
Index int
NS string
IsNumber bool
@@ -227,16 +286,16 @@ type RowEventSorter struct {
}
func (r RowEventSorter) Len() int {
- return len(r.Events)
+ return len(r.Events.events)
}
func (r RowEventSorter) Swap(i, j int) {
- r.Events[i], r.Events[j] = r.Events[j], r.Events[i]
+ r.Events.events[i], r.Events.events[j] = r.Events.events[j], r.Events.events[i]
}
func (r RowEventSorter) Less(i, j int) bool {
- f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields
- id1, id2 := r.Events[i].Row.ID, r.Events[j].Row.ID
+ f1, f2 := r.Events.events[i].Row.Fields, r.Events.events[j].Row.Fields
+ id1, id2 := r.Events.events[i].Row.ID, r.Events.events[j].Row.ID
less := Less(r.IsNumber, r.IsDuration, r.IsCapacity, id1, id2, f1[r.Index], f2[r.Index])
if r.Asc {
return less
diff --git a/internal/model1/row_event_test.go b/internal/model1/row_event_test.go
new file mode 100644
index 00000000..9ca93229
--- /dev/null
+++ b/internal/model1/row_event_test.go
@@ -0,0 +1,543 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/derailed/k9s/internal/model1"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRowEventCustomize(t *testing.T) {
+ uu := map[string]struct {
+ re1, e model1.RowEvent
+ cols []int
+ }{
+ "empty": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ e: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{}},
+ },
+ },
+ "full": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ e: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ cols: []int{0, 1, 2},
+ },
+ "deltas": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ Deltas: model1.DeltaRow{"a", "b", "c"},
+ },
+ e: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ Deltas: model1.DeltaRow{"a", "b", "c"},
+ },
+ cols: []int{0, 1, 2},
+ },
+ "deltas-skip": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ Deltas: model1.DeltaRow{"a", "b", "c"},
+ },
+ e: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "1"}},
+ Deltas: model1.DeltaRow{"c", "a"},
+ },
+ cols: []int{2, 0},
+ },
+ "reverse": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ e: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "2", "1"}},
+ },
+ cols: []int{2, 1, 0},
+ },
+ "skip": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ e: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "1"}},
+ },
+ cols: []int{2, 0},
+ },
+ "miss": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ e: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "", "1"}},
+ },
+ cols: []int{2, 10, 0},
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, u.re1.Customize(u.cols))
+ })
+ }
+}
+
+func TestRowEventDiff(t *testing.T) {
+ uu := map[string]struct {
+ re1, re2 model1.RowEvent
+ e bool
+ }{
+ "same": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ re2: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ },
+ "diff-kind": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ re2: model1.RowEvent{
+ Kind: model1.EventDelete,
+ Row: model1.Row{ID: "B", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ e: true,
+ },
+ "diff-delta": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ Deltas: model1.DeltaRow{"1", "2", "3"},
+ },
+ re2: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ Deltas: model1.DeltaRow{"10", "2", "3"},
+ },
+ e: true,
+ },
+ "diff-id": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ re2: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "B", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ e: true,
+ },
+ "diff-field": {
+ re1: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}},
+ },
+ re2: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{ID: "A", Fields: model1.Fields{"10", "2", "3"}},
+ },
+ e: true,
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, u.re1.Diff(u.re2, -1))
+ })
+ }
+}
+
+func TestRowEventsDiff(t *testing.T) {
+ uu := map[string]struct {
+ re1, re2 *model1.RowEvents
+ ageCol int
+ e bool
+ }{
+ "same": {
+ re1: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ re2: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ ageCol: -1,
+ },
+ "diff-len": {
+ re1: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ re2: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ ageCol: -1,
+ e: true,
+ },
+ "diff-id": {
+ re1: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ re2: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "D", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ ageCol: -1,
+ e: true,
+ },
+ "diff-order": {
+ re1: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ re2: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ ageCol: -1,
+ e: true,
+ },
+ "diff-withAge": {
+ re1: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ re2: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "13"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ ageCol: 1,
+ e: true,
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, u.re1.Diff(u.re2, u.ageCol))
+ })
+ }
+}
+
+func TestRowEventsUpsert(t *testing.T) {
+ uu := map[string]struct {
+ ee, e *model1.RowEvents
+ re model1.RowEvent
+ }{
+ "add": {
+ ee: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ re: model1.RowEvent{
+ Row: model1.Row{ID: "D", Fields: model1.Fields{"f1", "f2", "f3"}},
+ },
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "D", Fields: model1.Fields{"f1", "f2", "f3"}}},
+ ),
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ u.ee.Upsert(u.re)
+ assert.Equal(t, u.e, u.ee)
+ })
+ }
+}
+
+func TestRowEventsCustomize(t *testing.T) {
+ uu := map[string]struct {
+ re, e *model1.RowEvents
+ cols []int
+ }{
+ "same": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ cols: []int{0, 1, 2},
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ },
+ "reverse": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ cols: []int{2, 1, 0},
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "2", "1"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"3", "2", "0"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"3", "2", "10"}}},
+ ),
+ },
+ "skip": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ cols: []int{1, 0},
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"2", "1"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"2", "0"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"2", "10"}}},
+ ),
+ },
+ "missing": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ cols: []int{1, 0, 4},
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"2", "1", ""}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"2", "0", ""}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"2", "10", ""}}},
+ ),
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, u.re.Customize(u.cols))
+ })
+ }
+}
+
+func TestRowEventsDelete(t *testing.T) {
+ uu := map[string]struct {
+ re, e *model1.RowEvents
+ id string
+ }{
+ "first": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ id: "A",
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ },
+ "middle": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ id: "B",
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ },
+ "last": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ id: "C",
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ ),
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.NoError(t, u.re.Delete(u.id))
+ assert.Equal(t, u.e, u.re)
+ })
+ }
+}
+
+func TestRowEventsSort(t *testing.T) {
+ uu := map[string]struct {
+ re, e *model1.RowEvents
+ col int
+ duration, num, asc bool
+ capacity bool
+ }{
+ "age_time": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", testTime().String()}}},
+ ),
+ col: 2,
+ asc: true,
+ duration: true,
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", testTime().String()}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}},
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}},
+ ),
+ },
+ "col0": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ col: 0,
+ asc: true,
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}},
+ ),
+ },
+ "id_preserve": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3"}}},
+ ),
+ col: 1,
+ asc: true,
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3"}}},
+ ),
+ },
+ "capacity": {
+ re: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3", "1Gi"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3", "1.1G"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3", "0.5Ti"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3", "12e6"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3", "1234"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3", "0.1Ei"}}},
+ ),
+ col: 3,
+ asc: true,
+ capacity: true,
+ e: model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3", "1234"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3", "12e6"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3", "1Gi"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3", "1.1G"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3", "0.5Ti"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3", "0.1Ei"}}},
+ ),
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ u.re.Sort("", u.col, u.duration, u.num, u.capacity, u.asc)
+ assert.Equal(t, u.e, u.re)
+ })
+ }
+}
+
+func TestRowEventsClone(t *testing.T) {
+ uu := map[string]struct {
+ r *model1.RowEvents
+ }{
+ "empty": {
+ r: model1.NewRowEventsWithEvts(),
+ },
+ "full": {
+ r: makeRowEvents(),
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ c := u.r.Clone()
+ assert.Equal(t, u.r.Len(), c.Len())
+ if !u.r.Empty() {
+ r, ok := u.r.At(0)
+ assert.True(t, ok)
+ r.Row.Fields[0] = "blee"
+ cr, ok := c.At(0)
+ assert.True(t, ok)
+ assert.Equal(t, "A", cr.Row.Fields[0])
+ }
+ })
+ }
+}
+
+// Helpers...
+
+func makeRowEvents() *model1.RowEvents {
+ return model1.NewRowEventsWithEvts(
+ model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3"}}},
+ model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3"}}},
+ )
+}
diff --git a/internal/render/row_test.go b/internal/model1/row_test.go
similarity index 78%
rename from internal/render/row_test.go
rename to internal/model1/row_test.go
index f1d5a5bc..d2060831 100644
--- a/internal/render/row_test.go
+++ b/internal/model1/row_test.go
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
-package render_test
+package model1_test
import (
"fmt"
@@ -9,12 +9,12 @@ import (
"testing"
"time"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
"github.com/stretchr/testify/assert"
)
func BenchmarkRowCustomize(b *testing.B) {
- row := render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}}
+ row := model1.Row{ID: "fred", Fields: model1.Fields{"f1", "f2", "f3"}}
cols := []int{0, 1, 2}
b.ReportAllocs()
b.ResetTimer()
@@ -25,36 +25,36 @@ func BenchmarkRowCustomize(b *testing.B) {
func TestFieldCustomize(t *testing.T) {
uu := map[string]struct {
- fields render.Fields
+ fields model1.Fields
cols []int
- e render.Fields
+ e model1.Fields
}{
"empty": {
- fields: render.Fields{},
+ fields: model1.Fields{},
cols: []int{0, 1, 2},
- e: render.Fields{"", "", ""},
+ e: model1.Fields{"", "", ""},
},
"no-cols": {
- fields: render.Fields{"f1", "f2", "f3"},
+ fields: model1.Fields{"f1", "f2", "f3"},
cols: []int{},
- e: render.Fields{},
+ e: model1.Fields{},
},
"reverse": {
- fields: render.Fields{"f1", "f2", "f3"},
+ fields: model1.Fields{"f1", "f2", "f3"},
cols: []int{1, 0},
- e: render.Fields{"f2", "f1"},
+ e: model1.Fields{"f2", "f1"},
},
"missing": {
- fields: render.Fields{"f1", "f2", "f3"},
+ fields: model1.Fields{"f1", "f2", "f3"},
cols: []int{10, 0},
- e: render.Fields{"", "f1"},
+ e: model1.Fields{"", "f1"},
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
- ff := make(render.Fields, len(u.cols))
+ ff := make(model1.Fields, len(u.cols))
u.fields.Customize(u.cols, ff)
assert.Equal(t, u.e, ff)
})
@@ -62,7 +62,7 @@ func TestFieldCustomize(t *testing.T) {
}
func TestFieldClone(t *testing.T) {
- f := render.Fields{"a", "b", "c"}
+ f := model1.Fields{"a", "b", "c"}
f1 := f.Clone()
assert.True(t, reflect.DeepEqual(f, f1))
@@ -71,24 +71,24 @@ func TestFieldClone(t *testing.T) {
func TestRowlabelize(t *testing.T) {
uu := map[string]struct {
- row render.Row
+ row model1.Row
cols []int
- e render.Row
+ e model1.Row
}{
"empty": {
- row: render.Row{},
+ row: model1.Row{},
cols: []int{0, 1, 2},
- e: render.Row{ID: "", Fields: render.Fields{"", "", ""}},
+ e: model1.Row{ID: "", Fields: model1.Fields{"", "", ""}},
},
"no-cols-no-data": {
- row: render.Row{},
+ row: model1.Row{},
cols: []int{},
- e: render.Row{ID: "", Fields: render.Fields{}},
+ e: model1.Row{ID: "", Fields: model1.Fields{}},
},
"no-cols-data": {
- row: render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}},
+ row: model1.Row{ID: "fred", Fields: model1.Fields{"f1", "f2", "f3"}},
cols: []int{},
- e: render.Row{ID: "fred", Fields: render.Fields{}},
+ e: model1.Row{ID: "fred", Fields: model1.Fields{}},
},
}
@@ -103,24 +103,24 @@ func TestRowlabelize(t *testing.T) {
func TestRowCustomize(t *testing.T) {
uu := map[string]struct {
- row render.Row
+ row model1.Row
cols []int
- e render.Row
+ e model1.Row
}{
"empty": {
- row: render.Row{},
+ row: model1.Row{},
cols: []int{0, 1, 2},
- e: render.Row{ID: "", Fields: render.Fields{"", "", ""}},
+ e: model1.Row{ID: "", Fields: model1.Fields{"", "", ""}},
},
"no-cols-no-data": {
- row: render.Row{},
+ row: model1.Row{},
cols: []int{},
- e: render.Row{ID: "", Fields: render.Fields{}},
+ e: model1.Row{ID: "", Fields: model1.Fields{}},
},
"no-cols-data": {
- row: render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}},
+ row: model1.Row{ID: "fred", Fields: model1.Fields{"f1", "f2", "f3"}},
cols: []int{},
- e: render.Row{ID: "fred", Fields: render.Fields{}},
+ e: model1.Row{ID: "fred", Fields: model1.Fields{}},
},
}
@@ -135,49 +135,49 @@ func TestRowCustomize(t *testing.T) {
func TestRowsDelete(t *testing.T) {
uu := map[string]struct {
- rows render.Rows
+ rows model1.Rows
id string
- e render.Rows
+ e model1.Rows
}{
"first": {
- rows: render.Rows{
+ rows: model1.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
},
id: "a",
- e: render.Rows{
+ e: model1.Rows{
{ID: "b", Fields: []string{"albert", "blee"}},
},
},
"last": {
- rows: render.Rows{
+ rows: model1.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
},
id: "b",
- e: render.Rows{
+ e: model1.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
},
},
"middle": {
- rows: render.Rows{
+ rows: model1.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
{ID: "c", Fields: []string{"fred", "zorg"}},
},
id: "b",
- e: render.Rows{
+ e: model1.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "c", Fields: []string{"fred", "zorg"}},
},
},
"missing": {
- rows: render.Rows{
+ rows: model1.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
},
id: "zorg",
- e: render.Rows{
+ e: model1.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
},
@@ -195,29 +195,29 @@ func TestRowsDelete(t *testing.T) {
func TestRowsUpsert(t *testing.T) {
uu := map[string]struct {
- rows render.Rows
- row render.Row
- e render.Rows
+ rows model1.Rows
+ row model1.Row
+ e model1.Rows
}{
"add": {
- rows: render.Rows{
+ rows: model1.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
},
- row: render.Row{ID: "c", Fields: []string{"f1", "f2"}},
- e: render.Rows{
+ row: model1.Row{ID: "c", Fields: []string{"f1", "f2"}},
+ e: model1.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
{ID: "c", Fields: []string{"f1", "f2"}},
},
},
"update": {
- rows: render.Rows{
+ rows: model1.Rows{
{ID: "a", Fields: []string{"blee", "duh"}},
{ID: "b", Fields: []string{"albert", "blee"}},
},
- row: render.Row{ID: "a", Fields: []string{"f1", "f2"}},
- e: render.Rows{
+ row: model1.Row{ID: "a", Fields: []string{"f1", "f2"}},
+ e: model1.Rows{
{ID: "a", Fields: []string{"f1", "f2"}},
{ID: "b", Fields: []string{"albert", "blee"}},
},
@@ -235,69 +235,69 @@ func TestRowsUpsert(t *testing.T) {
func TestRowsSortText(t *testing.T) {
uu := map[string]struct {
- rows render.Rows
+ rows model1.Rows
col int
asc, num bool
- e render.Rows
+ e model1.Rows
}{
"plainAsc": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{"blee", "duh"}},
{Fields: []string{"albert", "blee"}},
},
col: 0,
asc: true,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{"albert", "blee"}},
{Fields: []string{"blee", "duh"}},
},
},
"plainDesc": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{"blee", "duh"}},
{Fields: []string{"albert", "blee"}},
},
col: 0,
asc: false,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{"blee", "duh"}},
{Fields: []string{"albert", "blee"}},
},
},
"numericAsc": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{"10", "duh"}},
{Fields: []string{"1", "blee"}},
},
col: 0,
num: true,
asc: true,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{"1", "blee"}},
{Fields: []string{"10", "duh"}},
},
},
"numericDesc": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{"10", "duh"}},
{Fields: []string{"1", "blee"}},
},
col: 0,
num: true,
asc: false,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{"10", "duh"}},
{Fields: []string{"1", "blee"}},
},
},
"composite": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{"blee-duh", "duh"}},
{Fields: []string{"blee", "blee"}},
},
col: 0,
asc: true,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{"blee", "blee"}},
{Fields: []string{"blee-duh", "duh"}},
},
@@ -315,54 +315,54 @@ func TestRowsSortText(t *testing.T) {
func TestRowsSortDuration(t *testing.T) {
uu := map[string]struct {
- rows render.Rows
+ rows model1.Rows
col int
asc bool
- e render.Rows
+ e model1.Rows
}{
"fred": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{"2m24s", "blee"}},
{Fields: []string{"2m12s", "duh"}},
},
col: 0,
asc: true,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{"2m12s", "duh"}},
{Fields: []string{"2m24s", "blee"}},
},
},
"years": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{testTime().Add(-365 * 24 * time.Hour).String(), "blee"}},
{Fields: []string{testTime().String(), "duh"}},
},
col: 0,
asc: true,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{testTime().String(), "duh"}},
{Fields: []string{testTime().Add(-365 * 24 * time.Hour).String(), "blee"}},
},
},
"durationAsc": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}},
{Fields: []string{testTime().String(), "blee"}},
},
col: 0,
asc: true,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{testTime().String(), "blee"}},
{Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}},
},
},
"durationDesc": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}},
{Fields: []string{testTime().String(), "blee"}},
},
col: 0,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}},
{Fields: []string{testTime().String(), "blee"}},
},
@@ -380,31 +380,31 @@ func TestRowsSortDuration(t *testing.T) {
func TestRowsSortMetrics(t *testing.T) {
uu := map[string]struct {
- rows render.Rows
+ rows model1.Rows
col int
asc bool
- e render.Rows
+ e model1.Rows
}{
"metricAsc": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{"10m", "duh"}},
{Fields: []string{"1m", "blee"}},
},
col: 0,
asc: true,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{"1m", "blee"}},
{Fields: []string{"10m", "duh"}},
},
},
"metricDesc": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{"10000m", "1000Mi"}},
{Fields: []string{"1m", "50Mi"}},
},
col: 1,
asc: false,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{"10000m", "1000Mi"}},
{Fields: []string{"1m", "50Mi"}},
},
@@ -422,31 +422,31 @@ func TestRowsSortMetrics(t *testing.T) {
func TestRowsSortCapacity(t *testing.T) {
uu := map[string]struct {
- rows render.Rows
+ rows model1.Rows
col int
asc bool
- e render.Rows
+ e model1.Rows
}{
"capacityAsc": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{"10Gi", "duh"}},
{Fields: []string{"10G", "blee"}},
},
col: 0,
asc: true,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{"10G", "blee"}},
{Fields: []string{"10Gi", "duh"}},
},
},
"capacityDesc": {
- rows: render.Rows{
+ rows: model1.Rows{
{Fields: []string{"10000m", "1000Mi"}},
{Fields: []string{"1m", "50Mi"}},
},
col: 1,
asc: false,
- e: render.Rows{
+ e: model1.Rows{
{Fields: []string{"10000m", "1000Mi"}},
{Fields: []string{"1m", "50Mi"}},
},
@@ -514,7 +514,7 @@ func TestLess(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, render.Less(u.isNumber, u.isDuration, u.isCapacity, u.id1, u.id2, u.v1, u.v2))
+ assert.Equal(t, u.e, model1.Less(u.isNumber, u.isDuration, u.isCapacity, u.id1, u.id2, u.v1, u.v2))
})
}
}
diff --git a/internal/model1/rows.go b/internal/model1/rows.go
new file mode 100644
index 00000000..4085cb9b
--- /dev/null
+++ b/internal/model1/rows.go
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1
+
+import "sort"
+
+// Rows represents a collection of rows.
+type Rows []Row
+
+// Delete removes an element by id.
+func (rr Rows) Delete(id string) Rows {
+ idx, ok := rr.Find(id)
+ if !ok {
+ return rr
+ }
+
+ if idx == 0 {
+ return rr[1:]
+ }
+ if idx+1 == len(rr) {
+ return rr[:len(rr)-1]
+ }
+
+ return append(rr[:idx], rr[idx+1:]...)
+}
+
+// Upsert adds a new item.
+func (rr Rows) Upsert(r Row) Rows {
+ idx, ok := rr.Find(r.ID)
+ if !ok {
+ return append(rr, r)
+ }
+ rr[idx] = r
+
+ return rr
+}
+
+// Find locates a row by id. Returns false is not found.
+func (rr Rows) Find(id string) (int, bool) {
+ for i, r := range rr {
+ if r.ID == id {
+ return i, true
+ }
+ }
+
+ return 0, false
+}
+
+// Sort rows based on column index and order.
+func (rr Rows) Sort(col int, asc, isNum, isDur, isCapacity bool) {
+ t := RowSorter{
+ Rows: rr,
+ Index: col,
+ IsNumber: isNum,
+ IsDuration: isDur,
+ IsCapacity: isCapacity,
+ Asc: asc,
+ }
+ sort.Sort(t)
+}
diff --git a/internal/model1/table_data.go b/internal/model1/table_data.go
new file mode 100644
index 00000000..2b8cd096
--- /dev/null
+++ b/internal/model1/table_data.go
@@ -0,0 +1,496 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "regexp"
+ "strings"
+ "sync"
+
+ "github.com/derailed/k9s/internal"
+ "github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/config"
+ "github.com/rs/zerolog/log"
+ "github.com/sahilm/fuzzy"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+)
+
+type (
+ // SortFn represent a function that can sort columnar data.
+ SortFn func(rows Rows, sortCol SortColumn)
+
+ // SortColumn represents a sortable column.
+ SortColumn struct {
+ Name string
+ ASC bool
+ }
+)
+
+const spacer = " "
+
+type FilterOpts struct {
+ Toast bool
+ Filter string
+ Invert bool
+}
+
+// TableData tracks a K8s resource for tabular display.
+type TableData struct {
+ header Header
+ rowEvents *RowEvents
+ namespace string
+ gvr client.GVR
+ mx sync.RWMutex
+}
+
+// NewTableData returns a new table.
+func NewTableData(gvr client.GVR) *TableData {
+ return &TableData{
+ gvr: gvr,
+ rowEvents: NewRowEvents(10),
+ }
+}
+
+func NewTableDataFull(gvr client.GVR, ns string, h Header, re *RowEvents) *TableData {
+ t := NewTableDataWithRows(gvr, h, re)
+ t.namespace = ns
+
+ return t
+}
+
+func NewTableDataWithRows(gvr client.GVR, h Header, re *RowEvents) *TableData {
+ t := NewTableData(gvr)
+ t.header, t.rowEvents = h, re
+
+ return t
+}
+
+func NewTableDataFromTable(td *TableData) *TableData {
+ t := NewTableData(td.gvr)
+ t.header = td.header
+ t.rowEvents = td.rowEvents
+ t.namespace = td.namespace
+
+ return t
+}
+
+func (t *TableData) AddRow(re RowEvent) {
+ t.rowEvents.Add(re)
+}
+
+func (t *TableData) SetRow(idx int, re RowEvent) {
+ t.rowEvents.Set(idx, re)
+}
+
+func (t *TableData) FindRow(id string) (RowEvent, bool) {
+ return t.rowEvents.Get(id)
+}
+
+func (t *TableData) RowAt(idx int) (RowEvent, bool) {
+ return t.rowEvents.At(idx)
+}
+
+func (t *TableData) RowsRange(f ReRangeFn) {
+ t.rowEvents.Range(f)
+}
+
+func (t *TableData) Sort(sc SortColumn) {
+ col, idx := t.HeadCol(sc.Name, false)
+ if idx < 0 {
+ return
+ }
+ t.rowEvents.Sort(
+ t.GetNamespace(),
+ idx,
+ col.Time,
+ col.MX,
+ col.Capacity,
+ sc.ASC,
+ )
+}
+
+func (t *TableData) Header() Header {
+ return t.header
+}
+
+// HeaderCount returns the number of header cols.
+func (t *TableData) HeaderCount() int {
+ t.mx.RLock()
+ defer t.mx.RUnlock()
+
+ return len(t.header)
+}
+
+func (t *TableData) HeadCol(n string, w bool) (HeaderColumn, int) {
+ idx, ok := t.header.IndexOf(n, w)
+ if !ok {
+ return HeaderColumn{}, -1
+ }
+
+ return t.header[idx], idx
+}
+
+func (t *TableData) Filter(f FilterOpts) *TableData {
+ td := NewTableDataFromTable(t)
+
+ if f.Toast {
+ td.rowEvents = t.filterToast()
+ }
+ if f.Filter == "" || internal.IsLabelSelector(f.Filter) {
+ return td
+ }
+ if f, ok := internal.IsFuzzySelector(f.Filter); ok {
+ td.rowEvents = t.fuzzyFilter(f)
+ return td
+ }
+ rr, err := t.rxFilter(f.Filter, internal.IsInverseSelector(f.Filter))
+ if err == nil {
+ td.rowEvents = rr
+ } else {
+ log.Error().Err(err).Msg("rx filter failed")
+ }
+
+ return td
+}
+
+func (t *TableData) rxFilter(q string, inverse bool) (*RowEvents, error) {
+ if inverse {
+ q = q[1:]
+ }
+ rx, err := regexp.Compile(`(?i)(` + q + `)`)
+ if err != nil {
+ return nil, fmt.Errorf("invalid rx filter %q: %w", q, err)
+ }
+
+ ageIndex, ok := t.header.IndexOf("AGE", true)
+
+ rr := NewRowEvents(t.RowCount() / 2)
+ t.rowEvents.Range(func(_ int, re RowEvent) bool {
+ ff := re.Row.Fields
+ if ok && ageIndex+1 <= len(ff) {
+ ff = append(ff[0:ageIndex], ff[ageIndex+1:]...)
+ }
+ fields := strings.Join(ff, spacer)
+ if (inverse && !rx.MatchString(fields)) ||
+ ((!inverse) && rx.MatchString(fields)) {
+ rr.Add(re)
+ }
+ return true
+ })
+
+ return rr, nil
+}
+
+func (t *TableData) fuzzyFilter(q string) *RowEvents {
+ q = strings.TrimSpace(q)
+ ss := make([]string, 0, t.RowCount()/2)
+ t.rowEvents.Range(func(_ int, re RowEvent) bool {
+ ss = append(ss, re.Row.ID)
+ return true
+ })
+
+ mm := fuzzy.Find(q, ss)
+ rr := NewRowEvents(t.RowCount() / 2)
+ for _, m := range mm {
+ re, ok := t.rowEvents.At(m.Index)
+ if !ok {
+ log.Error().Msgf("unable to find event for index in fuzzfilter: %d", m.Index)
+ continue
+ }
+ rr.Add(re)
+ }
+
+ return rr
+}
+
+func (t *TableData) filterToast() *RowEvents {
+ idx, ok := t.header.IndexOf("VALID", true)
+ if !ok {
+ return nil
+ }
+
+ rr := NewRowEvents(10)
+ t.rowEvents.Range(func(_ int, re RowEvent) bool {
+ if re.Row.Fields[idx] != "" {
+ rr.Add(re)
+ }
+ return true
+ })
+
+ return rr
+}
+
+func (t *TableData) GetNamespace() string {
+ t.mx.RLock()
+ defer t.mx.RUnlock()
+
+ return t.namespace
+}
+
+func (t *TableData) Reset(ns string) {
+ t.mx.Lock()
+ {
+ t.namespace = ns
+ }
+ t.mx.Unlock()
+
+ t.Clear()
+}
+
+func (t *TableData) Reconcile(ctx context.Context, r Renderer, oo []runtime.Object) error {
+ var rows Rows
+
+ if len(oo) > 0 {
+ if r.IsGeneric() {
+ table, ok := oo[0].(*metav1.Table)
+ if !ok {
+ return fmt.Errorf("expecting a meta table but got %T", oo[0])
+ }
+ rows = make(Rows, len(table.Rows))
+ if err := GenericHydrate(t.namespace, table, rows, r); err != nil {
+ return err
+ }
+ } else {
+ rows = make(Rows, len(oo))
+ if err := Hydrate(t.namespace, oo, rows, r); err != nil {
+ return err
+ }
+ }
+ }
+
+ t.Update(rows)
+ t.SetHeader(t.namespace, r.Header(t.namespace))
+ if t.HeaderCount() == 0 {
+ return fmt.Errorf("fail to list resource %s", t.gvr)
+ }
+
+ return nil
+}
+
+// Empty checks if there are no entries.
+func (t *TableData) Empty() bool {
+ t.mx.RLock()
+ defer t.mx.RUnlock()
+
+ return t.rowEvents.Empty()
+}
+
+func (t *TableData) SetRowEvents(re *RowEvents) {
+ t.rowEvents = re
+}
+
+func (t *TableData) GetRowEvents() *RowEvents {
+ return t.rowEvents
+}
+
+// RowCount returns the number of rows.
+func (t *TableData) RowCount() int {
+ t.mx.RLock()
+ defer t.mx.RUnlock()
+
+ return t.rowEvents.Len()
+}
+
+// IndexOfHeader return the index of the header.
+func (t *TableData) IndexOfHeader(h string) (int, bool) {
+ return t.header.IndexOf(h, false)
+}
+
+// Labelize prints out specific label columns.
+func (t *TableData) Labelize(labels []string) *TableData {
+ idx, ok := t.header.IndexOf("LABELS", true)
+ if !ok {
+ return t
+ }
+ cols := []int{0, 1}
+ if client.IsNamespaced(t.namespace) {
+ cols = cols[1:]
+ }
+ data := TableData{
+ namespace: t.namespace,
+ header: t.header.Labelize(cols, idx, t.rowEvents),
+ }
+ data.rowEvents = t.rowEvents.Labelize(cols, idx, labels)
+
+ return &data
+}
+
+// Customize returns a new model with customized column layout.
+func (t *TableData) Customize(vs *config.ViewSetting, sc SortColumn, manual, wide bool) (*TableData, SortColumn) {
+ if vs.IsBlank() {
+ if sc.Name != "" {
+ return t, sc
+ }
+ psc, err := t.sortCol(vs)
+ if err == nil {
+ return t, psc
+ }
+ return t, sc
+ }
+
+ cols := vs.Columns
+ cdata := TableData{
+ gvr: t.gvr,
+ namespace: t.namespace,
+ header: t.header.Customize(cols, wide),
+ }
+ ids := t.header.MapIndices(cols, wide)
+ cdata.rowEvents = t.rowEvents.Customize(ids)
+ if manual || vs == nil {
+ return &cdata, sc
+ }
+ psc, err := cdata.sortCol(vs)
+ if err != nil {
+ return &cdata, sc
+ }
+
+ return &cdata, psc
+}
+
+func (t *TableData) sortCol(vs *config.ViewSetting) (SortColumn, error) {
+ var psc SortColumn
+
+ if t.HeaderCount() == 0 {
+ return psc, errors.New("no header found")
+ }
+ name, order, _ := vs.SortCol()
+ if _, ok := t.header.IndexOf(name, false); ok {
+ psc.Name, psc.ASC = name, order
+ return psc, nil
+ }
+ if client.IsAllNamespaces(t.GetNamespace()) {
+ if _, ok := t.header.IndexOf("NAMESPACE", false); ok {
+ psc.Name = "NAMESPACE"
+ } else if _, ok := t.header.IndexOf("NAME", false); ok {
+ psc.Name = "NAME"
+ }
+ } else {
+ if _, ok := t.header.IndexOf("NAME", false); ok {
+ psc.Name = "NAME"
+ } else {
+ psc.Name = t.header[0].Name
+ }
+ }
+
+ return psc, nil
+}
+
+// Clear clears out the entire table.
+func (t *TableData) Clear() {
+ t.mx.Lock()
+ defer t.mx.Unlock()
+
+ t.header = t.header.Clear()
+ t.rowEvents.Clear()
+}
+
+// Clone returns a copy of the table.
+func (t *TableData) Clone() *TableData {
+ t.mx.RLock()
+ defer t.mx.RUnlock()
+
+ return &TableData{
+ header: t.header.Clone(),
+ rowEvents: t.rowEvents.Clone(),
+ namespace: t.namespace,
+ }
+}
+
+func (t *TableData) ColumnNames(w bool) []string {
+ t.mx.RLock()
+ defer t.mx.RUnlock()
+
+ return t.header.ColumnNames(w)
+}
+
+// GetHeader returns table header.
+func (t *TableData) GetHeader() Header {
+ t.mx.RLock()
+ defer t.mx.RUnlock()
+
+ return t.header
+}
+
+// SetHeader sets table header.
+func (t *TableData) SetHeader(ns string, h Header) {
+ t.mx.Lock()
+ defer t.mx.Unlock()
+
+ t.namespace, t.header = ns, h
+}
+
+// Update computes row deltas and update the table data.
+func (t *TableData) Update(rows Rows) {
+ empty := t.Empty()
+ kk := make(map[string]struct{}, len(rows))
+ var blankDelta DeltaRow
+ t.mx.Lock()
+ {
+ for _, row := range rows {
+ kk[row.ID] = struct{}{}
+ if empty {
+ t.rowEvents.Add(NewRowEvent(EventAdd, row))
+ continue
+ }
+ if index, ok := t.rowEvents.FindIndex(row.ID); ok {
+ ev, ok := t.rowEvents.At(index)
+ if !ok {
+ continue
+ }
+ delta := NewDeltaRow(ev.Row, row, t.header)
+ if delta.IsBlank() {
+ ev.Kind, ev.Deltas, ev.Row = EventUnchanged, blankDelta, row
+ t.rowEvents.Set(index, ev)
+ } else {
+ t.rowEvents.Set(index, NewRowEventWithDeltas(row, delta))
+ }
+ continue
+ }
+ t.rowEvents.Add(NewRowEvent(EventAdd, row))
+ }
+ }
+ t.mx.Unlock()
+
+ if !empty {
+ t.Delete(kk)
+ }
+}
+
+// Delete removes items in cache that are no longer valid.
+func (t *TableData) Delete(newKeys map[string]struct{}) {
+ t.mx.Lock()
+ {
+ victims := make([]string, 0, 10)
+ t.rowEvents.Range(func(_ int, e RowEvent) bool {
+ if _, ok := newKeys[e.Row.ID]; !ok {
+ victims = append(victims, e.Row.ID)
+ } else {
+ delete(newKeys, e.Row.ID)
+ }
+ return true
+ })
+ for _, id := range victims {
+ if err := t.rowEvents.Delete(id); err != nil {
+ log.Error().Err(err).Msgf("table delete failed: %q", id)
+ }
+ }
+ }
+ t.mx.Unlock()
+}
+
+// Diff checks if two tables are equal.
+func (t *TableData) Diff(t2 *TableData) bool {
+ if t2 == nil || t.namespace != t2.namespace || t.header.Diff(t2.header) {
+ return true
+ }
+ idx, ok := t.header.IndexOf("AGE", true)
+ if !ok {
+ idx = -1
+ }
+ return t.rowEvents.Diff(t2.rowEvents, idx)
+}
diff --git a/internal/model1/table_data_test.go b/internal/model1/table_data_test.go
new file mode 100644
index 00000000..fc338a56
--- /dev/null
+++ b/internal/model1/table_data_test.go
@@ -0,0 +1,404 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1
+
+import (
+ "testing"
+
+ "github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/config"
+ "github.com/rs/zerolog"
+ "github.com/stretchr/testify/assert"
+)
+
+func init() {
+ zerolog.SetGlobalLevel(zerolog.FatalLevel)
+}
+
+func TestTableDataCustomize(t *testing.T) {
+ uu := map[string]struct {
+ t1, e *TableData
+ vs config.ViewSetting
+ sc SortColumn
+ wide, manual bool
+ }{
+ "same": {
+ t1: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ vs: config.ViewSetting{Columns: []string{"A", "B", "C"}},
+ e: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ },
+ "wide-col": {
+ t1: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B", Wide: true},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ vs: config.ViewSetting{Columns: []string{"A", "B", "C"}},
+ e: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B", Wide: false},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ },
+ "wide": {
+ t1: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B", Wide: true},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ wide: true,
+ vs: config.ViewSetting{Columns: []string{"A", "C"}},
+ e: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "C"},
+ HeaderColumn{Name: "B", Wide: true},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "3", "2"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "3", "2"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "3", "2"}}},
+ ),
+ ),
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ td, _ := u.t1.Customize(&u.vs, u.sc, u.manual, u.wide)
+ assert.Equal(t, u.e, td)
+ })
+ }
+}
+
+func TestTableDataDiff(t *testing.T) {
+ uu := map[string]struct {
+ t1, t2 *TableData
+ e bool
+ }{
+ "empty": {
+ t1: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ e: true,
+ },
+ "same": {
+ t1: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ t2: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ },
+ "ns-diff": {
+ t1: NewTableDataFull(
+ client.NewGVR("test"),
+ "ns1",
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ t2: NewTableDataFull(
+ client.NewGVR("test"),
+ "ns-2",
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ e: true,
+ },
+ "header-diff": {
+ t1: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "D"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ t2: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ e: true,
+ },
+ "row-diff": {
+ t1: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ ),
+ t2: NewTableDataWithRows(
+ client.NewGVR("test"),
+ Header{
+ HeaderColumn{Name: "A"},
+ HeaderColumn{Name: "B"},
+ HeaderColumn{Name: "C"},
+ },
+ NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"100", "2", "3"}}},
+ ),
+ ),
+ e: true,
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, u.t1.Diff(u.t2))
+ })
+ }
+}
+
+func TestTableDataUpdate(t *testing.T) {
+ uu := map[string]struct {
+ re, e *RowEvents
+ rr Rows
+ }{
+ "no-change": {
+ re: NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ rr: Rows{
+ Row{ID: "A", Fields: Fields{"1", "2", "3"}},
+ Row{ID: "B", Fields: Fields{"0", "2", "3"}},
+ Row{ID: "C", Fields: Fields{"10", "2", "3"}},
+ },
+ e: NewRowEventsWithEvts(
+ RowEvent{Kind: EventUnchanged, Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Kind: EventUnchanged, Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ },
+ "add": {
+ re: NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ rr: Rows{
+ Row{ID: "A", Fields: Fields{"1", "2", "3"}},
+ Row{ID: "B", Fields: Fields{"0", "2", "3"}},
+ Row{ID: "C", Fields: Fields{"10", "2", "3"}},
+ Row{ID: "D", Fields: Fields{"10", "2", "3"}},
+ },
+ e: NewRowEventsWithEvts(
+ RowEvent{Kind: EventUnchanged, Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Kind: EventUnchanged, Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ RowEvent{Kind: EventAdd, Row: Row{ID: "D", Fields: Fields{"10", "2", "3"}}},
+ ),
+ },
+ "delete": {
+ re: NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ rr: Rows{
+ Row{ID: "A", Fields: Fields{"1", "2", "3"}},
+ Row{ID: "C", Fields: Fields{"10", "2", "3"}},
+ },
+ e: NewRowEventsWithEvts(
+ RowEvent{Kind: EventUnchanged, Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ },
+ "update": {
+ re: NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ rr: Rows{
+ Row{ID: "A", Fields: Fields{"10", "2", "3"}},
+ Row{ID: "B", Fields: Fields{"0", "2", "3"}},
+ Row{ID: "C", Fields: Fields{"10", "2", "3"}},
+ },
+ e: NewRowEventsWithEvts(
+ RowEvent{
+ Kind: EventUpdate,
+ Row: Row{ID: "A", Fields: Fields{"10", "2", "3"}},
+ Deltas: DeltaRow{"1", "", ""},
+ },
+ RowEvent{Kind: EventUnchanged, Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ },
+ }
+
+ var table TableData
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ table.SetRowEvents(u.re)
+ table.Update(u.rr)
+ assert.Equal(t, u.e, table.GetRowEvents())
+ })
+ }
+}
+
+func TestTableDataDelete(t *testing.T) {
+ uu := map[string]struct {
+ re, e *RowEvents
+ kk map[string]struct{}
+ }{
+ "ordered": {
+ re: NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ kk: map[string]struct{}{"A": {}, "C": {}},
+ e: NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ },
+ "unordered": {
+ re: NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ RowEvent{Row: Row{ID: "D", Fields: Fields{"10", "2", "3"}}},
+ ),
+ kk: map[string]struct{}{"C": {}, "A": {}},
+ e: NewRowEventsWithEvts(
+ RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}},
+ RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}},
+ ),
+ },
+ }
+
+ var table TableData
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ table.SetRowEvents(u.re)
+ table.Delete(u.kk)
+ assert.Equal(t, u.e, table.GetRowEvents())
+ })
+ }
+}
diff --git a/internal/model1/test_helper_test.go b/internal/model1/test_helper_test.go
new file mode 100644
index 00000000..42350b33
--- /dev/null
+++ b/internal/model1/test_helper_test.go
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1_test
+
+import (
+ "fmt"
+ "time"
+)
+
+func testTime() time.Time {
+ t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00")
+ if err != nil {
+ fmt.Println("TestTime Failed", err)
+ }
+ return t
+}
diff --git a/internal/model1/types.go b/internal/model1/types.go
new file mode 100644
index 00000000..2fc32ad2
--- /dev/null
+++ b/internal/model1/types.go
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package model1
+
+import (
+ "github.com/derailed/tcell/v2"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+ NAValue = "na"
+
+ // EventUnchanged notifies listener resource has not changed.
+ EventUnchanged ResEvent = 1 << iota
+
+ // EventAdd notifies listener of a resource was added.
+ EventAdd
+
+ // EventUpdate notifies listener of a resource updated.
+ EventUpdate
+
+ // EventDelete notifies listener of a resource was deleted.
+ EventDelete
+
+ // EventClear the stack was reset.
+ EventClear
+)
+
+// DecoratorFunc decorates a string.
+type DecoratorFunc func(string) string
+
+// ColorerFunc represents a resource row colorer.
+type ColorerFunc func(ns string, h Header, re *RowEvent) tcell.Color
+
+// Renderer represents a resource renderer.
+type Renderer interface {
+ // IsGeneric identifies a generic handler.
+ IsGeneric() bool
+
+ // Render converts raw resources to tabular data.
+ Render(o interface{}, ns string, row *Row) error
+
+ // Header returns the resource header.
+ Header(ns string) Header
+
+ // ColorerFunc returns a row colorer function.
+ ColorerFunc() ColorerFunc
+}
+
+// Generic represents a generic resource.
+type Generic interface {
+ // SetTable sets up the resource tabular definition.
+ SetTable(ns string, table *metav1.Table)
+
+ // Header returns a resource header.
+ Header(ns string) Header
+
+ // Render renders the resource.
+ Render(o interface{}, ns string, row *Row) error
+}
diff --git a/internal/render/alias.go b/internal/render/alias.go
index ce8f386d..592296f3 100644
--- a/internal/render/alias.go
+++ b/internal/render/alias.go
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
@@ -18,17 +19,17 @@ type Alias struct {
}
// Header returns a header row.
-func (Alias) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "RESOURCE"},
- HeaderColumn{Name: "COMMAND"},
- HeaderColumn{Name: "API-GROUP"},
+func (Alias) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "RESOURCE"},
+ model1.HeaderColumn{Name: "COMMAND"},
+ model1.HeaderColumn{Name: "API-GROUP"},
}
}
// Render renders a K8s resource to screen.
// BOZO!! Pass in a row with pre-alloc fields??
-func (Alias) Render(o interface{}, ns string, r *Row) error {
+func (Alias) Render(o interface{}, ns string, r *model1.Row) error {
a, ok := o.(AliasRes)
if !ok {
return fmt.Errorf("expected AliasRes, but got %T", o)
diff --git a/internal/render/alias_test.go b/internal/render/alias_test.go
index 85e61f86..af85a3f6 100644
--- a/internal/render/alias_test.go
+++ b/internal/render/alias_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/tcell/v2"
"github.com/stretchr/testify/assert"
@@ -14,30 +15,30 @@ import (
func TestAliasColorer(t *testing.T) {
var a render.Alias
- h := render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
+ h := model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "C"},
}
- r := render.Row{ID: "g/v/r", Fields: render.Fields{"r", "blee", "g"}}
+ r := model1.Row{ID: "g/v/r", Fields: model1.Fields{"r", "blee", "g"}}
uu := map[string]struct {
ns string
- re render.RowEvent
+ re model1.RowEvent
e tcell.Color
}{
"addAll": {
ns: client.NamespaceAll,
- re: render.RowEvent{Kind: render.EventAdd, Row: r},
+ re: model1.RowEvent{Kind: model1.EventAdd, Row: r},
e: tcell.ColorBlue,
},
"deleteAll": {
ns: client.NamespaceAll,
- re: render.RowEvent{Kind: render.EventDelete, Row: r},
+ re: model1.RowEvent{Kind: model1.EventDelete, Row: r},
e: tcell.ColorGray,
},
"updateAll": {
ns: client.NamespaceAll,
- re: render.RowEvent{Kind: render.EventUpdate, Row: r},
+ re: model1.RowEvent{Kind: model1.EventUpdate, Row: r},
e: tcell.ColorDefault,
},
}
@@ -45,16 +46,16 @@ func TestAliasColorer(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, a.ColorerFunc()(u.ns, h, u.re))
+ assert.Equal(t, u.e, a.ColorerFunc()(u.ns, h, &u.re))
})
}
}
func TestAliasHeader(t *testing.T) {
- h := render.Header{
- render.HeaderColumn{Name: "RESOURCE"},
- render.HeaderColumn{Name: "COMMAND"},
- render.HeaderColumn{Name: "API-GROUP"},
+ h := model1.Header{
+ model1.HeaderColumn{Name: "RESOURCE"},
+ model1.HeaderColumn{Name: "COMMAND"},
+ model1.HeaderColumn{Name: "API-GROUP"},
}
var a render.Alias
@@ -70,9 +71,9 @@ func TestAliasRender(t *testing.T) {
Aliases: []string{"a", "b", "c"},
}
- var r render.Row
+ var r model1.Row
assert.Nil(t, a.Render(o, "fred/v1/blee", &r))
- assert.Equal(t, render.Row{ID: "fred/v1/blee", Fields: render.Fields{"blee", "a,b,c", "fred"}}, r)
+ assert.Equal(t, model1.Row{ID: "fred/v1/blee", Fields: model1.Fields{"blee", "a,b,c", "fred"}}, r)
}
func BenchmarkAlias(b *testing.B) {
@@ -85,7 +86,7 @@ func BenchmarkAlias(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
- var r render.Row
+ var r model1.Row
_ = a.Render(o, "aliases", &r)
}
}
diff --git a/internal/render/base.go b/internal/render/base.go
index 65e66b44..003fe6a8 100644
--- a/internal/render/base.go
+++ b/internal/render/base.go
@@ -3,6 +3,10 @@
package render
+import (
+ "github.com/derailed/k9s/internal/model1"
+)
+
// DecoratorFunc decorates a string.
type DecoratorFunc func(string) string
@@ -19,11 +23,11 @@ func (Base) IsGeneric() bool {
}
// ColorerFunc colors a resource row.
-func (Base) ColorerFunc() ColorerFunc {
- return DefaultColorer
+func (Base) ColorerFunc() model1.ColorerFunc {
+ return model1.DefaultColorer
}
// Happy returns true if resource is happy, false otherwise.
-func (Base) Happy(_ string, _ Row) bool {
+func (Base) Happy(string, *model1.Row) bool {
return true
}
diff --git a/internal/render/benchmark.go b/internal/render/benchmark.go
index d8a3c754..ded85e31 100644
--- a/internal/render/benchmark.go
+++ b/internal/render/benchmark.go
@@ -12,6 +12,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -33,33 +34,34 @@ type Benchmark struct {
}
// ColorerFunc colors a resource row.
-func (b Benchmark) ColorerFunc() ColorerFunc {
- return func(ns string, h Header, re RowEvent) tcell.Color {
- if !Happy(ns, h, re.Row) {
- return ErrColor
+func (b Benchmark) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {
+ if !model1.IsValid(ns, h, re.Row) {
+ return model1.ErrColor
}
+
return tcell.ColorPaleGreen
}
}
// Header returns a header row.
-func (Benchmark) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "STATUS"},
- HeaderColumn{Name: "TIME"},
- HeaderColumn{Name: "REQ/S", Align: tview.AlignRight},
- HeaderColumn{Name: "2XX", Align: tview.AlignRight},
- HeaderColumn{Name: "4XX/5XX", Align: tview.AlignRight},
- HeaderColumn{Name: "REPORT"},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (Benchmark) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "STATUS"},
+ model1.HeaderColumn{Name: "TIME"},
+ model1.HeaderColumn{Name: "REQ/S", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "2XX", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "4XX/5XX", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "REPORT"},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (b Benchmark) Render(o interface{}, ns string, r *Row) error {
+func (b Benchmark) Render(o interface{}, ns string, r *model1.Row) error {
bench, ok := o.(BenchInfo)
if !ok {
return fmt.Errorf("no benchmarks available %T", o)
@@ -71,7 +73,7 @@ func (b Benchmark) Render(o interface{}, ns string, r *Row) error {
}
r.ID = bench.Path
- r.Fields = make(Fields, len(b.Header(ns)))
+ r.Fields = make(model1.Fields, len(b.Header(ns)))
if err := b.initRow(r.Fields, bench.File); err != nil {
return err
}
@@ -82,7 +84,7 @@ func (b Benchmark) Render(o interface{}, ns string, r *Row) error {
}
// Happy returns true if resource is happy, false otherwise.
-func (Benchmark) diagnose(ns string, ff Fields) error {
+func (Benchmark) diagnose(ns string, ff model1.Fields) error {
statusCol := 3
if !client.IsAllNamespaces(ns) {
statusCol--
@@ -109,7 +111,7 @@ func (Benchmark) readFile(file string) (string, error) {
return string(data), nil
}
-func (b Benchmark) initRow(row Fields, f os.FileInfo) error {
+func (b Benchmark) initRow(row model1.Fields, f os.FileInfo) error {
tokens := strings.Split(f.Name(), "_")
if len(tokens) < 2 {
return fmt.Errorf("invalid file name %s", f.Name())
@@ -122,7 +124,7 @@ func (b Benchmark) initRow(row Fields, f os.FileInfo) error {
return nil
}
-func (b Benchmark) augmentRow(fields Fields, data string) {
+func (b Benchmark) augmentRow(fields model1.Fields, data string) {
if len(data) == 0 {
return
}
diff --git a/internal/render/benchmark_int_test.go b/internal/render/benchmark_int_test.go
index bd296e23..a7d4a387 100644
--- a/internal/render/benchmark_int_test.go
+++ b/internal/render/benchmark_int_test.go
@@ -7,6 +7,7 @@ import (
"os"
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
)
@@ -18,23 +19,23 @@ func init() {
func TestAugmentRow(t *testing.T) {
uu := map[string]struct {
file string
- e Fields
+ e model1.Fields
}{
"cool": {
"testdata/b1.txt",
- Fields{"pass", "3.3544", "29.8116", "100", "0"},
+ model1.Fields{"pass", "3.3544", "29.8116", "100", "0"},
},
"2XX": {
"testdata/b4.txt",
- Fields{"pass", "3.3544", "29.8116", "160", "0"},
+ model1.Fields{"pass", "3.3544", "29.8116", "160", "0"},
},
"4XX/5XX": {
"testdata/b2.txt",
- Fields{"pass", "3.3544", "29.8116", "100", "12"},
+ model1.Fields{"pass", "3.3544", "29.8116", "100", "12"},
},
"toast": {
"testdata/b3.txt",
- Fields{"fail", "2.3688", "35.4606", "0", "0"},
+ model1.Fields{"fail", "2.3688", "35.4606", "0", "0"},
},
}
@@ -44,7 +45,7 @@ func TestAugmentRow(t *testing.T) {
data, err := os.ReadFile(u.file)
assert.Nil(t, err)
- fields := make(Fields, 8)
+ fields := make(model1.Fields, 8)
b := Benchmark{}
b.augmentRow(fields, string(data))
assert.Equal(t, u.e, fields[2:7])
diff --git a/internal/render/cm.go b/internal/render/cm.go
new file mode 100644
index 00000000..f6158efb
--- /dev/null
+++ b/internal/render/cm.go
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package render
+
+import (
+ "fmt"
+ "strconv"
+
+ v1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+
+ "github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
+
+// ConfigMap renders a K8s ConfigMap to screen.
+type ConfigMap struct {
+ Base
+}
+
+// Header returns a header rbw.
+func (ConfigMap) Header(string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "DATA"},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
+ }
+}
+
+// Render renders a K8s resource to screen.
+func (n ConfigMap) Render(o interface{}, _ string, r *model1.Row) error {
+ raw, ok := o.(*unstructured.Unstructured)
+ if !ok {
+ return fmt.Errorf("expected ConfigMap, but got %T", o)
+ }
+ var cm v1.ConfigMap
+ err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cm)
+ if err != nil {
+ return err
+ }
+
+ r.ID = client.FQN(cm.Namespace, cm.Name)
+ r.Fields = model1.Fields{
+ cm.Namespace,
+ cm.Name,
+ strconv.Itoa(len(cm.Data)),
+ "",
+ ToAge(cm.GetCreationTimestamp()),
+ }
+
+ return nil
+}
diff --git a/internal/render/color_test.go b/internal/render/color_test.go
deleted file mode 100644
index baa8c5ff..00000000
--- a/internal/render/color_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Copyright Authors of K9s
-
-package render_test
-
-import (
- "testing"
-
- "github.com/derailed/k9s/internal/render"
- "github.com/derailed/tcell/v2"
- "github.com/stretchr/testify/assert"
-)
-
-func TestDefaultColorer(t *testing.T) {
- uu := map[string]struct {
- re render.RowEvent
- e tcell.Color
- }{
- "add": {
- render.RowEvent{
- Kind: render.EventAdd,
- },
- render.AddColor,
- },
- "update": {
- render.RowEvent{
- Kind: render.EventUpdate,
- },
- render.ModColor,
- },
- "delete": {
- render.RowEvent{
- Kind: render.EventDelete,
- },
- render.KillColor,
- },
- "no-change": {
- render.RowEvent{
- Kind: render.EventUnchanged,
- },
- render.StdColor,
- },
- "invalid": {
- render.RowEvent{
- Kind: render.EventUnchanged,
- Row: render.Row{
- Fields: render.Fields{"", "", "blah"},
- },
- },
- render.ErrColor,
- },
- }
-
- h := render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "VALID"},
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, render.DefaultColorer("", h, u.re))
- })
- }
-}
diff --git a/internal/render/container.go b/internal/render/container.go
index b9bd968e..35f700f2 100644
--- a/internal/render/container.go
+++ b/internal/render/container.go
@@ -10,6 +10,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
v1 "k8s.io/api/core/v1"
@@ -43,60 +44,58 @@ type Container struct {
}
// ColorerFunc colors a resource row.
-func (c Container) ColorerFunc() ColorerFunc {
- return func(ns string, h Header, re RowEvent) tcell.Color {
- if !Happy(ns, h, re.Row) {
- return ErrColor
- }
+func (c Container) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {
+ c := model1.DefaultColorer(ns, h, re)
- stateCol := h.IndexOf("STATE", true)
- if stateCol == -1 {
- return DefaultColorer(ns, h, re)
+ idx, ok := h.IndexOf("STATE", true)
+ if !ok {
+ return c
}
- switch strings.TrimSpace(re.Row.Fields[stateCol]) {
+ switch strings.TrimSpace(re.Row.Fields[idx]) {
case Pending:
- return PendingColor
+ return model1.PendingColor
case ContainerCreating, PodInitializing:
- return AddColor
+ return model1.AddColor
case Terminating, Initialized:
- return HighlightColor
+ return model1.HighlightColor
case Completed:
- return CompletedColor
+ return model1.CompletedColor
case Running:
- return DefaultColorer(ns, h, re)
+ return c
default:
- return ErrColor
+ return model1.ErrColor
}
}
}
// Header returns a header row.
-func (Container) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "PF"},
- HeaderColumn{Name: "IMAGE"},
- HeaderColumn{Name: "READY"},
- HeaderColumn{Name: "STATE"},
- HeaderColumn{Name: "INIT"},
- HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight},
- HeaderColumn{Name: "PROBES(L:R)"},
- HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight},
- HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight},
- HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "PORTS"},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (Container) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "PF"},
+ model1.HeaderColumn{Name: "IMAGE"},
+ model1.HeaderColumn{Name: "READY"},
+ model1.HeaderColumn{Name: "STATE"},
+ model1.HeaderColumn{Name: "INIT"},
+ model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "PROBES(L:R)"},
+ model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "PORTS"},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (c Container) Render(o interface{}, name string, r *Row) error {
+func (c Container) Render(o interface{}, name string, r *model1.Row) error {
co, ok := o.(ContainerRes)
if !ok {
return fmt.Errorf("expected ContainerRes, but got %T", o)
@@ -109,7 +108,7 @@ func (c Container) Render(o interface{}, name string, r *Row) error {
}
r.ID = co.Container.Name
- r.Fields = Fields{
+ r.Fields = model1.Fields{
co.Container.Name,
"●",
co.Container.Image,
diff --git a/internal/render/container_test.go b/internal/render/container_test.go
index e574df56..f790b3a5 100644
--- a/internal/render/container_test.go
+++ b/internal/render/container_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"time"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
@@ -26,10 +27,10 @@ func TestContainer(t *testing.T) {
IsInit: false,
Age: makeAge(),
}
- var r render.Row
+ var r model1.Row
assert.Nil(t, c.Render(cres, "blee", &r))
assert.Equal(t, "fred", r.ID)
- assert.Equal(t, render.Fields{
+ assert.Equal(t, model1.Fields{
"fred",
"●",
"img",
@@ -63,7 +64,7 @@ func BenchmarkContainerRender(b *testing.B) {
IsInit: false,
Age: makeAge(),
}
- var r render.Row
+ var r model1.Row
b.ReportAllocs()
b.ResetTimer()
diff --git a/internal/render/context.go b/internal/render/context.go
index 3c105e0f..06a622ad 100644
--- a/internal/render/context.go
+++ b/internal/render/context.go
@@ -7,6 +7,7 @@ import (
"fmt"
"strings"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime"
@@ -20,11 +21,11 @@ type Context struct {
}
// ColorerFunc colors a resource row.
-func (Context) ColorerFunc() ColorerFunc {
- return func(ns string, h Header, r RowEvent) tcell.Color {
- c := DefaultColorer(ns, h, r)
+func (Context) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, h model1.Header, r *model1.RowEvent) tcell.Color {
+ c := model1.DefaultColorer(ns, h, r)
if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") {
- return HighlightColor
+ return model1.HighlightColor
}
return c
@@ -32,17 +33,17 @@ func (Context) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
-func (Context) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "CLUSTER"},
- HeaderColumn{Name: "AUTHINFO"},
- HeaderColumn{Name: "NAMESPACE"},
+func (Context) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "CLUSTER"},
+ model1.HeaderColumn{Name: "AUTHINFO"},
+ model1.HeaderColumn{Name: "NAMESPACE"},
}
}
// Render renders a K8s resource to screen.
-func (c Context) Render(o interface{}, _ string, r *Row) error {
+func (c Context) Render(o interface{}, _ string, r *model1.Row) error {
ctx, ok := o.(*NamedContext)
if !ok {
return fmt.Errorf("expected *NamedContext, but got %T", o)
@@ -54,7 +55,7 @@ func (c Context) Render(o interface{}, _ string, r *Row) error {
}
r.ID = ctx.Name
- r.Fields = Fields{
+ r.Fields = model1.Fields{
name,
ctx.Context.Cluster,
ctx.Context.AuthInfo,
diff --git a/internal/render/context_test.go b/internal/render/context_test.go
index 4c20249c..1cdc3911 100644
--- a/internal/render/context_test.go
+++ b/internal/render/context_test.go
@@ -6,6 +6,7 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
"k8s.io/client-go/tools/clientcmd/api"
@@ -20,7 +21,7 @@ func TestContextHeader(t *testing.T) {
func TestContextRender(t *testing.T) {
uu := map[string]struct {
ctx *render.NamedContext
- e render.Row
+ e model1.Row
}{
"active": {
ctx: &render.NamedContext{
@@ -33,9 +34,9 @@ func TestContextRender(t *testing.T) {
},
Config: &config{},
},
- e: render.Row{
+ e: model1.Row{
ID: "c1",
- Fields: render.Fields{"c1", "c1", "u1", "ns1"},
+ Fields: model1.Fields{"c1", "c1", "u1", "ns1"},
},
},
}
@@ -44,7 +45,7 @@ func TestContextRender(t *testing.T) {
for k := range uu {
uc := uu[k]
t.Run(k, func(t *testing.T) {
- row := render.NewRow(4)
+ row := model1.NewRow(4)
err := r.Render(uc.ctx, "", &row)
assert.Nil(t, err)
diff --git a/internal/render/cr.go b/internal/render/cr.go
index 5a0a84fd..a148fc70 100644
--- a/internal/render/cr.go
+++ b/internal/render/cr.go
@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -18,16 +19,16 @@ type ClusterRole struct {
}
// Header returns a header rbw.
-func (ClusterRole) Header(string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (ClusterRole) Header(string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (ClusterRole) Render(o interface{}, ns string, r *Row) error {
+func (ClusterRole) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expecting clusterrole, but got %T", o)
@@ -39,7 +40,7 @@ func (ClusterRole) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.FQN("-", cr.ObjectMeta.Name)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
cr.Name,
mapToStr(cr.Labels),
ToAge(cr.GetCreationTimestamp()),
diff --git a/internal/render/cr_test.go b/internal/render/cr_test.go
index d6908a0e..d6d17531 100644
--- a/internal/render/cr_test.go
+++ b/internal/render/cr_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestClusterRoleRender(t *testing.T) {
c := render.ClusterRole{}
- r := render.NewRow(2)
+ r := model1.NewRow(2)
assert.NoError(t, c.Render(load(t, "cr"), "-", &r))
assert.Equal(t, "-/blee", r.ID)
- assert.Equal(t, render.Fields{"blee"}, r.Fields[:1])
+ assert.Equal(t, model1.Fields{"blee"}, r.Fields[:1])
}
diff --git a/internal/render/crb.go b/internal/render/crb.go
index e051337d..8290973e 100644
--- a/internal/render/crb.go
+++ b/internal/render/crb.go
@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -18,19 +19,19 @@ type ClusterRoleBinding struct {
}
// Header returns a header rbw.
-func (ClusterRoleBinding) Header(string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "CLUSTERROLE"},
- HeaderColumn{Name: "SUBJECT-KIND"},
- HeaderColumn{Name: "SUBJECTS"},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (ClusterRoleBinding) Header(string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "CLUSTERROLE"},
+ model1.HeaderColumn{Name: "SUBJECT-KIND"},
+ model1.HeaderColumn{Name: "SUBJECTS"},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error {
+func (ClusterRoleBinding) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected ClusterRoleBinding, but got %T", o)
@@ -44,7 +45,7 @@ func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error {
kind, ss := renderSubjects(crb.Subjects)
r.ID = client.FQN("-", crb.ObjectMeta.Name)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
crb.Name,
crb.RoleRef.Name,
kind,
diff --git a/internal/render/crb_test.go b/internal/render/crb_test.go
index 85aac276..4b350a4c 100644
--- a/internal/render/crb_test.go
+++ b/internal/render/crb_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestClusterRoleBindingRender(t *testing.T) {
c := render.ClusterRoleBinding{}
- r := render.NewRow(5)
+ r := model1.NewRow(5)
assert.NoError(t, c.Render(load(t, "crb"), "-", &r))
assert.Equal(t, "-/blee", r.ID)
- assert.Equal(t, render.Fields{"blee", "blee", "User", "fernand"}, r.Fields[:4])
+ assert.Equal(t, model1.Fields{"blee", "blee", "User", "fernand"}, r.Fields[:4])
}
diff --git a/internal/render/crd.go b/internal/render/crd.go
index bed197fb..fddeec19 100644
--- a/internal/render/crd.go
+++ b/internal/render/crd.go
@@ -9,6 +9,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/rs/zerolog/log"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -21,18 +22,22 @@ type CustomResourceDefinition struct {
}
// Header returns a header rbw.
-func (CustomResourceDefinition) Header(string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "VERSIONS"},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (CustomResourceDefinition) Header(string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "GROUP"},
+ model1.HeaderColumn{Name: "KIND"},
+ model1.HeaderColumn{Name: "VERSIONS"},
+ model1.HeaderColumn{Name: "SCOPE"},
+ model1.HeaderColumn{Name: "ALIASES", Wide: true},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (c CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error {
+func (c CustomResourceDefinition) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected CustomResourceDefinition, but got %T", o)
@@ -44,7 +49,7 @@ func (c CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error
return err
}
- versions := make([]string, 0, 3)
+ versions := make([]string, 0, len(crd.Spec.Versions))
for _, v := range crd.Spec.Versions {
if v.Served {
n := v.Name
@@ -55,15 +60,19 @@ func (c CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error
}
}
if len(versions) == 0 {
- log.Warn().Msgf("unable to assert CRD versions for %s", crd.GetName())
+ log.Warn().Msgf("unable to assert CRD versions for %s", crd.Name)
}
- r.ID = client.FQN(client.ClusterScope, crd.GetName())
- r.Fields = Fields{
- crd.GetName(),
+ r.ID = client.MetaFQN(crd.ObjectMeta)
+ r.Fields = model1.Fields{
+ crd.Spec.Names.Plural,
+ crd.Spec.Group,
+ crd.Spec.Names.Kind,
naStrings(versions),
+ string(crd.Spec.Scope),
+ naStrings(crd.Spec.Names.ShortNames),
mapToIfc(crd.GetLabels()),
- AsStatus(c.diagnose(crd.GetName(), crd.Spec.Versions)),
+ AsStatus(c.diagnose(crd.Name, crd.Spec.Versions)),
ToAge(crd.GetCreationTimestamp()),
}
diff --git a/internal/render/crd_test.go b/internal/render/crd_test.go
index fd128510..a88715ee 100644
--- a/internal/render/crd_test.go
+++ b/internal/render/crd_test.go
@@ -6,15 +6,17 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestCustomResourceDefinitionRender(t *testing.T) {
c := render.CustomResourceDefinition{}
- r := render.NewRow(2)
+ r := model1.NewRow(2)
assert.NoError(t, c.Render(load(t, "crd"), "", &r))
assert.Equal(t, "-/adapters.config.istio.io", r.ID)
- assert.Equal(t, render.Fields{"adapters.config.istio.io"}, r.Fields[:1])
+ assert.Equal(t, "adapters", r.Fields[0])
+ assert.Equal(t, "config.istio.io", r.Fields[1])
}
diff --git a/internal/render/cronjob.go b/internal/render/cronjob.go
index e75d74f8..bd1ce7c3 100644
--- a/internal/render/cronjob.go
+++ b/internal/render/cronjob.go
@@ -9,6 +9,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -21,29 +22,26 @@ type CronJob struct {
}
// Header returns a header row.
-func (CronJob) Header(ns string) Header {
- h := Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "VS", VS: true},
- HeaderColumn{Name: "SCHEDULE"},
- HeaderColumn{Name: "SUSPEND"},
- HeaderColumn{Name: "ACTIVE"},
- HeaderColumn{Name: "LAST_SCHEDULE", Time: true},
- HeaderColumn{Name: "SELECTOR", Wide: true},
- HeaderColumn{Name: "CONTAINERS", Wide: true},
- HeaderColumn{Name: "IMAGES", Wide: true},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (CronJob) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "VS", VS: true},
+ model1.HeaderColumn{Name: "SCHEDULE"},
+ model1.HeaderColumn{Name: "SUSPEND"},
+ model1.HeaderColumn{Name: "ACTIVE"},
+ model1.HeaderColumn{Name: "LAST_SCHEDULE", Time: true},
+ model1.HeaderColumn{Name: "SELECTOR", Wide: true},
+ model1.HeaderColumn{Name: "CONTAINERS", Wide: true},
+ model1.HeaderColumn{Name: "IMAGES", Wide: true},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
-
- return h
-
}
// Render renders a K8s resource to screen.
-func (c CronJob) Render(o interface{}, ns string, r *Row) error {
+func (c CronJob) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected CronJob, but got %T", o)
@@ -60,7 +58,7 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(cj.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
cj.Namespace,
cj.Name,
computeVulScore(cj.ObjectMeta, &cj.Spec.JobTemplate.Spec.Template.Spec),
diff --git a/internal/render/cronjob_test.go b/internal/render/cronjob_test.go
index ff11bd9b..34a77a96 100644
--- a/internal/render/cronjob_test.go
+++ b/internal/render/cronjob_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestCronJobRender(t *testing.T) {
c := render.CronJob{}
- r := render.NewRow(6)
+ r := model1.NewRow(6)
assert.NoError(t, c.Render(load(t, "cj"), "", &r))
assert.Equal(t, "default/hello", r.ID)
- assert.Equal(t, render.Fields{"default", "hello", "0", "*/1 * * * *", "false", "0"}, r.Fields[:6])
+ assert.Equal(t, model1.Fields{"default", "hello", "0", "*/1 * * * *", "false", "0"}, r.Fields[:6])
}
diff --git a/internal/render/delta_test.go b/internal/render/delta_test.go
deleted file mode 100644
index 08d8960c..00000000
--- a/internal/render/delta_test.go
+++ /dev/null
@@ -1,266 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Copyright Authors of K9s
-
-package render_test
-
-import (
- "testing"
-
- "github.com/derailed/k9s/internal/render"
- "github.com/stretchr/testify/assert"
-)
-
-func TestDeltaLabelize(t *testing.T) {
- uu := map[string]struct {
- o render.Row
- n render.Row
- e render.DeltaRow
- }{
- "same": {
- o: render.Row{
- Fields: render.Fields{"a", "b", "blee=fred,doh=zorg"},
- },
- n: render.Row{
- Fields: render.Fields{"a", "b", "blee=fred1,doh=zorg"},
- },
- e: render.DeltaRow{"", "", "fred", "zorg"},
- },
- }
-
- hh := render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- }
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- d := render.NewDeltaRow(u.o, u.n, hh)
- d = d.Labelize([]int{0, 1}, 2)
- assert.Equal(t, u.e, d)
- })
- }
-}
-
-func TestDeltaCustomize(t *testing.T) {
- uu := map[string]struct {
- r1, r2 render.Row
- cols []int
- e render.DeltaRow
- }{
- "same": {
- r1: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- r2: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- cols: []int{0, 1, 2},
- e: render.DeltaRow{"", "", ""},
- },
- "empty": {
- r1: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- r2: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- e: render.DeltaRow{},
- },
- "diff-full": {
- r1: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- r2: render.Row{
- Fields: render.Fields{"a1", "b1", "c1"},
- },
- cols: []int{0, 1, 2},
- e: render.DeltaRow{"a", "b", "c"},
- },
- "diff-reverse": {
- r1: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- r2: render.Row{
- Fields: render.Fields{"a1", "b1", "c1"},
- },
- cols: []int{2, 1, 0},
- e: render.DeltaRow{"c", "b", "a"},
- },
- "diff-skip": {
- r1: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- r2: render.Row{
- Fields: render.Fields{"a1", "b1", "c1"},
- },
- cols: []int{2, 0},
- e: render.DeltaRow{"c", "a"},
- },
- "diff-missing": {
- r1: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- r2: render.Row{
- Fields: render.Fields{"a1", "b1", "c1"},
- },
- cols: []int{2, 10, 0},
- e: render.DeltaRow{"c", "", "a"},
- },
- "diff-negative": {
- r1: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- r2: render.Row{
- Fields: render.Fields{"a1", "b1", "c1"},
- },
- cols: []int{2, -1, 0},
- e: render.DeltaRow{"c", "", "a"},
- },
- }
-
- hh := render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- }
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- d := render.NewDeltaRow(u.r1, u.r2, hh)
- out := make(render.DeltaRow, len(u.cols))
- d.Customize(u.cols, out)
- assert.Equal(t, u.e, out)
- })
- }
-}
-
-func TestDeltaNew(t *testing.T) {
- uu := map[string]struct {
- o render.Row
- n render.Row
- blank bool
- e render.DeltaRow
- }{
- "same": {
- o: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- n: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- blank: true,
- e: render.DeltaRow{"", "", ""},
- },
- "diff": {
- o: render.Row{
- Fields: render.Fields{"a1", "b", "c"},
- },
- n: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- e: render.DeltaRow{"a1", "", ""},
- },
- "diff2": {
- o: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- n: render.Row{
- Fields: render.Fields{"a", "b1", "c"},
- },
- e: render.DeltaRow{"", "b", ""},
- },
- "diffLast": {
- o: render.Row{
- Fields: render.Fields{"a", "b", "c"},
- },
- n: render.Row{
- Fields: render.Fields{"a", "b", "c1"},
- },
- e: render.DeltaRow{"", "", "c"},
- },
- }
-
- hh := render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- }
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- d := render.NewDeltaRow(u.o, u.n, hh)
- assert.Equal(t, u.e, d)
- assert.Equal(t, u.blank, d.IsBlank())
- })
- }
-}
-
-func TestDeltaBlank(t *testing.T) {
- uu := map[string]struct {
- r render.DeltaRow
- e bool
- }{
- "empty": {
- r: render.DeltaRow{},
- e: true,
- },
- "blank": {
- r: render.DeltaRow{"", "", ""},
- e: true,
- },
- "notblank": {
- r: render.DeltaRow{"", "", "z"},
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.r.IsBlank())
- })
- }
-}
-
-func TestDeltaDiff(t *testing.T) {
- uu := map[string]struct {
- d1, d2 render.DeltaRow
- ageCol int
- e bool
- }{
- "empty": {
- d1: render.DeltaRow{"f1", "f2", "f3"},
- ageCol: 2,
- e: true,
- },
- "same": {
- d1: render.DeltaRow{"f1", "f2", "f3"},
- d2: render.DeltaRow{"f1", "f2", "f3"},
- ageCol: -1,
- },
- "diff": {
- d1: render.DeltaRow{"f1", "f2", "f3"},
- d2: render.DeltaRow{"f1", "f2", "f13"},
- ageCol: -1,
- e: true,
- },
- "diff-age-first": {
- d1: render.DeltaRow{"f1", "f2", "f3"},
- d2: render.DeltaRow{"f1", "f2", "f13"},
- ageCol: 0,
- e: true,
- },
- "diff-age-last": {
- d1: render.DeltaRow{"f1", "f2", "f3"},
- d2: render.DeltaRow{"f1", "f2", "f13"},
- ageCol: 2,
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.d1.Diff(u.d2, u.ageCol))
- })
- }
-}
diff --git a/internal/render/dir.go b/internal/render/dir.go
index b444c8c7..8e076d4e 100644
--- a/internal/render/dir.go
+++ b/internal/render/dir.go
@@ -7,6 +7,7 @@ import (
"fmt"
"os"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -21,22 +22,22 @@ func (Dir) IsGeneric() bool {
}
// ColorerFunc colors a resource row.
-func (Dir) ColorerFunc() ColorerFunc {
- return func(ns string, _ Header, re RowEvent) tcell.Color {
+func (Dir) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color {
return tcell.ColorCadetBlue
}
}
// Header returns a header row.
-func (Dir) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
+func (Dir) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
}
}
// Render renders a K8s resource to screen.
// BOZO!! Pass in a row with pre-alloc fields??
-func (Dir) Render(o interface{}, ns string, r *Row) error {
+func (Dir) Render(o interface{}, ns string, r *model1.Row) error {
d, ok := o.(DirRes)
if !ok {
return fmt.Errorf("expected DirRes, but got %T", o)
diff --git a/internal/render/dp.go b/internal/render/dp.go
index 01eb96e6..1444eeb9 100644
--- a/internal/render/dp.go
+++ b/internal/render/dp.go
@@ -9,6 +9,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
appsv1 "k8s.io/api/apps/v1"
@@ -22,20 +23,18 @@ type Deployment struct {
}
// ColorerFunc colors a resource row.
-func (d Deployment) ColorerFunc() ColorerFunc {
- return func(ns string, h Header, re RowEvent) tcell.Color {
- c := DefaultColorer(ns, h, re)
- if !Happy(ns, h, re.Row) {
- return ErrColor
- }
- rdCol := h.IndexOf("READY", true)
- if rdCol == -1 {
+func (d Deployment) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {
+ c := model1.DefaultColorer(ns, h, re)
+
+ idx, ok := h.IndexOf("READY", true)
+ if !ok {
return c
}
- ready := strings.TrimSpace(re.Row.Fields[rdCol])
+ ready := strings.TrimSpace(re.Row.Fields[idx])
tt := strings.Split(ready, "/")
if len(tt) == 2 && tt[1] == "0" {
- return PendingColor
+ return model1.PendingColor
}
return c
@@ -43,24 +42,22 @@ func (d Deployment) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
-func (Deployment) Header(ns string) Header {
- h := Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "VS", VS: true},
- HeaderColumn{Name: "READY", Align: tview.AlignRight},
- HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight},
- HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (Deployment) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "VS", VS: true},
+ model1.HeaderColumn{Name: "READY", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
-
- return h
}
// Render renders a K8s resource to screen.
-func (d Deployment) Render(o interface{}, ns string, r *Row) error {
+func (d Deployment) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected Deployment, but got %T", o)
@@ -73,7 +70,7 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(dp.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
dp.Namespace,
dp.Name,
computeVulScore(dp.ObjectMeta, &dp.Spec.Template.Spec),
diff --git a/internal/render/dp_test.go b/internal/render/dp_test.go
index 92653b18..e4ecc4b1 100644
--- a/internal/render/dp_test.go
+++ b/internal/render/dp_test.go
@@ -6,22 +6,23 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestDpRender(t *testing.T) {
c := render.Deployment{}
- r := render.NewRow(7)
+ r := model1.NewRow(7)
assert.Nil(t, c.Render(load(t, "dp"), "", &r))
assert.Equal(t, "icx/icx-db", r.ID)
- assert.Equal(t, render.Fields{"icx", "icx-db", "0", "1/1", "1", "1"}, r.Fields[:6])
+ assert.Equal(t, model1.Fields{"icx", "icx-db", "0", "1/1", "1", "1"}, r.Fields[:6])
}
func BenchmarkDpRender(b *testing.B) {
c := render.Deployment{}
- r := render.NewRow(7)
+ r := model1.NewRow(7)
o := load(b, "dp")
b.ResetTimer()
diff --git a/internal/render/ds.go b/internal/render/ds.go
index d14a1569..b3f047aa 100644
--- a/internal/render/ds.go
+++ b/internal/render/ds.go
@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tview"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -20,26 +21,24 @@ type DaemonSet struct {
}
// Header returns a header row.
-func (DaemonSet) Header(ns string) Header {
- h := Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "VS", VS: true},
- HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
- HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
- HeaderColumn{Name: "READY", Align: tview.AlignRight},
- HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight},
- HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (DaemonSet) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "VS", VS: true},
+ model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "READY", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
-
- return h
}
// Render renders a K8s resource to screen.
-func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
+func (d DaemonSet) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected DaemonSet, but got %T", o)
@@ -51,7 +50,7 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(ds.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
ds.Namespace,
ds.Name,
computeVulScore(ds.ObjectMeta, &ds.Spec.Template.Spec),
diff --git a/internal/render/ds_test.go b/internal/render/ds_test.go
index 5753bcb6..16598332 100644
--- a/internal/render/ds_test.go
+++ b/internal/render/ds_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestDaemonSetRender(t *testing.T) {
c := render.DaemonSet{}
- r := render.NewRow(9)
+ r := model1.NewRow(9)
assert.NoError(t, c.Render(load(t, "ds"), "", &r))
assert.Equal(t, "kube-system/fluentd-gcp-v3.2.0", r.ID)
- assert.Equal(t, render.Fields{"kube-system", "fluentd-gcp-v3.2.0", "0", "2", "2", "2", "2", "2"}, r.Fields[:8])
+ assert.Equal(t, model1.Fields{"kube-system", "fluentd-gcp-v3.2.0", "0", "2", "2", "2", "2", "2"}, r.Fields[:8])
}
diff --git a/internal/render/ep.go b/internal/render/ep.go
index 15af70d1..9fa4bcc8 100644
--- a/internal/render/ep.go
+++ b/internal/render/ep.go
@@ -9,6 +9,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -20,17 +21,17 @@ type Endpoints struct {
}
// Header returns a header row.
-func (Endpoints) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "ENDPOINTS"},
- HeaderColumn{Name: "AGE", Time: true},
+func (Endpoints) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "ENDPOINTS"},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (e Endpoints) Render(o interface{}, ns string, r *Row) error {
+func (e Endpoints) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected Endpoints, but got %T", o)
@@ -42,8 +43,8 @@ func (e Endpoints) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(ep.ObjectMeta)
- r.Fields = make(Fields, 0, len(e.Header(ns)))
- r.Fields = Fields{
+ r.Fields = make(model1.Fields, 0, len(e.Header(ns)))
+ r.Fields = model1.Fields{
ep.Namespace,
ep.Name,
missing(toEPs(ep.Subsets)),
diff --git a/internal/render/ep_test.go b/internal/render/ep_test.go
index 620f87e0..f4359f3a 100644
--- a/internal/render/ep_test.go
+++ b/internal/render/ep_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestEndpointsRender(t *testing.T) {
c := render.Endpoints{}
- r := render.NewRow(4)
+ r := model1.NewRow(4)
assert.NoError(t, c.Render(load(t, "ep"), "", &r))
assert.Equal(t, "default/dictionary1", r.ID)
- assert.Equal(t, render.Fields{"default", "dictionary1", ""}, r.Fields[:3])
+ assert.Equal(t, model1.Fields{"default", "dictionary1", ""}, r.Fields[:3])
}
diff --git a/internal/render/ev.go b/internal/render/ev.go
index f2e8b9b1..28e04f79 100644
--- a/internal/render/ev.go
+++ b/internal/render/ev.go
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -22,14 +23,14 @@ func (*Event) IsGeneric() bool {
}
// ColorerFunc colors a resource row.
-func (e *Event) ColorerFunc() ColorerFunc {
- return func(ns string, h Header, re RowEvent) tcell.Color {
- reasonCol := h.IndexOf("REASON", true)
- if reasonCol >= 0 && strings.TrimSpace(re.Row.Fields[reasonCol]) == "Killing" {
- return KillColor
+func (e *Event) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {
+ idx, ok := h.IndexOf("REASON", true)
+ if ok && strings.TrimSpace(re.Row.Fields[idx]) == "Killing" {
+ return model1.KillColor
}
- return DefaultColorer(ns, h, re)
+ return model1.DefaultColorer(ns, h, re)
}
}
@@ -46,14 +47,14 @@ var wideCols = map[string]struct{}{
"MESSAGE": {},
}
-func (e *Event) Header(ns string) Header {
+func (e *Event) Header(ns string) model1.Header {
if e.table == nil {
- return Header{}
+ return model1.Header{}
}
- hh := make(Header, 0, len(e.table.ColumnDefinitions))
- hh = append(hh, HeaderColumn{Name: "NAMESPACE"})
+ hh := make(model1.Header, 0, len(e.table.ColumnDefinitions))
+ hh = append(hh, model1.HeaderColumn{Name: "NAMESPACE"})
for _, h := range e.table.ColumnDefinitions {
- header := HeaderColumn{Name: strings.ToUpper(h.Name)}
+ header := model1.HeaderColumn{Name: strings.ToUpper(h.Name)}
if _, ok := ageCols[header.Name]; ok {
header.Time = true
}
@@ -67,7 +68,7 @@ func (e *Event) Header(ns string) Header {
}
// Render renders a K8s resource to screen.
-func (e *Event) Render(o interface{}, ns string, r *Row) error {
+func (e *Event) Render(o interface{}, ns string, r *model1.Row) error {
row, ok := o.(metav1.TableRow)
if !ok {
return fmt.Errorf("expecting a TableRow but got %T", o)
@@ -81,7 +82,7 @@ func (e *Event) Render(o interface{}, ns string, r *Row) error {
return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0])
}
r.ID = client.FQN(nns, name)
- r.Fields = make(Fields, 0, len(e.Header(ns)))
+ r.Fields = make(model1.Fields, 0, len(e.Header(ns)))
r.Fields = append(r.Fields, nns)
for _, o := range row.Cells {
if o == nil {
diff --git a/internal/render/ev_test.go b/internal/render/ev_test.go
index ce7ba27c..dbb0794b 100644
--- a/internal/render/ev_test.go
+++ b/internal/render/ev_test.go
@@ -6,17 +6,17 @@ package render_test
// BOZO!!
// func TestEventRender(t *testing.T) {
// c := render.Event{}
-// r := render.NewRow(7)
+// r := model1.NewRow(7)
// c.Render(load(t, "ev"), "", &r)
// assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID)
-// assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7])
+// assert.Equal(t, model1.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7])
// }
// func BenchmarkEventRender(b *testing.B) {
// ev := load(b, "ev")
// var re render.Event
-// r := render.NewRow(7)
+// r := model1.NewRow(7)
// b.ResetTimer()
// b.ReportAllocs()
diff --git a/internal/render/generic.go b/internal/render/generic.go
index fc89fdbe..56f55239 100644
--- a/internal/render/generic.go
+++ b/internal/render/generic.go
@@ -10,6 +10,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -20,7 +21,7 @@ const ageTableCol = "Age"
type Generic struct {
Base
table *metav1.Table
- header Header
+ header model1.Header
ageIndex int
}
@@ -35,38 +36,38 @@ func (g *Generic) SetTable(ns string, t *metav1.Table) {
}
// ColorerFunc colors a resource row.
-func (*Generic) ColorerFunc() ColorerFunc {
- return DefaultColorer
+func (*Generic) ColorerFunc() model1.ColorerFunc {
+ return model1.DefaultColorer
}
// Header returns a header row.
-func (g *Generic) Header(ns string) Header {
+func (g *Generic) Header(ns string) model1.Header {
if g.header != nil {
return g.header
}
if g.table == nil {
- return Header{}
+ return model1.Header{}
}
- h := make(Header, 0, len(g.table.ColumnDefinitions))
+ h := make(model1.Header, 0, len(g.table.ColumnDefinitions))
if !client.IsClusterScoped(ns) {
- h = append(h, HeaderColumn{Name: "NAMESPACE"})
+ h = append(h, model1.HeaderColumn{Name: "NAMESPACE"})
}
for i, c := range g.table.ColumnDefinitions {
if c.Name == ageTableCol {
g.ageIndex = i
continue
}
- h = append(h, HeaderColumn{Name: strings.ToUpper(c.Name)})
+ h = append(h, model1.HeaderColumn{Name: strings.ToUpper(c.Name)})
}
if g.ageIndex > 0 {
- h = append(h, HeaderColumn{Name: "AGE", Time: true})
+ h = append(h, model1.HeaderColumn{Name: "AGE", Time: true})
}
return h
}
// Render renders a K8s resource to screen.
-func (g *Generic) Render(o interface{}, ns string, r *Row) error {
+func (g *Generic) Render(o interface{}, ns string, r *model1.Row) error {
row, ok := o.(metav1.TableRow)
if !ok {
return fmt.Errorf("expecting a TableRow but got %T", o)
@@ -80,7 +81,7 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0])
}
r.ID = client.FQN(nns, name)
- r.Fields = make(Fields, 0, len(g.Header(ns)))
+ r.Fields = make(model1.Fields, 0, len(g.Header(ns)))
if !client.IsClusterScoped(ns) {
r.Fields = append(r.Fields, nns)
}
diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go
index 851aaa72..a7180762 100644
--- a/internal/render/generic_test.go
+++ b/internal/render/generic_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
@@ -18,65 +19,65 @@ func TestGenericRender(t *testing.T) {
ns string
table *metav1beta1.Table
eID string
- eFields render.Fields
- eHeader render.Header
+ eFields model1.Fields
+ eHeader model1.Header
}{
"withNS": {
ns: "ns1",
table: makeNSGeneric(),
eID: "ns1/fred",
- eFields: render.Fields{"ns1", "c1", "c2", "c3"},
- eHeader: render.Header{
- render.HeaderColumn{Name: "NAMESPACE"},
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
+ eFields: model1.Fields{"ns1", "c1", "c2", "c3"},
+ eHeader: model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "C"},
},
},
"all": {
ns: client.NamespaceAll,
table: makeNSGeneric(),
eID: "ns1/fred",
- eFields: render.Fields{"ns1", "c1", "c2", "c3"},
- eHeader: render.Header{
- render.HeaderColumn{Name: "NAMESPACE"},
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
+ eFields: model1.Fields{"ns1", "c1", "c2", "c3"},
+ eHeader: model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "C"},
},
},
"allNS": {
ns: client.NamespaceAll,
table: makeNSGeneric(),
eID: "ns1/fred",
- eFields: render.Fields{"ns1", "c1", "c2", "c3"},
- eHeader: render.Header{
- render.HeaderColumn{Name: "NAMESPACE"},
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
+ eFields: model1.Fields{"ns1", "c1", "c2", "c3"},
+ eHeader: model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "C"},
},
},
"clusterWide": {
ns: client.ClusterScope,
table: makeNoNSGeneric(),
eID: "-/fred",
- eFields: render.Fields{"c1", "c2", "c3"},
- eHeader: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
+ eFields: model1.Fields{"c1", "c2", "c3"},
+ eHeader: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "C"},
},
},
"age": {
ns: client.ClusterScope,
table: makeAgeGeneric(),
eID: "-/fred",
- eFields: render.Fields{"c1", "c2", "2d"},
- eHeader: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "C"},
- render.HeaderColumn{Name: "AGE", Time: true},
+ eFields: model1.Fields{"c1", "c2", "2d"},
+ eHeader: model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "C"},
+ model1.HeaderColumn{Name: "AGE", Time: true},
},
},
}
@@ -85,7 +86,7 @@ func TestGenericRender(t *testing.T) {
var re render.Generic
u := uu[k]
t.Run(k, func(t *testing.T) {
- var r render.Row
+ var r model1.Row
re.SetTable(u.ns, u.table)
assert.Equal(t, u.eHeader, re.Header(u.ns))
diff --git a/internal/render/helm/chart.go b/internal/render/helm/chart.go
index b41d51f0..ee17b98a 100644
--- a/internal/render/helm/chart.go
+++ b/internal/render/helm/chart.go
@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"helm.sh/helm/v3/pkg/release"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -24,33 +25,33 @@ func (Chart) IsGeneric() bool {
}
// ColorerFunc colors a resource row.
-func (Chart) ColorerFunc() render.ColorerFunc {
- return render.DefaultColorer
+func (Chart) ColorerFunc() model1.ColorerFunc {
+ return model1.DefaultColorer
}
// Header returns a header row.
-func (Chart) Header(_ string) render.Header {
- return render.Header{
- render.HeaderColumn{Name: "NAMESPACE"},
- render.HeaderColumn{Name: "NAME"},
- render.HeaderColumn{Name: "REVISION"},
- render.HeaderColumn{Name: "STATUS"},
- render.HeaderColumn{Name: "CHART"},
- render.HeaderColumn{Name: "APP VERSION"},
- render.HeaderColumn{Name: "VALID", Wide: true},
- render.HeaderColumn{Name: "AGE", Time: true},
+func (Chart) Header(_ string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "REVISION"},
+ model1.HeaderColumn{Name: "STATUS"},
+ model1.HeaderColumn{Name: "CHART"},
+ model1.HeaderColumn{Name: "APP VERSION"},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a chart to screen.
-func (c Chart) Render(o interface{}, ns string, r *render.Row) error {
+func (c Chart) Render(o interface{}, ns string, r *model1.Row) error {
h, ok := o.(ReleaseRes)
if !ok {
return fmt.Errorf("expected ReleaseRes, but got %T", o)
}
r.ID = client.FQN(h.Release.Namespace, h.Release.Name)
- r.Fields = render.Fields{
+ r.Fields = model1.Fields{
h.Release.Namespace,
h.Release.Name,
strconv.Itoa(h.Release.Version),
diff --git a/internal/render/helm/history.go b/internal/render/helm/history.go
index 5fbbaf6d..cf0f118d 100644
--- a/internal/render/helm/history.go
+++ b/internal/render/helm/history.go
@@ -9,6 +9,7 @@ import (
"strconv"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
)
@@ -26,24 +27,24 @@ func (History) IsGeneric() bool {
}
// ColorerFunc colors a resource row.
-func (History) ColorerFunc() render.ColorerFunc {
- return render.DefaultColorer
+func (History) ColorerFunc() model1.ColorerFunc {
+ return model1.DefaultColorer
}
// Header returns a header row.
-func (History) Header(_ string) render.Header {
- return render.Header{
- render.HeaderColumn{Name: "REVISION"},
- render.HeaderColumn{Name: "STATUS"},
- render.HeaderColumn{Name: "CHART"},
- render.HeaderColumn{Name: "APP VERSION"},
- render.HeaderColumn{Name: "DESCRIPTION"},
- render.HeaderColumn{Name: "VALID", Wide: true},
+func (History) Header(_ string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "REVISION"},
+ model1.HeaderColumn{Name: "STATUS"},
+ model1.HeaderColumn{Name: "CHART"},
+ model1.HeaderColumn{Name: "APP VERSION"},
+ model1.HeaderColumn{Name: "DESCRIPTION"},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
}
}
// Render renders a chart to screen.
-func (c History) Render(o interface{}, ns string, r *render.Row) error {
+func (c History) Render(o interface{}, ns string, r *model1.Row) error {
h, ok := o.(ReleaseRes)
if !ok {
return fmt.Errorf("expected HistoryRes, but got %T", o)
@@ -51,7 +52,7 @@ func (c History) Render(o interface{}, ns string, r *render.Row) error {
r.ID = client.FQN(h.Release.Namespace, h.Release.Name)
r.ID += ":" + strconv.Itoa(h.Release.Version)
- r.Fields = render.Fields{
+ r.Fields = model1.Fields{
strconv.Itoa(h.Release.Version),
h.Release.Info.Status.String(),
h.Release.Chart.Metadata.Name + "-" + h.Release.Chart.Metadata.Version,
diff --git a/internal/render/helpers.go b/internal/render/helpers.go
index bd474cdd..522a6fdf 100644
--- a/internal/render/helpers.go
+++ b/internal/render/helpers.go
@@ -5,7 +5,6 @@ package render
import (
"context"
- "math"
"sort"
"strconv"
"strings"
@@ -19,11 +18,21 @@ import (
"golang.org/x/text/language"
"golang.org/x/text/message"
v1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/duration"
)
+// ExtractImages returns a collection of container images.
+// !!BOZO!! If this has any legs?? enable scans on other container types.
+func ExtractImages(spec *v1.PodSpec) []string {
+ ii := make([]string, 0, len(spec.Containers))
+ for _, c := range spec.Containers {
+ ii = append(ii, c.Image)
+ }
+
+ return ii
+}
+
func computeVulScore(m metav1.ObjectMeta, spec *v1.PodSpec) string {
if vul.ImgScanner == nil || vul.ImgScanner.ShouldExcludes(m) {
return "0"
@@ -46,62 +55,12 @@ func runesToNum(rr []rune) int64 {
return r
}
-func durationToSeconds(duration string) int64 {
- if len(duration) == 0 {
- return 0
- }
- if duration == NAValue {
- return math.MaxInt64
- }
-
- num := make([]rune, 0, 5)
- var n, m int64
- for _, r := range duration {
- switch r {
- case 'y':
- m = 365 * 24 * 60 * 60
- case 'd':
- m = 24 * 60 * 60
- case 'h':
- m = 60 * 60
- case 'm':
- m = 60
- case 's':
- m = 1
- default:
- num = append(num, r)
- continue
- }
- n, num = n+runesToNum(num)*m, num[:0]
- }
-
- return n
-}
-
-func capacityToNumber(capacity string) int64 {
- quantity := resource.MustParse(capacity)
- return quantity.Value()
-}
-
// AsThousands prints a number with thousand separator.
func AsThousands(n int64) string {
p := message.NewPrinter(language.English)
return p.Sprintf("%d", n)
}
-// Happy returns true if resource is happy, false otherwise.
-func Happy(ns string, h Header, r Row) bool {
- if len(r.Fields) == 0 {
- return true
- }
- validCol := h.IndexOf("VALID", true)
- if validCol < 0 {
- return true
- }
-
- return strings.TrimSpace(r.Fields[validCol]) == ""
-}
-
// AsStatus returns error as string.
func AsStatus(err error) string {
if err == nil {
@@ -333,15 +292,15 @@ func strPtrToStr(s *string) string {
return *s
}
-// Check if string is in a string list.
-func in(ll []string, s string) bool {
- for _, l := range ll {
- if l == s {
- return true
- }
- }
- return false
-}
+// // Check if string is in a string list.
+// func in(ll []string, s string) bool {
+// for _, l := range ll {
+// if l == s {
+// return true
+// }
+// }
+// return false
+// }
// Pad a string up to the given length or truncates if greater than length.
func Pad(s string, width int) string {
@@ -356,29 +315,29 @@ func Pad(s string, width int) string {
return s + strings.Repeat(" ", width-len(s))
}
-// Converts labels string to map.
-func labelize(labels string) map[string]string {
- ll := strings.Split(labels, ",")
- data := make(map[string]string, len(ll))
+// // Converts labels string to map.
+// func labelize(labels string) map[string]string {
+// ll := strings.Split(labels, ",")
+// data := make(map[string]string, len(ll))
- for _, l := range ll {
- tokens := strings.Split(l, "=")
- if len(tokens) == 2 {
- data[tokens[0]] = tokens[1]
- }
- }
+// for _, l := range ll {
+// tokens := strings.Split(l, "=")
+// if len(tokens) == 2 {
+// data[tokens[0]] = tokens[1]
+// }
+// }
- return data
-}
+// return data
+// }
-func sortLabels(m map[string]string) (keys, vals []string) {
- for k := range m {
- keys = append(keys, k)
- }
- sort.Strings(keys)
- for _, k := range keys {
- vals = append(vals, m[k])
- }
+// func sortLabels(m map[string]string) (keys, vals []string) {
+// for k := range m {
+// keys = append(keys, k)
+// }
+// sort.Strings(keys)
+// for _, k := range keys {
+// vals = append(vals, m[k])
+// }
- return
-}
+// return
+// }
diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go
index 05b0f89d..0c6d0787 100644
--- a/internal/render/helpers_test.go
+++ b/internal/render/helpers_test.go
@@ -4,91 +4,57 @@
package render
import (
- "math"
+ "encoding/json"
+ "fmt"
+ "os"
"testing"
"time"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
+ "k8s.io/apimachinery/pkg/runtime"
)
-func TestSortLabels(t *testing.T) {
- uu := map[string]struct {
- labels string
- e [][]string
- }{
- "simple": {
- labels: "a=b,c=d",
- e: [][]string{
- {"a", "c"},
- {"b", "d"},
+func TestTableGenericHydrate(t *testing.T) {
+ raw := raw(t, "p1")
+ tt := metav1beta1.Table{
+ ColumnDefinitions: []metav1beta1.TableColumnDefinition{
+ {Name: "c1"},
+ {Name: "c2"},
+ },
+ Rows: []metav1beta1.TableRow{
+ {
+ Cells: []interface{}{"fred", 10},
+ Object: runtime.RawExtension{Raw: raw},
+ },
+ {
+ Cells: []interface{}{"blee", 20},
+ Object: runtime.RawExtension{Raw: raw},
},
},
}
+ rr := make([]model1.Row, 2)
+ var re Generic
+ re.SetTable("blee", &tt)
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- hh, vv := sortLabels(labelize(u.labels))
- assert.Equal(t, u.e[0], hh)
- assert.Equal(t, u.e[1], vv)
- })
- }
+ assert.Nil(t, model1.GenericHydrate("blee", &tt, rr, &re))
+ assert.Equal(t, 2, len(rr))
+ assert.Equal(t, 3, len(rr[0].Fields))
}
-func TestLabelize(t *testing.T) {
- uu := map[string]struct {
- labels string
- e map[string]string
- }{
- "simple": {
- labels: "a=b,c=d",
- e: map[string]string{"a": "b", "c": "d"},
- },
+func TestTableHydrate(t *testing.T) {
+ oo := []runtime.Object{
+ &PodWithMetrics{Raw: load(t, "p1")},
}
+ rr := make([]model1.Row, 1)
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, labelize(u.labels))
- })
- }
-}
-
-func TestDurationToSecond(t *testing.T) {
- uu := map[string]struct {
- s string
- e int64
- }{
- "seconds": {s: "22s", e: 22},
- "minutes": {s: "22m", e: 1320},
- "hours": {s: "12h", e: 43200},
- "days": {s: "3d", e: 259200},
- "day_hour": {s: "3d9h", e: 291600},
- "day_hour_minute": {s: "2d22h3m", e: 252180},
- "day_hour_minute_seconds": {s: "2d22h3m50s", e: 252230},
- "year": {s: "3y", e: 94608000},
- "year_day": {s: "1y2d", e: 31708800},
- "n/a": {s: NAValue, e: math.MaxInt64},
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, durationToSeconds(u.s))
- })
- }
-}
-
-func BenchmarkDurationToSecond(b *testing.B) {
- t := "2d22h3m50s"
-
- b.ReportAllocs()
- b.ResetTimer()
- for n := 0; n < b.N; n++ {
- durationToSeconds(t)
- }
+ assert.Nil(t, model1.Hydrate("blee", oo, rr, Pod{}))
+ assert.Equal(t, 1, len(rr))
+ assert.Equal(t, 23, len(rr[0].Fields))
}
func TestToAge(t *testing.T) {
@@ -308,34 +274,6 @@ func TestBlank(t *testing.T) {
}
}
-func TestIn(t *testing.T) {
- uu := map[string]struct {
- a []string
- v string
- e bool
- }{
- "in": {
- a: []string{"fred", "blee"},
- v: "blee",
- e: true,
- },
- "empty": {
- v: "blee",
- },
- "missing": {
- a: []string{"fred", "blee"},
- v: "duh",
- },
- }
-
- for k := range uu {
- uc := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, uc.e, in(uc.a, uc.v))
- })
- }
-}
-
func TestMetaFQN(t *testing.T) {
uu := map[string]struct {
m metav1.ObjectMeta
@@ -489,3 +427,20 @@ func BenchmarkIntToStr(b *testing.B) {
IntToStr(v)
}
}
+
+// Helpers...
+
+func load(t *testing.T, n string) *unstructured.Unstructured {
+ raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
+ assert.Nil(t, err)
+ var o unstructured.Unstructured
+ err = json.Unmarshal(raw, &o)
+ assert.Nil(t, err)
+ return &o
+}
+
+func raw(t *testing.T, n string) []byte {
+ raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n))
+ assert.Nil(t, err)
+ return raw
+}
diff --git a/internal/render/img_scan.go b/internal/render/img_scan.go
index 03ab3c2e..691e2f10 100644
--- a/internal/render/img_scan.go
+++ b/internal/render/img_scan.go
@@ -7,6 +7,7 @@ import (
"fmt"
"strings"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/vul"
"github.com/derailed/tcell/v2"
"k8s.io/apimachinery/pkg/runtime"
@@ -24,15 +25,15 @@ type ImageScan struct {
}
// ColorerFunc colors a resource row.
-func (c ImageScan) ColorerFunc() ColorerFunc {
- return func(ns string, h Header, re RowEvent) tcell.Color {
- c := DefaultColorer(ns, h, re)
+func (c ImageScan) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {
+ c := model1.DefaultColorer(ns, h, re)
- sevCol := h.IndexOf(sevColName, true)
- if sevCol == -1 {
+ idx, ok := h.IndexOf(sevColName, true)
+ if !ok {
return c
}
- sev := strings.TrimSpace(re.Row.Fields[sevCol])
+ sev := strings.TrimSpace(re.Row.Fields[idx])
switch sev {
case vul.Sev1:
c = tcell.ColorRed
@@ -54,27 +55,27 @@ func (c ImageScan) ColorerFunc() ColorerFunc {
}
// Header returns a header row.
-func (ImageScan) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "SEVERITY"},
- HeaderColumn{Name: "VULNERABILITY"},
- HeaderColumn{Name: "IMAGE"},
- HeaderColumn{Name: "LIBRARY"},
- HeaderColumn{Name: "VERSION"},
- HeaderColumn{Name: "FIXED-IN"},
- HeaderColumn{Name: "TYPE"},
+func (ImageScan) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "SEVERITY"},
+ model1.HeaderColumn{Name: "VULNERABILITY"},
+ model1.HeaderColumn{Name: "IMAGE"},
+ model1.HeaderColumn{Name: "LIBRARY"},
+ model1.HeaderColumn{Name: "VERSION"},
+ model1.HeaderColumn{Name: "FIXED-IN"},
+ model1.HeaderColumn{Name: "TYPE"},
}
}
// Render renders a K8s resource to screen.
-func (is ImageScan) Render(o interface{}, name string, r *Row) error {
+func (is ImageScan) Render(o interface{}, name string, r *model1.Row) error {
res, ok := o.(ImageScanRes)
if !ok {
return fmt.Errorf("expected ImageScanRes, but got %T", o)
}
r.ID = fmt.Sprintf("%s|%s", res.Image, strings.Join(res.Row, "|"))
- r.Fields = Fields{
+ r.Fields = model1.Fields{
res.Row.Severity(),
res.Row.Vulnerability(),
res.Image,
diff --git a/internal/render/job.go b/internal/render/job.go
index 89298d31..451f811c 100644
--- a/internal/render/job.go
+++ b/internal/render/job.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -24,25 +25,23 @@ type Job struct {
}
// Header returns a header row.
-func (Job) Header(ns string) Header {
- h := Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "VS", VS: true},
- HeaderColumn{Name: "COMPLETIONS"},
- HeaderColumn{Name: "DURATION"},
- HeaderColumn{Name: "SELECTOR", Wide: true},
- HeaderColumn{Name: "CONTAINERS", Wide: true},
- HeaderColumn{Name: "IMAGES", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (Job) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "VS", VS: true},
+ model1.HeaderColumn{Name: "COMPLETIONS"},
+ model1.HeaderColumn{Name: "DURATION"},
+ model1.HeaderColumn{Name: "SELECTOR", Wide: true},
+ model1.HeaderColumn{Name: "CONTAINERS", Wide: true},
+ model1.HeaderColumn{Name: "IMAGES", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
-
- return h
}
// Render renders a K8s resource to screen.
-func (j Job) Render(o interface{}, ns string, r *Row) error {
+func (j Job) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected Job, but got %T", o)
@@ -57,7 +56,7 @@ func (j Job) Render(o interface{}, ns string, r *Row) error {
cc, ii := toContainers(job.Spec.Template.Spec)
r.ID = client.MetaFQN(job.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
job.Namespace,
job.Name,
computeVulScore(job.ObjectMeta, &job.Spec.Template.Spec),
diff --git a/internal/render/job_test.go b/internal/render/job_test.go
index b2617996..028a4ddf 100644
--- a/internal/render/job_test.go
+++ b/internal/render/job_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestJobRender(t *testing.T) {
c := render.Job{}
- r := render.NewRow(4)
+ r := model1.NewRow(4)
assert.NoError(t, c.Render(load(t, "job"), "", &r))
assert.Equal(t, "default/hello-1567179180", r.ID)
- assert.Equal(t, render.Fields{"default", "hello-1567179180", "0", "1/1", "8s", "controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218", "c1", "blang/busybox-bash"}, r.Fields[:8])
+ assert.Equal(t, model1.Fields{"default", "hello-1567179180", "0", "1/1", "8s", "controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218", "c1", "blang/busybox-bash"}, r.Fields[:8])
}
diff --git a/internal/render/node.go b/internal/render/node.go
index 96b93581..4a43c43e 100644
--- a/internal/render/node.go
+++ b/internal/render/node.go
@@ -11,6 +11,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tview"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -30,32 +31,32 @@ type Node struct {
}
// Header returns a header row.
-func (Node) Header(_ string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "STATUS"},
- HeaderColumn{Name: "ROLE"},
- HeaderColumn{Name: "ARCH", Wide: true},
- HeaderColumn{Name: "TAINTS"},
- HeaderColumn{Name: "VERSION"},
- HeaderColumn{Name: "KERNEL", Wide: true},
- HeaderColumn{Name: "INTERNAL-IP", Wide: true},
- HeaderColumn{Name: "EXTERNAL-IP", Wide: true},
- HeaderColumn{Name: "PODS", Align: tview.AlignRight},
- HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "CPU/A", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "MEM/A", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (Node) Header(string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "STATUS"},
+ model1.HeaderColumn{Name: "ROLE"},
+ model1.HeaderColumn{Name: "ARCH", Wide: true},
+ model1.HeaderColumn{Name: "TAINTS"},
+ model1.HeaderColumn{Name: "VERSION"},
+ model1.HeaderColumn{Name: "KERNEL", Wide: true},
+ model1.HeaderColumn{Name: "INTERNAL-IP", Wide: true},
+ model1.HeaderColumn{Name: "EXTERNAL-IP", Wide: true},
+ model1.HeaderColumn{Name: "PODS", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "CPU/A", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "MEM/A", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (n Node) Render(o interface{}, ns string, r *Row) error {
+func (n Node) Render(o interface{}, ns string, r *model1.Row) error {
oo, ok := o.(*NodeWithMetrics)
if !ok {
return fmt.Errorf("expected *NodeAndMetrics, but got %T", o)
@@ -87,7 +88,7 @@ func (n Node) Render(o interface{}, ns string, r *Row) error {
podCount = NAValue
}
r.ID = client.FQN("", na)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
no.Name,
join(statuses, ","),
join(roles, ","),
diff --git a/internal/render/node_test.go b/internal/render/node_test.go
index a276f06b..09fb4a68 100644
--- a/internal/render/node_test.go
+++ b/internal/render/node_test.go
@@ -6,6 +6,7 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -19,12 +20,12 @@ func TestNodeRender(t *testing.T) {
}
var no render.Node
- r := render.NewRow(14)
+ r := model1.NewRow(14)
err := no.Render(&pom, "", &r)
assert.Nil(t, err)
assert.Equal(t, "minikube", r.ID)
- e := render.Fields{"minikube", "Ready", "master", "amd64", "0", "v1.15.2", "4.15.0", "192.168.64.107", "", "0", "10", "20", "0", "0", "4000", "7874"}
+ e := model1.Fields{"minikube", "Ready", "master", "amd64", "0", "v1.15.2", "4.15.0", "192.168.64.107", "", "0", "10", "20", "0", "0", "4000", "7874"}
assert.Equal(t, e, r.Fields[:16])
}
@@ -34,7 +35,7 @@ func BenchmarkNodeRender(b *testing.B) {
MX: makeNodeMX("n1", "10m", "10Mi"),
}
var no render.Node
- r := render.NewRow(14)
+ r := model1.NewRow(14)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
diff --git a/internal/render/np.go b/internal/render/np.go
index 26333aeb..8f7bb242 100644
--- a/internal/render/np.go
+++ b/internal/render/np.go
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -20,24 +21,24 @@ type NetworkPolicy struct {
}
// Header returns a header row.
-func (NetworkPolicy) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "ING-SELECTOR", Wide: true},
- HeaderColumn{Name: "ING-PORTS"},
- HeaderColumn{Name: "ING-BLOCK"},
- HeaderColumn{Name: "EGR-SELECTOR", Wide: true},
- HeaderColumn{Name: "EGR-PORTS"},
- HeaderColumn{Name: "EGR-BLOCK"},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (NetworkPolicy) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "ING-SELECTOR", Wide: true},
+ model1.HeaderColumn{Name: "ING-PORTS"},
+ model1.HeaderColumn{Name: "ING-BLOCK"},
+ model1.HeaderColumn{Name: "EGR-SELECTOR", Wide: true},
+ model1.HeaderColumn{Name: "EGR-PORTS"},
+ model1.HeaderColumn{Name: "EGR-BLOCK"},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
+func (n NetworkPolicy) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected NetworkPolicy, but got %T", o)
@@ -52,7 +53,7 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error {
ep, es, eb := egress(np.Spec.Egress)
r.ID = client.MetaFQN(np.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
np.Namespace,
np.Name,
is,
diff --git a/internal/render/np_test.go b/internal/render/np_test.go
index bd3412f2..bd371df4 100644
--- a/internal/render/np_test.go
+++ b/internal/render/np_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestNetworkPolicyRender(t *testing.T) {
c := render.NetworkPolicy{}
- r := render.NewRow(9)
+ r := model1.NewRow(9)
assert.NoError(t, c.Render(load(t, "np"), "", &r))
assert.Equal(t, "default/fred", r.ID)
- assert.Equal(t, render.Fields{"default", "fred", "ns:app=blee,po:app=fred", "TCP:6379", "172.17.0.0/16[172.17.1.0/24,172.17.3.0/24...]", "", "TCP:5978", "10.0.0.0/24"}, r.Fields[:8])
+ assert.Equal(t, model1.Fields{"default", "fred", "ns:app=blee,po:app=fred", "TCP:6379", "172.17.0.0/16[172.17.1.0/24,172.17.3.0/24...]", "", "TCP:5978", "10.0.0.0/24"}, r.Fields[:8])
}
diff --git a/internal/render/ns.go b/internal/render/ns.go
index 77954a5e..4f9ecf81 100644
--- a/internal/render/ns.go
+++ b/internal/render/ns.go
@@ -9,6 +9,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -21,19 +22,17 @@ type Namespace struct {
}
// ColorerFunc colors a resource row.
-func (n Namespace) ColorerFunc() ColorerFunc {
- return func(ns string, h Header, re RowEvent) tcell.Color {
- c := DefaultColorer(ns, h, re)
-
- if re.Kind == EventUpdate {
- c = StdColor
+func (n Namespace) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {
+ c := model1.DefaultColorer(ns, h, re)
+ if c == model1.ErrColor {
+ return c
+ }
+ if re.Kind == model1.EventUpdate {
+ c = model1.StdColor
}
if strings.Contains(strings.TrimSpace(re.Row.Fields[0]), "*") {
- c = HighlightColor
- }
-
- if !Happy(ns, h, re.Row) {
- c = ErrColor
+ c = model1.HighlightColor
}
return c
@@ -41,18 +40,18 @@ func (n Namespace) ColorerFunc() ColorerFunc {
}
// Header returns a header rbw.
-func (Namespace) Header(string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "STATUS"},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (Namespace) Header(string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "STATUS"},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (n Namespace) Render(o interface{}, _ string, r *Row) error {
+func (n Namespace) Render(o interface{}, _ string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected Namespace, but got %T", o)
@@ -64,7 +63,7 @@ func (n Namespace) Render(o interface{}, _ string, r *Row) error {
}
r.ID = client.MetaFQN(ns.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
ns.Name,
string(ns.Status.Phase),
mapToStr(ns.Labels),
diff --git a/internal/render/ns_test.go b/internal/render/ns_test.go
index 81ca793e..ad4337bd 100644
--- a/internal/render/ns_test.go
+++ b/internal/render/ns_test.go
@@ -6,6 +6,7 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/tcell/v2"
"github.com/stretchr/testify/assert"
@@ -13,66 +14,66 @@ import (
func TestNSColorer(t *testing.T) {
uu := map[string]struct {
- re render.RowEvent
+ re model1.RowEvent
e tcell.Color
}{
"add": {
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{
"blee",
"Active",
},
},
},
- e: render.AddColor,
+ e: model1.AddColor,
},
"update": {
- re: render.RowEvent{
- Kind: render.EventUpdate,
- Row: render.Row{
- Fields: render.Fields{
+ re: model1.RowEvent{
+ Kind: model1.EventUpdate,
+ Row: model1.Row{
+ Fields: model1.Fields{
"blee",
"Active",
},
},
},
- e: render.StdColor,
+ e: model1.StdColor,
},
"decorator": {
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{
"blee*",
"Active",
},
},
},
- e: render.HighlightColor,
+ e: model1.HighlightColor,
},
}
- h := render.Header{
- render.HeaderColumn{Name: "NAME"},
- render.HeaderColumn{Name: "STATUS"},
+ h := model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "STATUS"},
}
var r render.Namespace
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, r.ColorerFunc()("", h, u.re))
+ assert.Equal(t, u.e, r.ColorerFunc()("", h, &u.re))
})
}
}
func TestNamespaceRender(t *testing.T) {
c := render.Namespace{}
- r := render.NewRow(3)
+ r := model1.NewRow(3)
assert.NoError(t, c.Render(load(t, "ns"), "-", &r))
assert.Equal(t, "-/kube-system", r.ID)
- assert.Equal(t, render.Fields{"kube-system", "Active"}, r.Fields[:2])
+ assert.Equal(t, model1.Fields{"kube-system", "Active"}, r.Fields[:2])
}
diff --git a/internal/render/pdb.go b/internal/render/pdb.go
index 3b29962d..656b49b8 100644
--- a/internal/render/pdb.go
+++ b/internal/render/pdb.go
@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tview"
v1beta1 "k8s.io/api/policy/v1beta1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -21,24 +22,24 @@ type PodDisruptionBudget struct {
}
// Header returns a header row.
-func (PodDisruptionBudget) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "MIN AVAILABLE", Align: tview.AlignRight},
- HeaderColumn{Name: "MAX UNAVAILABLE", Align: tview.AlignRight},
- HeaderColumn{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight},
- HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
- HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
- HeaderColumn{Name: "EXPECTED", Align: tview.AlignRight},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (PodDisruptionBudget) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "MIN AVAILABLE", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "MAX UNAVAILABLE", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "EXPECTED", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
+func (p PodDisruptionBudget) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected PodDisruptionBudget, but got %T", o)
@@ -50,7 +51,7 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(pdb.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
pdb.Namespace,
pdb.Name,
numbToStr(pdb.Spec.MinAvailable),
diff --git a/internal/render/pdb_test.go b/internal/render/pdb_test.go
index 2f40acae..9567c487 100644
--- a/internal/render/pdb_test.go
+++ b/internal/render/pdb_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestPodDisruptionBudgetRender(t *testing.T) {
c := render.PodDisruptionBudget{}
- r := render.NewRow(9)
+ r := model1.NewRow(9)
assert.NoError(t, c.Render(load(t, "pdb"), "", &r))
assert.Equal(t, "default/fred", r.ID)
- assert.Equal(t, render.Fields{"default", "fred", "2", render.NAValue, "0", "0", "2", "0"}, r.Fields[:8])
+ assert.Equal(t, model1.Fields{"default", "fred", "2", render.NAValue, "0", "0", "2", "0"}, r.Fields[:8])
}
diff --git a/internal/render/pod.go b/internal/render/pod.go
index 710bd6ee..829834df 100644
--- a/internal/render/pod.go
+++ b/internal/render/pod.go
@@ -18,6 +18,7 @@ import (
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
)
const (
@@ -51,84 +52,67 @@ type Pod struct {
}
// ColorerFunc colors a resource row.
-func (p Pod) ColorerFunc() ColorerFunc {
- return func(ns string, h Header, re RowEvent) tcell.Color {
- c := DefaultColorer(ns, h, re)
+func (p Pod) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {
+ c := model1.DefaultColorer(ns, h, re)
- statusCol := h.IndexOf("STATUS", true)
- if statusCol == -1 {
+ idx, ok := h.IndexOf("STATUS", true)
+ if !ok {
return c
}
- status := strings.TrimSpace(re.Row.Fields[statusCol])
+ status := strings.TrimSpace(re.Row.Fields[idx])
switch status {
case Pending, ContainerCreating:
- c = PendingColor
+ c = model1.PendingColor
case PodInitializing:
- c = AddColor
+ c = model1.AddColor
case Initialized:
- c = HighlightColor
+ c = model1.HighlightColor
case Completed:
- c = CompletedColor
+ c = model1.CompletedColor
case Running:
- c = StdColor
- if !Happy(ns, h, re.Row) {
- c = ErrColor
+ if c != model1.ErrColor {
+ c = model1.StdColor
}
case Terminating:
- c = KillColor
- default:
- if !Happy(ns, h, re.Row) {
- c = ErrColor
- }
+ c = model1.KillColor
}
+
return c
}
}
// Header returns a header row.
-func (Pod) Header(ns string) Header {
- h := Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "VS", VS: true},
- HeaderColumn{Name: "PF"},
- HeaderColumn{Name: "READY"},
- HeaderColumn{Name: "STATUS"},
- HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight},
- HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight, Wide: true},
- HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight, Wide: true},
- HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true},
- HeaderColumn{Name: "IP"},
- HeaderColumn{Name: "NODE"},
- HeaderColumn{Name: "NOMINATED NODE", Wide: true},
- HeaderColumn{Name: "READINESS GATES", Wide: true},
- HeaderColumn{Name: "QOS", Wide: true},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (p Pod) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "VS", VS: true},
+ model1.HeaderColumn{Name: "PF"},
+ model1.HeaderColumn{Name: "READY"},
+ model1.HeaderColumn{Name: "STATUS"},
+ model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight, Wide: true},
+ model1.HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight, Wide: true},
+ model1.HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true},
+ model1.HeaderColumn{Name: "IP"},
+ model1.HeaderColumn{Name: "NODE"},
+ model1.HeaderColumn{Name: "NOMINATED NODE", Wide: true},
+ model1.HeaderColumn{Name: "READINESS GATES", Wide: true},
+ model1.HeaderColumn{Name: "QOS", Wide: true},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
-
- return h
-}
-
-// ExtractImages returns a collection of container images.
-// !!BOZO!! If this has any legs?? enable scans on other container types.
-func ExtractImages(spec *v1.PodSpec) []string {
- ii := make([]string, 0, len(spec.Containers))
- for _, c := range spec.Containers {
- ii = append(ii, c.Image)
- }
-
- return ii
}
// Render renders a K8s resource to screen.
-func (p Pod) Render(o interface{}, ns string, row *Row) error {
+func (p Pod) Render(o interface{}, ns string, row *model1.Row) error {
pwm, ok := o.(*PodWithMetrics)
if !ok {
return fmt.Errorf("expected PodWithMetrics, but got %T", o)
@@ -151,9 +135,10 @@ func (p Pod) Render(o interface{}, ns string, row *Row) error {
c, r := gatherCoMX(po.Spec.Containers, ccmx)
phase := p.Phase(&po)
row.ID = client.MetaFQN(po.ObjectMeta)
- row.Fields = Fields{
+
+ row.Fields = model1.Fields{
po.Namespace,
- po.ObjectMeta.Name,
+ po.Name,
computeVulScore(po.ObjectMeta, &po.Spec),
"●",
strconv.Itoa(cr) + "/" + strconv.Itoa(len(po.Spec.Containers)),
diff --git a/internal/render/pod_test.go b/internal/render/pod_test.go
index ec2e058a..57fe6c4d 100644
--- a/internal/render/pod_test.go
+++ b/internal/render/pod_test.go
@@ -6,6 +6,7 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/tcell/v2"
"github.com/stretchr/testify/assert"
@@ -16,128 +17,128 @@ import (
)
func init() {
- render.AddColor = tcell.ColorBlue
- render.HighlightColor = tcell.ColorYellow
- render.CompletedColor = tcell.ColorGray
- render.StdColor = tcell.ColorWhite
- render.ErrColor = tcell.ColorRed
- render.KillColor = tcell.ColorGray
+ model1.AddColor = tcell.ColorBlue
+ model1.HighlightColor = tcell.ColorYellow
+ model1.CompletedColor = tcell.ColorGray
+ model1.StdColor = tcell.ColorWhite
+ model1.ErrColor = tcell.ColorRed
+ model1.KillColor = tcell.ColorGray
}
func TestPodColorer(t *testing.T) {
- stdHeader := render.Header{
- render.HeaderColumn{Name: "NAMESPACE"},
- render.HeaderColumn{Name: "NAME"},
- render.HeaderColumn{Name: "READY"},
- render.HeaderColumn{Name: "RESTARTS"},
- render.HeaderColumn{Name: "STATUS"},
- render.HeaderColumn{Name: "VALID"},
+ stdHeader := model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "READY"},
+ model1.HeaderColumn{Name: "RESTARTS"},
+ model1.HeaderColumn{Name: "STATUS"},
+ model1.HeaderColumn{Name: "VALID"},
}
uu := map[string]struct {
- re render.RowEvent
- h render.Header
+ re model1.RowEvent
+ h model1.Header
e tcell.Color
}{
"valid": {
h: stdHeader,
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{"blee", "fred", "1/1", "0", render.Running, ""},
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Running, ""},
},
},
- e: render.StdColor,
+ e: model1.StdColor,
},
"init": {
h: stdHeader,
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, ""},
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, ""},
},
},
- e: render.AddColor,
+ e: model1.AddColor,
},
"init-err": {
h: stdHeader,
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, "blah"},
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, "blah"},
},
},
- e: render.AddColor,
+ e: model1.AddColor,
},
"initialized": {
h: stdHeader,
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{"blee", "fred", "1/1", "0", render.Initialized, "blah"},
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Initialized, "blah"},
},
},
- e: render.HighlightColor,
+ e: model1.HighlightColor,
},
"completed": {
h: stdHeader,
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{"blee", "fred", "1/1", "0", render.Completed, "blah"},
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Completed, "blah"},
},
},
- e: render.CompletedColor,
+ e: model1.CompletedColor,
},
"terminating": {
h: stdHeader,
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{"blee", "fred", "1/1", "0", render.Terminating, "blah"},
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Terminating, "blah"},
},
},
- e: render.KillColor,
+ e: model1.KillColor,
},
"invalid": {
h: stdHeader,
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{"blee", "fred", "1/1", "0", "Running", "blah"},
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{"blee", "fred", "1/1", "0", "Running", "blah"},
},
},
- e: render.ErrColor,
+ e: model1.ErrColor,
},
"unknown-cool": {
h: stdHeader,
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", ""},
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", ""},
},
},
- e: render.AddColor,
+ e: model1.AddColor,
},
"unknown-err": {
h: stdHeader,
- re: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{
- Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", "doh"},
+ re: model1.RowEvent{
+ Kind: model1.EventAdd,
+ Row: model1.Row{
+ Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", "doh"},
},
},
- e: render.ErrColor,
+ e: model1.ErrColor,
},
"status": {
h: stdHeader[0:3],
- re: render.RowEvent{
- Kind: render.EventDelete,
- Row: render.Row{
- Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", ""},
+ re: model1.RowEvent{
+ Kind: model1.EventDelete,
+ Row: model1.Row{
+ Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", ""},
},
},
- e: render.KillColor,
+ e: model1.KillColor,
},
}
@@ -145,7 +146,7 @@ func TestPodColorer(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, r.ColorerFunc()("", u.h, u.re))
+ assert.Equal(t, u.e, r.ColorerFunc()("", u.h, &u.re))
})
}
}
@@ -157,12 +158,12 @@ func TestPodRender(t *testing.T) {
}
var po render.Pod
- r := render.NewRow(14)
+ r := model1.NewRow(14)
err := po.Render(&pom, "", &r)
assert.Nil(t, err)
assert.Equal(t, "default/nginx", r.ID)
- e := render.Fields{"default", "nginx", "0", "●", "1/1", "Running", "0", "100", "50", "100:0", "70:170", "100", "n/a", "71", "29", "172.17.0.6", "minikube", "", ""}
+ e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Running", "0", "100", "50", "100:0", "70:170", "100", "n/a", "71", "29", "172.17.0.6", "minikube", "", ""}
assert.Equal(t, e, r.Fields[:19])
}
@@ -172,7 +173,7 @@ func BenchmarkPodRender(b *testing.B) {
MX: makePodMX("nginx", "10m", "10Mi"),
}
var po render.Pod
- r := render.NewRow(12)
+ r := model1.NewRow(12)
b.ReportAllocs()
b.ResetTimer()
@@ -188,12 +189,12 @@ func TestPodInitRender(t *testing.T) {
}
var po render.Pod
- r := render.NewRow(14)
+ r := model1.NewRow(14)
err := po.Render(&pom, "", &r)
assert.Nil(t, err)
assert.Equal(t, "default/nginx", r.ID)
- e := render.Fields{"default", "nginx", "0", "●", "1/1", "Init:0/1", "0", "10", "10", "100:0", "70:170", "10", "n/a", "14", "5", "172.17.0.6", "minikube", "", ""}
+ e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Init:0/1", "0", "10", "10", "100:0", "70:170", "10", "n/a", "14", "5", "172.17.0.6", "minikube", "", ""}
assert.Equal(t, e, r.Fields[:19])
}
diff --git a/internal/render/policy.go b/internal/render/policy.go
index e750bcb0..777ecfaa 100644
--- a/internal/render/policy.go
+++ b/internal/render/policy.go
@@ -7,23 +7,24 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
-func rbacVerbHeader() Header {
- return Header{
- HeaderColumn{Name: "GET "},
- HeaderColumn{Name: "LIST "},
- HeaderColumn{Name: "WATCH "},
- HeaderColumn{Name: "CREATE"},
- HeaderColumn{Name: "PATCH "},
- HeaderColumn{Name: "UPDATE"},
- HeaderColumn{Name: "DELETE"},
- HeaderColumn{Name: "DEL-LIST "},
- HeaderColumn{Name: "EXTRAS", Wide: true},
+func rbacVerbHeader() model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "GET "},
+ model1.HeaderColumn{Name: "LIST "},
+ model1.HeaderColumn{Name: "WATCH "},
+ model1.HeaderColumn{Name: "CREATE"},
+ model1.HeaderColumn{Name: "PATCH "},
+ model1.HeaderColumn{Name: "UPDATE"},
+ model1.HeaderColumn{Name: "DELETE"},
+ model1.HeaderColumn{Name: "DEL-LIST "},
+ model1.HeaderColumn{Name: "EXTRAS", Wide: true},
}
}
@@ -33,28 +34,28 @@ type Policy struct {
}
// ColorerFunc colors a resource row.
-func (Policy) ColorerFunc() ColorerFunc {
- return func(ns string, _ Header, re RowEvent) tcell.Color {
+func (Policy) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color {
return tcell.ColorMediumSpringGreen
}
}
// Header returns a header row.
-func (Policy) Header(ns string) Header {
- h := Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "API-GROUP"},
- HeaderColumn{Name: "BINDING"},
+func (Policy) Header(ns string) model1.Header {
+ h := model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "API-GROUP"},
+ model1.HeaderColumn{Name: "BINDING"},
}
h = append(h, rbacVerbHeader()...)
- h = append(h, HeaderColumn{Name: "VALID", Wide: true})
+ h = append(h, model1.HeaderColumn{Name: "VALID", Wide: true})
return h
}
// Render renders a K8s resource to screen.
-func (Policy) Render(o interface{}, gvr string, r *Row) error {
+func (Policy) Render(o interface{}, gvr string, r *model1.Row) error {
p, ok := o.(PolicyRes)
if !ok {
return fmt.Errorf("expecting PolicyRes but got %T", o)
diff --git a/internal/render/policy_test.go b/internal/render/policy_test.go
index 536425fb..b0770d14 100644
--- a/internal/render/policy_test.go
+++ b/internal/render/policy_test.go
@@ -7,6 +7,7 @@ import (
"errors"
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
@@ -46,7 +47,7 @@ func TestPolicyResMerge(t *testing.T) {
func TestPolicyRender(t *testing.T) {
var p render.Policy
- var r render.Row
+ var r model1.Row
o := render.PolicyRes{
Namespace: "blee",
Binding: "fred",
@@ -59,7 +60,7 @@ func TestPolicyRender(t *testing.T) {
assert.Nil(t, p.Render(o, "fred", &r))
assert.Equal(t, "blee/res", r.ID)
- assert.Equal(t, render.Fields{
+ assert.Equal(t, model1.Fields{
"blee",
"res",
"grp",
diff --git a/internal/render/popeye.go b/internal/render/popeye.go
index c8dd757d..e43df218 100644
--- a/internal/render/popeye.go
+++ b/internal/render/popeye.go
@@ -3,92 +3,82 @@
package render
-import (
- "fmt"
- "math"
- "strconv"
- "strings"
+import "github.com/derailed/popeye/pkg/config"
- "github.com/derailed/k9s/internal/client"
- "github.com/derailed/popeye/pkg/config"
- "github.com/derailed/tcell/v2"
- "github.com/derailed/tview"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
-)
+// !!BOZO!! Popeye
-// Popeye renders a sanitizer to screen.
-type Popeye struct {
- Base
-}
+// // 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 RowEvent) tcell.Color {
- c := DefaultColorer(ns, h, re)
+// // 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
- }
-}
+// 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) Header {
- return Header{
- HeaderColumn{Name: "RESOURCE"},
- HeaderColumn{Name: "SCORE%", Align: tview.AlignRight},
- HeaderColumn{Name: "SCANNED", Align: tview.AlignRight},
- HeaderColumn{Name: "ERROR", Align: tview.AlignRight},
- HeaderColumn{Name: "WARNING", Align: tview.AlignRight},
- HeaderColumn{Name: "INFO", Align: tview.AlignRight},
- HeaderColumn{Name: "OK", Align: tview.AlignRight},
- }
-}
+// // 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 *Row) error {
- s, ok := o.(Section)
- if !ok {
- return fmt.Errorf("expected Section, but got %T", o)
- }
+// // 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
-}
+// 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...
+// // ----------------------------------------------------------------------------
+// // Helpers...
type (
- // Builder represents a popeye report.
- Builder struct {
- Report Report `json:"popeye" yaml:"popeye"`
- }
+ // // 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"`
- }
+ // // 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
@@ -116,89 +106,90 @@ type (
}
// 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
-}
+// // 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))
-}
+// // 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))
-}
+// 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)
-}
+// // 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]
-}
+// // 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()
-}
+// // 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
-}
+// // 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
-}
+// // 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
- }
- }
+// // 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
-}
+// 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
- }
- }
+// // 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
-}
+// 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++
- }
- }
+// // 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
-}
+// return count
+// }
diff --git a/internal/render/port_forward_test.go b/internal/render/port_forward_test.go
index c3d0d9c2..6c4cd188 100644
--- a/internal/render/port_forward_test.go
+++ b/internal/render/port_forward_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"time"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
@@ -23,10 +24,10 @@ func TestPortForwardRender(t *testing.T) {
}
var p render.PortForward
- var r render.Row
+ var r model1.Row
assert.Nil(t, p.Render(o, "fred", &r))
assert.Equal(t, "blee/fred", r.ID)
- assert.Equal(t, render.Fields{
+ assert.Equal(t, model1.Fields{
"blee",
"fred",
"co",
diff --git a/internal/render/portforward.go b/internal/render/portforward.go
index 5ae2f71e..267be33c 100644
--- a/internal/render/portforward.go
+++ b/internal/render/portforward.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -39,29 +40,29 @@ type PortForward struct {
}
// ColorerFunc colors a resource row.
-func (PortForward) ColorerFunc() ColorerFunc {
- return func(ns string, _ Header, re RowEvent) tcell.Color {
+func (PortForward) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color {
return tcell.ColorSkyblue
}
}
// Header returns a header row.
-func (PortForward) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "CONTAINER"},
- HeaderColumn{Name: "PORTS"},
- HeaderColumn{Name: "URL"},
- HeaderColumn{Name: "C"},
- HeaderColumn{Name: "N"},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (PortForward) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "CONTAINER"},
+ model1.HeaderColumn{Name: "PORTS"},
+ model1.HeaderColumn{Name: "URL"},
+ model1.HeaderColumn{Name: "C"},
+ model1.HeaderColumn{Name: "N"},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
+func (f PortForward) Render(o interface{}, gvr string, r *model1.Row) error {
pf, ok := o.(ForwardRes)
if !ok {
return fmt.Errorf("expecting a ForwardRes but got %T", o)
@@ -71,7 +72,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error {
r.ID = pf.ID()
ns, n := client.Namespaced(r.ID)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
ns,
trimContainer(n),
pf.Container(),
diff --git a/internal/render/pv.go b/internal/render/pv.go
index d91ea18b..9e428937 100644
--- a/internal/render/pv.go
+++ b/internal/render/pv.go
@@ -9,6 +9,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -23,51 +24,49 @@ type PersistentVolume struct {
}
// ColorerFunc colors a resource row.
-func (p PersistentVolume) ColorerFunc() ColorerFunc {
- return func(ns string, h Header, re RowEvent) tcell.Color {
- if !Happy(ns, h, re.Row) {
- return ErrColor
- }
+func (p PersistentVolume) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {
+ c := model1.DefaultColorer(ns, h, re)
- statusCol := h.IndexOf("STATUS", true)
- if statusCol == -1 {
- return DefaultColorer(ns, h, re)
+ idx, ok := h.IndexOf("STATUS", true)
+ if ok {
+ return c
}
- switch strings.TrimSpace(re.Row.Fields[statusCol]) {
+ switch strings.TrimSpace(re.Row.Fields[idx]) {
case string(v1.VolumeBound):
- return StdColor
+ return model1.StdColor
case string(v1.VolumeAvailable):
return tcell.ColorGreen
case string(v1.VolumePending):
- return PendingColor
+ return model1.PendingColor
case terminatingPhase:
- return CompletedColor
+ return model1.CompletedColor
}
- return DefaultColorer(ns, h, re)
+ return c
}
}
// Header returns a header rbw.
-func (PersistentVolume) Header(string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "CAPACITY", Capacity: true},
- HeaderColumn{Name: "ACCESS MODES"},
- HeaderColumn{Name: "RECLAIM POLICY"},
- HeaderColumn{Name: "STATUS"},
- HeaderColumn{Name: "CLAIM"},
- HeaderColumn{Name: "STORAGECLASS"},
- HeaderColumn{Name: "REASON"},
- HeaderColumn{Name: "VOLUMEMODE", Wide: true},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (PersistentVolume) Header(string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "CAPACITY", Capacity: true},
+ model1.HeaderColumn{Name: "ACCESS MODES"},
+ model1.HeaderColumn{Name: "RECLAIM POLICY"},
+ model1.HeaderColumn{Name: "STATUS"},
+ model1.HeaderColumn{Name: "CLAIM"},
+ model1.HeaderColumn{Name: "STORAGECLASS"},
+ model1.HeaderColumn{Name: "REASON"},
+ model1.HeaderColumn{Name: "VOLUMEMODE", Wide: true},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error {
+func (p PersistentVolume) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected PersistentVolume, but got %T", o)
@@ -94,7 +93,7 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error {
size := pv.Spec.Capacity[v1.ResourceStorage]
r.ID = client.MetaFQN(pv.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
pv.Name,
size.String(),
accessMode(pv.Spec.AccessModes),
diff --git a/internal/render/pv_test.go b/internal/render/pv_test.go
index 93f77fb5..615fd8b6 100644
--- a/internal/render/pv_test.go
+++ b/internal/render/pv_test.go
@@ -6,24 +6,25 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestPersistentVolumeRender(t *testing.T) {
c := render.PersistentVolume{}
- r := render.NewRow(9)
+ r := model1.NewRow(9)
assert.NoError(t, c.Render(load(t, "pv"), "-", &r))
assert.Equal(t, "-/pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", r.ID)
- assert.Equal(t, render.Fields{"pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", "1Gi", "RWO", "Delete", "Bound", "default/www-nginx-sts-1", "standard"}, r.Fields[:7])
+ assert.Equal(t, model1.Fields{"pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", "1Gi", "RWO", "Delete", "Bound", "default/www-nginx-sts-1", "standard"}, r.Fields[:7])
}
func TestTerminatingPersistentVolumeRender(t *testing.T) {
c := render.PersistentVolume{}
- r := render.NewRow(9)
+ r := model1.NewRow(9)
assert.NoError(t, c.Render(load(t, "pv_terminating"), "-", &r))
assert.Equal(t, "-/pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", r.ID)
- assert.Equal(t, render.Fields{"pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", "1Gi", "RWO", "Delete", "Terminating", "default/www-nginx-sts-2", "standard"}, r.Fields[:7])
+ assert.Equal(t, model1.Fields{"pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", "1Gi", "RWO", "Delete", "Terminating", "default/www-nginx-sts-2", "standard"}, r.Fields[:7])
}
diff --git a/internal/render/pvc.go b/internal/render/pvc.go
index bd3cc43b..79678346 100644
--- a/internal/render/pvc.go
+++ b/internal/render/pvc.go
@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -18,23 +19,23 @@ type PersistentVolumeClaim struct {
}
// Header returns a header rbw.
-func (PersistentVolumeClaim) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "STATUS"},
- HeaderColumn{Name: "VOLUME"},
- HeaderColumn{Name: "CAPACITY", Capacity: true},
- HeaderColumn{Name: "ACCESS MODES"},
- HeaderColumn{Name: "STORAGECLASS"},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (PersistentVolumeClaim) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "STATUS"},
+ model1.HeaderColumn{Name: "VOLUME"},
+ model1.HeaderColumn{Name: "CAPACITY", Capacity: true},
+ model1.HeaderColumn{Name: "ACCESS MODES"},
+ model1.HeaderColumn{Name: "STORAGECLASS"},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
+func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected PersistentVolumeClaim, but got %T", o)
@@ -65,7 +66,7 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(pvc.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
pvc.Namespace,
pvc.Name,
string(phase),
diff --git a/internal/render/pvc_test.go b/internal/render/pvc_test.go
index c1005cb1..ec85c2e1 100644
--- a/internal/render/pvc_test.go
+++ b/internal/render/pvc_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestPersistentVolumeClaimRender(t *testing.T) {
c := render.PersistentVolumeClaim{}
- r := render.NewRow(8)
+ r := model1.NewRow(8)
assert.NoError(t, c.Render(load(t, "pvc"), "", &r))
assert.Equal(t, "default/www-nginx-sts-0", r.ID)
- assert.Equal(t, render.Fields{"default", "www-nginx-sts-0", "Bound", "pvc-fbabd470-8725-11e9-a8e8-42010a80015b", "1Gi", "RWO", "standard"}, r.Fields[:7])
+ assert.Equal(t, model1.Fields{"default", "www-nginx-sts-0", "Bound", "pvc-fbabd470-8725-11e9-a8e8-42010a80015b", "1Gi", "RWO", "standard"}, r.Fields[:7])
}
diff --git a/internal/render/rbac.go b/internal/render/rbac.go
index ec23c8dd..12ad96e7 100644
--- a/internal/render/rbac.go
+++ b/internal/render/rbac.go
@@ -7,6 +7,7 @@ import (
"fmt"
"strings"
+ "github.com/derailed/k9s/internal/model1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
@@ -37,31 +38,31 @@ type Rbac struct {
}
// ColorerFunc colors a resource row.
-func (Rbac) ColorerFunc() ColorerFunc {
- return DefaultColorer
+func (Rbac) ColorerFunc() model1.ColorerFunc {
+ return model1.DefaultColorer
}
// Header returns a header row.
-func (Rbac) Header(ns string) Header {
- h := make(Header, 0, 10)
+func (Rbac) Header(ns string) model1.Header {
+ h := make(model1.Header, 0, 10)
h = append(h,
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "API-GROUP"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "API-GROUP"},
)
h = append(h, rbacVerbHeader()...)
- return append(h, HeaderColumn{Name: "VALID", Wide: true})
+ return append(h, model1.HeaderColumn{Name: "VALID", Wide: true})
}
// Render renders a K8s resource to screen.
-func (r Rbac) Render(o interface{}, ns string, ro *Row) error {
+func (r Rbac) Render(o interface{}, ns string, ro *model1.Row) error {
p, ok := o.(PolicyRes)
if !ok {
return fmt.Errorf("expecting RuleRes but got %T", o)
}
ro.ID = p.Resource
- ro.Fields = make(Fields, 0, len(r.Header(ns)))
+ ro.Fields = make(model1.Fields, 0, len(r.Header(ns)))
ro.Fields = append(ro.Fields,
cleanseResource(p.Resource),
p.Group,
diff --git a/internal/render/reference.go b/internal/render/reference.go
index 31695438..21dec9d7 100644
--- a/internal/render/reference.go
+++ b/internal/render/reference.go
@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -18,24 +19,24 @@ type Reference struct {
}
// ColorerFunc colors a resource row.
-func (Reference) ColorerFunc() ColorerFunc {
- return func(ns string, _ Header, re RowEvent) tcell.Color {
+func (Reference) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color {
return tcell.ColorCadetBlue
}
}
// Header returns a header row.
-func (Reference) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "GVR"},
+func (Reference) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "GVR"},
}
}
// Render renders a K8s resource to screen.
// BOZO!! Pass in a row with pre-alloc fields??
-func (Reference) Render(o interface{}, ns string, r *Row) error {
+func (Reference) Render(o interface{}, ns string, r *model1.Row) error {
ref, ok := o.(ReferenceRes)
if !ok {
return fmt.Errorf("expected ReferenceRes, but got %T", o)
diff --git a/internal/render/reference_test.go b/internal/render/reference_test.go
index 50654c1a..46aaf700 100644
--- a/internal/render/reference_test.go
+++ b/internal/render/reference_test.go
@@ -6,6 +6,7 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
@@ -19,11 +20,11 @@ func TestReferenceRender(t *testing.T) {
var (
ref = render.Reference{}
- r render.Row
+ r model1.Row
)
assert.Nil(t, ref.Render(o, "fred", &r))
assert.Equal(t, "ns1/blee", r.ID)
- assert.Equal(t, render.Fields{
+ assert.Equal(t, model1.Fields{
"ns1",
"blee",
"v1/secrets",
diff --git a/internal/render/ro.go b/internal/render/ro.go
index 3e901646..7b3ce315 100644
--- a/internal/render/ro.go
+++ b/internal/render/ro.go
@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -18,22 +19,22 @@ type Role struct {
}
// Header returns a header row.
-func (Role) Header(ns string) Header {
- var h Header
+func (Role) Header(ns string) model1.Header {
+ var h model1.Header
if client.IsAllNamespaces(ns) {
- h = append(h, HeaderColumn{Name: "NAMESPACE"})
+ h = append(h, model1.HeaderColumn{Name: "NAMESPACE"})
}
return append(h,
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
)
}
// Render renders a K8s resource to screen.
-func (r Role) Render(o interface{}, ns string, row *Row) error {
+func (r Role) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected Role, but got %T", o)
@@ -45,7 +46,7 @@ func (r Role) Render(o interface{}, ns string, row *Row) error {
}
row.ID = client.MetaFQN(ro.ObjectMeta)
- row.Fields = make(Fields, 0, len(r.Header(ns)))
+ row.Fields = make(model1.Fields, 0, len(r.Header(ns)))
if client.IsAllNamespaces(ns) {
row.Fields = append(row.Fields, ro.Namespace)
}
diff --git a/internal/render/ro_test.go b/internal/render/ro_test.go
index 1e0e4cc5..5beb907d 100644
--- a/internal/render/ro_test.go
+++ b/internal/render/ro_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestRoleRender(t *testing.T) {
c := render.Role{}
- r := render.NewRow(3)
+ r := model1.NewRow(3)
assert.NoError(t, c.Render(load(t, "ro"), "", &r))
assert.Equal(t, "default/blee", r.ID)
- assert.Equal(t, render.Fields{"default", "blee"}, r.Fields[:2])
+ assert.Equal(t, model1.Fields{"default", "blee"}, r.Fields[:2])
}
diff --git a/internal/render/rob.go b/internal/render/rob.go
index 46783a88..1f58fd60 100644
--- a/internal/render/rob.go
+++ b/internal/render/rob.go
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -19,25 +20,25 @@ type RoleBinding struct {
}
// Header returns a header rbw.
-func (RoleBinding) Header(ns string) Header {
- var h Header
+func (RoleBinding) Header(ns string) model1.Header {
+ var h model1.Header
if client.IsAllNamespaces(ns) {
- h = append(h, HeaderColumn{Name: "NAMESPACE"})
+ h = append(h, model1.HeaderColumn{Name: "NAMESPACE"})
}
return append(h,
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "ROLE"},
- HeaderColumn{Name: "KIND"},
- HeaderColumn{Name: "SUBJECTS"},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "ROLE"},
+ model1.HeaderColumn{Name: "KIND"},
+ model1.HeaderColumn{Name: "SUBJECTS"},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
)
}
// Render renders a K8s resource to screen.
-func (r RoleBinding) Render(o interface{}, ns string, row *Row) error {
+func (r RoleBinding) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected RoleBinding, but got %T", o)
@@ -51,7 +52,7 @@ func (r RoleBinding) Render(o interface{}, ns string, row *Row) error {
kind, ss := renderSubjects(rb.Subjects)
row.ID = client.MetaFQN(rb.ObjectMeta)
- row.Fields = make(Fields, 0, len(r.Header(ns)))
+ row.Fields = make(model1.Fields, 0, len(r.Header(ns)))
if client.IsAllNamespaces(ns) {
row.Fields = append(row.Fields, rb.Namespace)
}
diff --git a/internal/render/rob_test.go b/internal/render/rob_test.go
index 306cab30..f18a08bf 100644
--- a/internal/render/rob_test.go
+++ b/internal/render/rob_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestRoleBindingRender(t *testing.T) {
c := render.RoleBinding{}
- r := render.NewRow(6)
+ r := model1.NewRow(6)
assert.NoError(t, c.Render(load(t, "rb"), "", &r))
assert.Equal(t, "default/blee", r.ID)
- assert.Equal(t, render.Fields{"default", "blee", "blee", "SvcAcct", "fernand"}, r.Fields[:5])
+ assert.Equal(t, model1.Fields{"default", "blee", "blee", "SvcAcct", "fernand"}, r.Fields[:5])
}
diff --git a/internal/render/row.go b/internal/render/row.go
deleted file mode 100644
index 858b1198..00000000
--- a/internal/render/row.go
+++ /dev/null
@@ -1,231 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Copyright Authors of K9s
-
-package render
-
-import (
- "reflect"
- "sort"
- "strings"
-
- "github.com/fvbommel/sortorder"
-)
-
-// Fields represents a collection of row fields.
-type Fields []string
-
-// Customize returns a subset of fields.
-func (f Fields) Customize(cols []int, out Fields) {
- for i, c := range cols {
- if c < 0 {
- out[i] = NAValue
- continue
- }
- if c < len(f) {
- out[i] = f[c]
- }
- }
-}
-
-// Diff returns true if fields differ or false otherwise.
-func (f Fields) Diff(ff Fields, ageCol int) bool {
- if ageCol < 0 {
- return !reflect.DeepEqual(f[:len(f)-1], ff[:len(ff)-1])
- }
- if !reflect.DeepEqual(f[:ageCol], ff[:ageCol]) {
- return true
- }
- return !reflect.DeepEqual(f[ageCol+1:], ff[ageCol+1:])
-}
-
-// Clone returns a copy of the fields.
-func (f Fields) Clone() Fields {
- cp := make(Fields, len(f))
- copy(cp, f)
-
- return cp
-}
-
-// ----------------------------------------------------------------------------
-
-// Row represents a collection of columns.
-type Row struct {
- ID string
- Fields Fields
-}
-
-// NewRow returns a new row with initialized fields.
-func NewRow(size int) Row {
- return Row{Fields: make([]string, size)}
-}
-
-// Labelize returns a new row based on labels.
-func (r Row) Labelize(cols []int, labelCol int, labels []string) Row {
- out := NewRow(len(cols) + len(labels))
- for _, col := range cols {
- out.Fields = append(out.Fields, r.Fields[col])
- }
- m := labelize(r.Fields[labelCol])
- for _, label := range labels {
- out.Fields = append(out.Fields, m[label])
- }
-
- return out
-}
-
-// Customize returns a row subset based on given col indices.
-func (r Row) Customize(cols []int) Row {
- out := NewRow(len(cols))
- r.Fields.Customize(cols, out.Fields)
- out.ID = r.ID
-
- return out
-}
-
-// Diff returns true if row differ or false otherwise.
-func (r Row) Diff(ro Row, ageCol int) bool {
- if r.ID != ro.ID {
- return true
- }
- return r.Fields.Diff(ro.Fields, ageCol)
-}
-
-// Clone copies a row.
-func (r Row) Clone() Row {
- return Row{
- ID: r.ID,
- Fields: r.Fields.Clone(),
- }
-}
-
-// Len returns the length of the row.
-func (r Row) Len() int {
- return len(r.Fields)
-}
-
-// ----------------------------------------------------------------------------
-
-// Rows represents a collection of rows.
-type Rows []Row
-
-// Delete removes an element by id.
-func (rr Rows) Delete(id string) Rows {
- idx, ok := rr.Find(id)
- if !ok {
- return rr
- }
-
- if idx == 0 {
- return rr[1:]
- }
- if idx+1 == len(rr) {
- return rr[:len(rr)-1]
- }
-
- return append(rr[:idx], rr[idx+1:]...)
-}
-
-// Upsert adds a new item.
-func (rr Rows) Upsert(r Row) Rows {
- idx, ok := rr.Find(r.ID)
- if !ok {
- return append(rr, r)
- }
- rr[idx] = r
-
- return rr
-}
-
-// Find locates a row by id. Returns false is not found.
-func (rr Rows) Find(id string) (int, bool) {
- for i, r := range rr {
- if r.ID == id {
- return i, true
- }
- }
-
- return 0, false
-}
-
-// Sort rows based on column index and order.
-func (rr Rows) Sort(col int, asc, isNum, isDur, isCapacity bool) {
- t := RowSorter{
- Rows: rr,
- Index: col,
- IsNumber: isNum,
- IsDuration: isDur,
- IsCapacity: isCapacity,
- Asc: asc,
- }
- sort.Sort(t)
-}
-
-// ----------------------------------------------------------------------------
-
-// RowSorter sorts rows.
-type RowSorter struct {
- Rows Rows
- Index int
- IsNumber bool
- IsDuration bool
- IsCapacity bool
- Asc bool
-}
-
-func (s RowSorter) Len() int {
- return len(s.Rows)
-}
-
-func (s RowSorter) Swap(i, j int) {
- s.Rows[i], s.Rows[j] = s.Rows[j], s.Rows[i]
-}
-
-func (s RowSorter) Less(i, j int) bool {
- v1, v2 := s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index]
- id1, id2 := s.Rows[i].ID, s.Rows[j].ID
- less := Less(s.IsNumber, s.IsDuration, s.IsCapacity, id1, id2, v1, v2)
- if s.Asc {
- return less
- }
- return !less
-}
-
-// ----------------------------------------------------------------------------
-// Helpers...
-
-// Less return true if c1 <= c2.
-func Less(isNumber, isDuration, isCapacity bool, id1, id2, v1, v2 string) bool {
- var less bool
- switch {
- case isNumber:
- less = lessNumber(v1, v2)
- case isDuration:
- less = lessDuration(v1, v2)
- case isCapacity:
- less = lessCapacity(v1, v2)
- default:
- less = sortorder.NaturalLess(v1, v2)
- }
- if v1 == v2 {
- return sortorder.NaturalLess(id1, id2)
- }
-
- return less
-}
-
-func lessDuration(s1, s2 string) bool {
- d1, d2 := durationToSeconds(s1), durationToSeconds(s2)
- return d1 <= d2
-}
-
-func lessCapacity(s1, s2 string) bool {
- c1, c2 := capacityToNumber(s1), capacityToNumber(s2)
-
- return c1 <= c2
-}
-
-func lessNumber(s1, s2 string) bool {
- v1, v2 := strings.Replace(s1, ",", "", -1), strings.Replace(s2, ",", "", -1)
-
- return sortorder.NaturalLess(v1, v2)
-}
diff --git a/internal/render/row_event_test.go b/internal/render/row_event_test.go
deleted file mode 100644
index d75e1894..00000000
--- a/internal/render/row_event_test.go
+++ /dev/null
@@ -1,539 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Copyright Authors of K9s
-
-package render_test
-
-import (
- "testing"
- "time"
-
- "github.com/derailed/k9s/internal/render"
- "github.com/stretchr/testify/assert"
-)
-
-func TestRowEventCustomize(t *testing.T) {
- uu := map[string]struct {
- re1, e render.RowEvent
- cols []int
- }{
- "empty": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- e: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{}},
- },
- },
- "full": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- e: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- cols: []int{0, 1, 2},
- },
- "deltas": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- Deltas: render.DeltaRow{"a", "b", "c"},
- },
- e: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- Deltas: render.DeltaRow{"a", "b", "c"},
- },
- cols: []int{0, 1, 2},
- },
- "deltas-skip": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- Deltas: render.DeltaRow{"a", "b", "c"},
- },
- e: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"3", "1"}},
- Deltas: render.DeltaRow{"c", "a"},
- },
- cols: []int{2, 0},
- },
- "reverse": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- e: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"3", "2", "1"}},
- },
- cols: []int{2, 1, 0},
- },
- "skip": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- e: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"3", "1"}},
- },
- cols: []int{2, 0},
- },
- "miss": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- e: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"3", "", "1"}},
- },
- cols: []int{2, 10, 0},
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.re1.Customize(u.cols))
- })
- }
-}
-
-func TestRowEventDiff(t *testing.T) {
- uu := map[string]struct {
- re1, re2 render.RowEvent
- e bool
- }{
- "same": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- re2: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- },
- "diff-kind": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- re2: render.RowEvent{
- Kind: render.EventDelete,
- Row: render.Row{ID: "B", Fields: render.Fields{"1", "2", "3"}},
- },
- e: true,
- },
- "diff-delta": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- Deltas: render.DeltaRow{"1", "2", "3"},
- },
- re2: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- Deltas: render.DeltaRow{"10", "2", "3"},
- },
- e: true,
- },
- "diff-id": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- re2: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "B", Fields: render.Fields{"1", "2", "3"}},
- },
- e: true,
- },
- "diff-field": {
- re1: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- },
- re2: render.RowEvent{
- Kind: render.EventAdd,
- Row: render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}},
- },
- e: true,
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.re1.Diff(u.re2, -1))
- })
- }
-}
-
-func TestRowEventsDiff(t *testing.T) {
- uu := map[string]struct {
- re1, re2 render.RowEvents
- ageCol int
- e bool
- }{
- "same": {
- re1: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- re2: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- ageCol: -1,
- },
- "diff-len": {
- re1: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- re2: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- ageCol: -1,
- e: true,
- },
- "diff-id": {
- re1: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- re2: render.RowEvents{
- {Row: render.Row{ID: "D", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- ageCol: -1,
- e: true,
- },
- "diff-order": {
- re1: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- re2: render.RowEvents{
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- ageCol: -1,
- e: true,
- },
- "diff-withAge": {
- re1: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- re2: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "13"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- ageCol: 1,
- e: true,
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.re1.Diff(u.re2, u.ageCol))
- })
- }
-}
-
-func TestRowEventsUpsert(t *testing.T) {
- uu := map[string]struct {
- ee, e render.RowEvents
- re render.RowEvent
- }{
- "add": {
- ee: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- re: render.RowEvent{
- Row: render.Row{ID: "D", Fields: render.Fields{"f1", "f2", "f3"}},
- },
- e: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- {Row: render.Row{ID: "D", Fields: render.Fields{"f1", "f2", "f3"}}},
- },
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.ee.Upsert(u.re))
- })
- }
-}
-
-func TestRowEventsCustomize(t *testing.T) {
- uu := map[string]struct {
- re, e render.RowEvents
- cols []int
- }{
- "same": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- cols: []int{0, 1, 2},
- e: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- "reverse": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- cols: []int{2, 1, 0},
- e: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"3", "2", "1"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"3", "2", "0"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"3", "2", "10"}}},
- },
- },
- "skip": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- cols: []int{1, 0},
- e: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"2", "1"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"2", "0"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"2", "10"}}},
- },
- },
- "missing": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- cols: []int{1, 0, 4},
- e: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"2", "1", ""}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"2", "0", ""}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"2", "10", ""}}},
- },
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.re.Customize(u.cols))
- })
- }
-}
-
-func TestRowEventsDelete(t *testing.T) {
- uu := map[string]struct {
- re render.RowEvents
- id string
- e render.RowEvents
- }{
- "first": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- id: "A",
- e: render.RowEvents{
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- "middle": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- id: "B",
- e: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- "last": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- id: "C",
- e: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- },
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.re.Delete(u.id))
- })
- }
-}
-
-func TestRowEventsSort(t *testing.T) {
- uu := map[string]struct {
- re render.RowEvents
- col int
- duration, num, asc bool
- capacity bool
- e render.RowEvents
- }{
- "age_time": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", testTime().String()}}},
- },
- col: 2,
- asc: true,
- duration: true,
- e: render.RowEvents{
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", testTime().String()}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}},
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}},
- },
- },
- "col0": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- col: 0,
- asc: true,
- e: render.RowEvents{
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- "id_preserve": {
- re: render.RowEvents{
- {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}},
- {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}},
- {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}},
- {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}},
- {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}},
- {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}},
- },
- col: 1,
- asc: true,
- e: render.RowEvents{
- {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}},
- {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}},
- {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}},
- {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}},
- {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}},
- {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}},
- },
- },
- "capacity": {
- re: render.RowEvents{
- {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3", "1Gi"}}},
- {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3", "1.1G"}}},
- {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3", "0.5Ti"}}},
- {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3", "12e6"}}},
- {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3", "1234"}}},
- {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3", "0.1Ei"}}},
- },
- col: 3,
- asc: true,
- capacity: true,
- e: render.RowEvents{
- {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3", "1234"}}},
- {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3", "12e6"}}},
- {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3", "1Gi"}}},
- {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3", "1.1G"}}},
- {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3", "0.5Ti"}}},
- {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3", "0.1Ei"}}},
- },
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- u.re.Sort("", u.col, u.duration, u.num, u.capacity, u.asc)
- assert.Equal(t, u.e, u.re)
- })
- }
-}
-
-func TestRowEventsClone(t *testing.T) {
- uu := map[string]struct {
- r render.RowEvents
- }{
- "empty": {
- r: render.RowEvents{},
- },
- "full": {
- r: makeRowEvents(),
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- c := u.r.Clone()
- assert.Equal(t, len(u.r), len(c))
- if len(u.r) > 0 {
- u.r[0].Row.Fields[0] = "blee"
- assert.Equal(t, "A", c[0].Row.Fields[0])
- }
- })
- }
-}
-
-// Helpers...
-
-func makeRowEvents() render.RowEvents {
- return render.RowEvents{
- {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}},
- {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}},
- {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}},
- {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}},
- {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}},
- {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}},
- }
-}
diff --git a/internal/render/rs.go b/internal/render/rs.go
index 6dd7f38f..85d5dbed 100644
--- a/internal/render/rs.go
+++ b/internal/render/rs.go
@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tview"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -20,29 +21,27 @@ type ReplicaSet struct {
}
// ColorerFunc colors a resource row.
-func (r ReplicaSet) ColorerFunc() ColorerFunc {
- return DefaultColorer
+func (r ReplicaSet) ColorerFunc() model1.ColorerFunc {
+ return model1.DefaultColorer
}
// Header returns a header row.
-func (ReplicaSet) Header(ns string) Header {
- h := Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "VS", VS: true},
- HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
- HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
- HeaderColumn{Name: "READY", Align: tview.AlignRight},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (ReplicaSet) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "VS", VS: true},
+ model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "READY", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
-
- return h
}
// Render renders a K8s resource to screen.
-func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error {
+func (r ReplicaSet) Render(o interface{}, ns string, row *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected ReplicaSet, but got %T", o)
@@ -54,7 +53,7 @@ func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error {
}
row.ID = client.MetaFQN(rs.ObjectMeta)
- row.Fields = Fields{
+ row.Fields = model1.Fields{
rs.Namespace,
rs.Name,
computeVulScore(rs.ObjectMeta, &rs.Spec.Template.Spec),
diff --git a/internal/render/rs_test.go b/internal/render/rs_test.go
index 8a85dc59..7a84cf38 100644
--- a/internal/render/rs_test.go
+++ b/internal/render/rs_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestReplicaSetRender(t *testing.T) {
c := render.ReplicaSet{}
- r := render.NewRow(4)
+ r := model1.NewRow(4)
assert.NoError(t, c.Render(load(t, "rs"), "", &r))
assert.Equal(t, "icx/icx-db-7d4b578979", r.ID)
- assert.Equal(t, render.Fields{"icx", "icx-db-7d4b578979", "0", "1", "1", "1"}, r.Fields[:6])
+ assert.Equal(t, model1.Fields{"icx", "icx-db-7d4b578979", "0", "1", "1", "1"}, r.Fields[:6])
}
diff --git a/internal/render/sa.go b/internal/render/sa.go
index 43f7c898..1f463a4e 100644
--- a/internal/render/sa.go
+++ b/internal/render/sa.go
@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -19,19 +20,19 @@ type ServiceAccount struct {
}
// Header returns a header row.
-func (ServiceAccount) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "SECRET"},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (ServiceAccount) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "SECRET"},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error {
+func (s ServiceAccount) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected ServiceAccount, but got %T", o)
@@ -43,7 +44,7 @@ func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(sa.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
sa.Namespace,
sa.Name,
strconv.Itoa(len(sa.Secrets)),
diff --git a/internal/render/sa_test.go b/internal/render/sa_test.go
index c143bc50..932ee798 100644
--- a/internal/render/sa_test.go
+++ b/internal/render/sa_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestServiceAccountRender(t *testing.T) {
c := render.ServiceAccount{}
- r := render.NewRow(4)
+ r := model1.NewRow(4)
assert.NoError(t, c.Render(load(t, "sa"), "", &r))
assert.Equal(t, "default/blee", r.ID)
- assert.Equal(t, render.Fields{"default", "blee", "2"}, r.Fields[:3])
+ assert.Equal(t, model1.Fields{"default", "blee", "2"}, r.Fields[:3])
}
diff --git a/internal/render/sc.go b/internal/render/sc.go
index d5f5ecaa..f805fb10 100644
--- a/internal/render/sc.go
+++ b/internal/render/sc.go
@@ -7,6 +7,7 @@ import (
"fmt"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
storagev1 "k8s.io/api/storage/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -20,21 +21,21 @@ type StorageClass struct {
}
// Header returns a header row.
-func (StorageClass) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "PROVISIONER"},
- HeaderColumn{Name: "RECLAIMPOLICY"},
- HeaderColumn{Name: "VOLUMEBINDINGMODE"},
- HeaderColumn{Name: "ALLOWVOLUMEEXPANSION"},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (StorageClass) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "PROVISIONER"},
+ model1.HeaderColumn{Name: "RECLAIMPOLICY"},
+ model1.HeaderColumn{Name: "VOLUMEBINDINGMODE"},
+ model1.HeaderColumn{Name: "ALLOWVOLUMEEXPANSION"},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (s StorageClass) Render(o interface{}, ns string, r *Row) error {
+func (s StorageClass) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected StorageClass, but got %T", o)
@@ -46,7 +47,7 @@ func (s StorageClass) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.FQN(client.ClusterScope, sc.ObjectMeta.Name)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
s.nameWithDefault(sc.ObjectMeta),
sc.Provisioner,
strPtrToStr((*string)(sc.ReclaimPolicy)),
diff --git a/internal/render/sc_test.go b/internal/render/sc_test.go
index 004e91c4..c5883136 100644
--- a/internal/render/sc_test.go
+++ b/internal/render/sc_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestStorageClassRender(t *testing.T) {
c := render.StorageClass{}
- r := render.NewRow(4)
+ r := model1.NewRow(4)
assert.NoError(t, c.Render(load(t, "sc"), "", &r))
assert.Equal(t, "-/standard", r.ID)
- assert.Equal(t, render.Fields{"standard (default)", "kubernetes.io/gce-pd", "Delete", "Immediate", "true"}, r.Fields[:5])
+ assert.Equal(t, model1.Fields{"standard (default)", "kubernetes.io/gce-pd", "Delete", "Immediate", "true"}, r.Fields[:5])
}
diff --git a/internal/render/screen_dump.go b/internal/render/screen_dump.go
index 9b237d7a..8193c612 100644
--- a/internal/render/screen_dump.go
+++ b/internal/render/screen_dump.go
@@ -9,6 +9,7 @@ import (
"path/filepath"
"time"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -21,31 +22,31 @@ type ScreenDump struct {
}
// ColorerFunc colors a resource row.
-func (ScreenDump) ColorerFunc() ColorerFunc {
- return func(ns string, _ Header, re RowEvent) tcell.Color {
+func (ScreenDump) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color {
return tcell.ColorNavajoWhite
}
}
// Header returns a header row.
-func (ScreenDump) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "DIR"},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (ScreenDump) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "DIR"},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (b ScreenDump) Render(o interface{}, ns string, r *Row) error {
+func (b ScreenDump) Render(o interface{}, ns string, r *model1.Row) error {
f, ok := o.(FileRes)
if !ok {
return fmt.Errorf("expecting screendumper, but got %T", o)
}
r.ID = filepath.Join(f.Dir, f.File.Name())
- r.Fields = Fields{
+ r.Fields = model1.Fields{
f.File.Name(),
f.Dir,
"",
diff --git a/internal/render/screen_dump_test.go b/internal/render/screen_dump_test.go
index bde7f109..55c82f38 100644
--- a/internal/render/screen_dump_test.go
+++ b/internal/render/screen_dump_test.go
@@ -8,13 +8,14 @@ import (
"testing"
"time"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestScreenDumpRender(t *testing.T) {
var s render.ScreenDump
- var r render.Row
+ var r model1.Row
o := render.FileRes{
File: fileInfo{},
Dir: "fred/blee",
@@ -22,7 +23,7 @@ func TestScreenDumpRender(t *testing.T) {
assert.Nil(t, s.Render(o, "fred", &r))
assert.Equal(t, "fred/blee/bob", r.ID)
- assert.Equal(t, render.Fields{
+ assert.Equal(t, model1.Fields{
"bob",
"fred/blee",
"",
diff --git a/internal/render/secret.go b/internal/render/secret.go
new file mode 100644
index 00000000..d1d3aad9
--- /dev/null
+++ b/internal/render/secret.go
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package render
+
+import (
+ "fmt"
+ "strconv"
+
+ v1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+
+ "github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
+
+// Secret renders a K8s Secret to screen.
+type Secret struct {
+ Base
+}
+
+// Header returns a header rbw.
+func (Secret) Header(string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "TYPE"},
+ model1.HeaderColumn{Name: "DATA"},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
+ }
+}
+
+// Render renders a K8s resource to screen.
+func (n Secret) Render(o interface{}, _ string, r *model1.Row) error {
+ raw, ok := o.(*unstructured.Unstructured)
+ if !ok {
+ return fmt.Errorf("expected Secret, but got %T", o)
+ }
+ var sec v1.Secret
+ err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sec)
+ if err != nil {
+ return err
+ }
+
+ r.ID = client.FQN(sec.Namespace, sec.Name)
+ r.Fields = model1.Fields{
+ sec.Namespace,
+ sec.Name,
+ string(sec.Type),
+ strconv.Itoa(len(sec.Data)),
+ "",
+ ToAge(raw.GetCreationTimestamp()),
+ }
+
+ return nil
+}
diff --git a/internal/render/sts.go b/internal/render/sts.go
index cc6b0523..e35560ce 100644
--- a/internal/render/sts.go
+++ b/internal/render/sts.go
@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -19,26 +20,24 @@ type StatefulSet struct {
}
// Header returns a header row.
-func (StatefulSet) Header(ns string) Header {
- h := Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "VS", VS: true},
- HeaderColumn{Name: "READY"},
- HeaderColumn{Name: "SELECTOR", Wide: true},
- HeaderColumn{Name: "SERVICE"},
- HeaderColumn{Name: "CONTAINERS", Wide: true},
- HeaderColumn{Name: "IMAGES", Wide: true},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (StatefulSet) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "VS", VS: true},
+ model1.HeaderColumn{Name: "READY"},
+ model1.HeaderColumn{Name: "SELECTOR", Wide: true},
+ model1.HeaderColumn{Name: "SERVICE"},
+ model1.HeaderColumn{Name: "CONTAINERS", Wide: true},
+ model1.HeaderColumn{Name: "IMAGES", Wide: true},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
-
- return h
}
// Render renders a K8s resource to screen.
-func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
+func (s StatefulSet) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected StatefulSet, but got %T", o)
@@ -50,7 +49,7 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(sts.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
sts.Namespace,
sts.Name,
computeVulScore(sts.ObjectMeta, &sts.Spec.Template.Spec),
diff --git a/internal/render/sts_test.go b/internal/render/sts_test.go
index 0070d1a8..d8a4edc8 100644
--- a/internal/render/sts_test.go
+++ b/internal/render/sts_test.go
@@ -6,15 +6,16 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestStatefulSetRender(t *testing.T) {
c := render.StatefulSet{}
- r := render.NewRow(4)
+ r := model1.NewRow(4)
assert.Nil(t, c.Render(load(t, "sts"), "", &r))
assert.Equal(t, "default/nginx-sts", r.ID)
- assert.Equal(t, render.Fields{"default", "nginx-sts", "0", "4/4", "app=nginx-sts", "nginx-sts", "nginx", "k8s.gcr.io/nginx-slim:0.8", "app=nginx-sts", ""}, r.Fields[:len(r.Fields)-1])
+ assert.Equal(t, model1.Fields{"default", "nginx-sts", "0", "4/4", "app=nginx-sts", "nginx-sts", "nginx", "k8s.gcr.io/nginx-slim:0.8", "app=nginx-sts", ""}, r.Fields[:len(r.Fields)-1])
}
diff --git a/internal/render/subject.go b/internal/render/subject.go
index b58e0ba7..af3c0a61 100644
--- a/internal/render/subject.go
+++ b/internal/render/subject.go
@@ -6,6 +6,7 @@ package render
import (
"fmt"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -17,31 +18,31 @@ type Subject struct {
}
// ColorerFunc colors a resource row.
-func (Subject) ColorerFunc() ColorerFunc {
- return func(ns string, _ Header, re RowEvent) tcell.Color {
+func (Subject) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color {
return tcell.ColorMediumSpringGreen
}
}
// Header returns a header row.
-func (Subject) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "KIND"},
- HeaderColumn{Name: "FIRST LOCATION"},
- HeaderColumn{Name: "VALID", Wide: true},
+func (Subject) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "KIND"},
+ model1.HeaderColumn{Name: "FIRST LOCATION"},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
}
}
// Render renders a K8s resource to screen.
-func (s Subject) Render(o interface{}, ns string, r *Row) error {
+func (s Subject) Render(o interface{}, ns string, r *model1.Row) error {
res, ok := o.(SubjectRes)
if !ok {
return fmt.Errorf("expected SubjectRes, but got %T", s)
}
r.ID = res.Name
- r.Fields = Fields{
+ r.Fields = model1.Fields{
res.Name,
res.Kind,
res.FirstLocation,
diff --git a/internal/render/svc.go b/internal/render/svc.go
index 2f3cb30c..73081cad 100644
--- a/internal/render/svc.go
+++ b/internal/render/svc.go
@@ -10,6 +10,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -21,23 +22,23 @@ type Service struct {
}
// Header returns a header row.
-func (Service) Header(ns string) Header {
- return Header{
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "TYPE"},
- HeaderColumn{Name: "CLUSTER-IP"},
- HeaderColumn{Name: "EXTERNAL-IP"},
- HeaderColumn{Name: "SELECTOR", Wide: true},
- HeaderColumn{Name: "PORTS", Wide: false},
- HeaderColumn{Name: "LABELS", Wide: true},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (Service) Header(ns string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "TYPE"},
+ model1.HeaderColumn{Name: "CLUSTER-IP"},
+ model1.HeaderColumn{Name: "EXTERNAL-IP"},
+ model1.HeaderColumn{Name: "SELECTOR", Wide: true},
+ model1.HeaderColumn{Name: "PORTS", Wide: false},
+ model1.HeaderColumn{Name: "LABELS", Wide: true},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (s Service) Render(o interface{}, ns string, r *Row) error {
+func (s Service) Render(o interface{}, ns string, r *model1.Row) error {
raw, ok := o.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("expected Service, but got %T", o)
@@ -49,7 +50,7 @@ func (s Service) Render(o interface{}, ns string, r *Row) error {
}
r.ID = client.MetaFQN(svc.ObjectMeta)
- r.Fields = Fields{
+ r.Fields = model1.Fields{
svc.Namespace,
svc.ObjectMeta.Name,
string(svc.Spec.Type),
diff --git a/internal/render/svc_test.go b/internal/render/svc_test.go
index e0b70471..6a1ea62f 100644
--- a/internal/render/svc_test.go
+++ b/internal/render/svc_test.go
@@ -6,22 +6,23 @@ package render_test
import (
"testing"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert"
)
func TestServiceRender(t *testing.T) {
c := render.Service{}
- r := render.NewRow(4)
+ r := model1.NewRow(4)
assert.NoError(t, c.Render(load(t, "svc"), "", &r))
assert.Equal(t, "default/dictionary1", r.ID)
- assert.Equal(t, render.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "", "app=dictionary1", "http:4001►0"}, r.Fields[:7])
+ assert.Equal(t, model1.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "", "app=dictionary1", "http:4001►0"}, r.Fields[:7])
}
func BenchmarkSvcRender(b *testing.B) {
var svc render.Service
- r := render.NewRow(4)
+ r := model1.NewRow(4)
s := load(b, "svc")
b.ResetTimer()
b.ReportAllocs()
diff --git a/internal/render/table_data.go b/internal/render/table_data.go
deleted file mode 100644
index aaf96cf2..00000000
--- a/internal/render/table_data.go
+++ /dev/null
@@ -1,150 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Copyright Authors of K9s
-
-package render
-
-import (
- "sync"
-
- "github.com/derailed/k9s/internal/client"
-)
-
-// TableData tracks a K8s resource for tabular display.
-type TableData struct {
- Header Header
- RowEvents RowEvents
- Namespace string
- mx sync.RWMutex
-}
-
-// NewTableData returns a new table.
-func NewTableData() *TableData {
- return &TableData{}
-}
-
-// Empty checks if there are no entries.
-func (t *TableData) Empty() bool {
- t.mx.RLock()
- defer t.mx.RUnlock()
-
- return len(t.RowEvents) == 0
-}
-
-// Count returns the number of entries.
-func (t *TableData) Count() int {
- t.mx.RLock()
- defer t.mx.RUnlock()
-
- return len(t.RowEvents)
-}
-
-// IndexOfHeader return the index of the header.
-func (t *TableData) IndexOfHeader(h string) int {
- return t.Header.IndexOf(h, false)
-}
-
-// Labelize prints out specific label columns.
-func (t *TableData) Labelize(labels []string) *TableData {
- labelCol := t.Header.IndexOf("LABELS", true)
- cols := []int{0, 1}
- if client.IsNamespaced(t.Namespace) {
- cols = cols[1:]
- }
- data := TableData{
- Namespace: t.Namespace,
- Header: t.Header.Labelize(cols, labelCol, t.RowEvents),
- }
- data.RowEvents = t.RowEvents.Labelize(cols, labelCol, labels)
-
- return &data
-}
-
-// Customize returns a new model with customized column layout.
-func (t *TableData) Customize(cols []string, wide bool) *TableData {
- res := TableData{
- Namespace: t.Namespace,
- Header: t.Header.Customize(cols, wide),
- }
- ids := t.Header.MapIndices(cols, wide)
- res.RowEvents = t.RowEvents.Customize(ids)
-
- return &res
-}
-
-// Clear clears out the entire table.
-func (t *TableData) Clear() {
- t.Header, t.RowEvents = Header{}, RowEvents{}
-}
-
-// Clone returns a copy of the table.
-func (t *TableData) Clone() *TableData {
- return &TableData{
- Header: t.Header.Clone(),
- RowEvents: t.RowEvents.Clone(),
- Namespace: t.Namespace,
- }
-}
-
-// SetHeader sets table header.
-func (t *TableData) SetHeader(ns string, h Header) {
- t.Namespace, t.Header = ns, h
-}
-
-// Update computes row deltas and update the table data.
-func (t *TableData) Update(rows Rows) {
- empty := t.Empty()
- kk := make(map[string]struct{}, len(rows))
- t.mx.Lock()
- {
- var blankDelta DeltaRow
- for _, row := range rows {
- kk[row.ID] = struct{}{}
- if empty {
- t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row))
- continue
- }
- if index, ok := t.RowEvents.FindIndex(row.ID); ok {
- delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header)
- if delta.IsBlank() {
- t.RowEvents[index].Kind, t.RowEvents[index].Deltas = EventUnchanged, blankDelta
- t.RowEvents[index].Row = row
- } else {
- t.RowEvents[index] = NewRowEventWithDeltas(row, delta)
- }
- continue
- }
- t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row))
- }
- }
- t.mx.Unlock()
-
- if !empty {
- t.Delete(kk)
- }
-}
-
-// Delete removes items in cache that are no longer valid.
-func (t *TableData) Delete(newKeys map[string]struct{}) {
- t.mx.Lock()
- {
- var victims []string
- for _, re := range t.RowEvents {
- if _, ok := newKeys[re.Row.ID]; !ok {
- victims = append(victims, re.Row.ID)
- }
- }
- for _, id := range victims {
- t.RowEvents = t.RowEvents.Delete(id)
- }
- }
- t.mx.Unlock()
-}
-
-// Diff checks if two tables are equal.
-func (t *TableData) Diff(t2 *TableData) bool {
- if t2 == nil || t.Namespace != t2.Namespace || t.Header.Diff(t2.Header) {
- return true
- }
-
- return t.RowEvents.Diff(t2.RowEvents, t.Header.IndexOf("AGE", true))
-}
diff --git a/internal/render/table_data_test.go b/internal/render/table_data_test.go
deleted file mode 100644
index 0ddb7c15..00000000
--- a/internal/render/table_data_test.go
+++ /dev/null
@@ -1,396 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-// Copyright Authors of K9s
-
-package render_test
-
-import (
- "testing"
-
- "github.com/derailed/k9s/internal/render"
- "github.com/stretchr/testify/assert"
-)
-
-func TestTableDataCustomize(t *testing.T) {
- uu := map[string]struct {
- t1, e *render.TableData
- cols []string
- wide bool
- }{
- "same": {
- t1: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- cols: []string{"A", "B", "C"},
- e: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- },
- "wide-col": {
- t1: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- cols: []string{"A", "B", "C"},
- e: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: false},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- },
- "wide": {
- t1: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B", Wide: true},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- wide: true,
- cols: []string{"A", "C"},
- e: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "C"},
- render.HeaderColumn{Name: "B", Wide: true},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "3", "2"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "3", "2"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "3", "2"}}},
- },
- },
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.t1.Customize(u.cols, u.wide))
- })
- }
-}
-
-func TestTableDataDiff(t *testing.T) {
- uu := map[string]struct {
- t1, t2 *render.TableData
- e bool
- }{
- "empty": {
- t1: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- e: true,
- },
- "same": {
- t1: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- t2: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- },
- "ns-diff": {
- t1: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- t2: &render.TableData{
- Namespace: "blee",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- e: true,
- },
- "header-diff": {
- t1: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "D"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- t2: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- e: true,
- },
- "row-diff": {
- t1: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- t2: &render.TableData{
- Namespace: "fred",
- Header: render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- },
- RowEvents: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"100", "2", "3"}}},
- },
- },
- e: true,
- },
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.e, u.t1.Diff(u.t2))
- })
- }
-}
-
-func TestTableDataUpdate(t *testing.T) {
- uu := map[string]struct {
- re render.RowEvents
- rr render.Rows
- e render.RowEvents
- }{
- "no-change": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- rr: render.Rows{
- render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}},
- render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
- },
- e: render.RowEvents{
- {Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- "add": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- rr: render.Rows{
- render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}},
- render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
- render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}},
- },
- e: render.RowEvents{
- {Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- {Kind: render.EventAdd, Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- "delete": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- rr: render.Rows{
- render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}},
- render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
- },
- e: render.RowEvents{
- {Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- "update": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- rr: render.Rows{
- render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}},
- render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}},
- render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}},
- },
- e: render.RowEvents{
- {
- Kind: render.EventUpdate,
- Row: render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}},
- Deltas: render.DeltaRow{"1", "", ""},
- },
- {Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- }
-
- var table render.TableData
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- table.RowEvents = u.re
- table.Update(u.rr)
- assert.Equal(t, u.e, table.RowEvents)
- })
- }
-}
-
-func TestTableDataDelete(t *testing.T) {
- uu := map[string]struct {
- re render.RowEvents
- kk map[string]struct{}
- e render.RowEvents
- }{
- "ordered": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- kk: map[string]struct{}{"A": {}, "C": {}},
- e: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- "unordered": {
- re: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- {Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}},
- },
- kk: map[string]struct{}{"C": {}, "A": {}},
- e: render.RowEvents{
- {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
- {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}},
- },
- },
- }
-
- var table render.TableData
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- table.RowEvents = u.re
- table.Delete(u.kk)
- assert.Equal(t, u.e, table.RowEvents)
- })
- }
-}
diff --git a/internal/render/testdata/p1.json b/internal/render/testdata/p1.json
new file mode 100644
index 00000000..ea8d8dad
--- /dev/null
+++ b/internal/render/testdata/p1.json
@@ -0,0 +1,146 @@
+{
+ "apiVersion": "v1",
+ "kind": "Pod",
+ "metadata": {
+ "annotations": {
+ "kubectl.kubernetes.io/restartedAt": "2019-12-31T12:26:47-07:00"
+ },
+ "creationTimestamp": "2019-12-31T19:27:22Z",
+ "generateName": "nginx-7fb78fb6d8-",
+ "labels": {
+ "app": "nginx",
+ "pod-template-hash": "7fb78fb6d8"
+ },
+ "name": "nginx-7fb78fb6d8-2w75j",
+ "namespace": "default",
+ "ownerReferences": [
+ {
+ "apiVersion": "apps/v1",
+ "blockOwnerDeletion": true,
+ "controller": true,
+ "kind": "ReplicaSet",
+ "name": "nginx-7fb78fb6d8",
+ "uid": "7ccd0600-2c03-11ea-883f-42010a800044"
+ }
+ ],
+ "resourceVersion": "87290191",
+ "selfLink": "/api/v1/namespaces/default/pods/nginx-7fb78fb6d8-2w75j",
+ "uid": "91bb1cf2-2c03-11ea-883f-42010a800044"
+ },
+ "spec": {
+ "containers": [
+ {
+ "image": "k8s.gcr.io/nginx-slim:0.8",
+ "imagePullPolicy": "IfNotPresent",
+ "name": "nginx",
+ "ports": [
+ {
+ "containerPort": 80,
+ "protocol": "TCP"
+ }
+ ],
+ "resources": {
+ "limits": {
+ "cpu": "200m",
+ "memory": "20Mi"
+ },
+ "requests": {
+ "cpu": "200m",
+ "memory": "20Mi"
+ }
+ },
+ "terminationMessagePath": "/dev/termination-log",
+ "terminationMessagePolicy": "File",
+ "volumeMounts": [
+ {
+ "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
+ "name": "default-token-dsl46",
+ "readOnly": true
+ }
+ ]
+ }
+ ],
+ "dnsPolicy": "ClusterFirst",
+ "enableServiceLinks": true,
+ "nodeName": "gke-k9s-default-pool-0fa2fb89-lbtf",
+ "priority": 0,
+ "restartPolicy": "Always",
+ "schedulerName": "default-scheduler",
+ "securityContext": {},
+ "serviceAccount": "default",
+ "serviceAccountName": "default",
+ "terminationGracePeriodSeconds": 30,
+ "tolerations": [
+ {
+ "effect": "NoExecute",
+ "key": "node.kubernetes.io/not-ready",
+ "operator": "Exists",
+ "tolerationSeconds": 300
+ },
+ {
+ "effect": "NoExecute",
+ "key": "node.kubernetes.io/unreachable",
+ "operator": "Exists",
+ "tolerationSeconds": 300
+ }
+ ],
+ "volumes": [
+ {
+ "name": "default-token-dsl46",
+ "secret": {
+ "defaultMode": 420,
+ "secretName": "default-token-dsl46"
+ }
+ }
+ ]
+ },
+ "status": {
+ "conditions": [
+ {
+ "lastProbeTime": null,
+ "lastTransitionTime": "2019-12-31T19:27:23Z",
+ "status": "True",
+ "type": "Initialized"
+ },
+ {
+ "lastProbeTime": null,
+ "lastTransitionTime": "2019-12-31T19:27:25Z",
+ "status": "True",
+ "type": "Ready"
+ },
+ {
+ "lastProbeTime": null,
+ "lastTransitionTime": "2019-12-31T19:27:25Z",
+ "status": "True",
+ "type": "ContainersReady"
+ },
+ {
+ "lastProbeTime": null,
+ "lastTransitionTime": "2019-12-31T19:27:22Z",
+ "status": "True",
+ "type": "PodScheduled"
+ }
+ ],
+ "containerStatuses": [
+ {
+ "containerID": "docker://90e0abf7a779dd76d36038883312baed57a8351428a1d6340df3cff698f51809",
+ "image": "k8s.gcr.io/nginx-slim:0.8",
+ "imageID": "docker-pullable://k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52",
+ "lastState": {},
+ "name": "nginx",
+ "ready": true,
+ "restartCount": 0,
+ "state": {
+ "running": {
+ "startedAt": "2019-12-31T19:27:24Z"
+ }
+ }
+ }
+ ],
+ "hostIP": "10.128.0.15",
+ "phase": "Running",
+ "podIP": "10.44.0.229",
+ "qosClass": "Guaranteed",
+ "startTime": "2019-12-31T19:27:23Z"
+ }
+}
\ No newline at end of file
diff --git a/internal/render/workload.go b/internal/render/workload.go
index 250c52ec..7a3a2645 100644
--- a/internal/render/workload.go
+++ b/internal/render/workload.go
@@ -7,6 +7,7 @@ import (
"fmt"
"strings"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/tcell/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -19,17 +20,17 @@ type Workload struct {
}
// ColorerFunc colors a resource row.
-func (n Workload) ColorerFunc() ColorerFunc {
- return func(ns string, h Header, re RowEvent) tcell.Color {
- c := DefaultColorer(ns, h, re)
+func (n Workload) ColorerFunc() model1.ColorerFunc {
+ return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {
+ c := model1.DefaultColorer(ns, h, re)
- statusCol := h.IndexOf("STATUS", true)
- if statusCol == -1 {
+ idx, ok := h.IndexOf("STATUS", true)
+ if !ok {
return c
}
- status := strings.TrimSpace(re.Row.Fields[statusCol])
+ status := strings.TrimSpace(re.Row.Fields[idx])
if status == "DEGRADED" {
- c = PendingColor
+ c = model1.PendingColor
}
return c
@@ -37,27 +38,27 @@ func (n Workload) ColorerFunc() ColorerFunc {
}
// Header returns a header rbw.
-func (Workload) Header(string) Header {
- return Header{
- HeaderColumn{Name: "KIND"},
- HeaderColumn{Name: "NAMESPACE"},
- HeaderColumn{Name: "NAME"},
- HeaderColumn{Name: "STATUS"},
- HeaderColumn{Name: "READY"},
- HeaderColumn{Name: "VALID", Wide: true},
- HeaderColumn{Name: "AGE", Time: true},
+func (Workload) Header(string) model1.Header {
+ return model1.Header{
+ model1.HeaderColumn{Name: "KIND"},
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME"},
+ model1.HeaderColumn{Name: "STATUS"},
+ model1.HeaderColumn{Name: "READY"},
+ model1.HeaderColumn{Name: "VALID", Wide: true},
+ model1.HeaderColumn{Name: "AGE", Time: true},
}
}
// Render renders a K8s resource to screen.
-func (n Workload) Render(o interface{}, _ string, r *Row) error {
+func (n Workload) Render(o interface{}, _ string, r *model1.Row) error {
res, ok := o.(*WorkloadRes)
if !ok {
return fmt.Errorf("expected allRes but got %T", o)
}
r.ID = fmt.Sprintf("%s|%s|%s", res.Row.Cells[0].(string), res.Row.Cells[1].(string), res.Row.Cells[2].(string))
- r.Fields = Fields{
+ r.Fields = model1.Fields{
res.Row.Cells[0].(string),
res.Row.Cells[1].(string),
res.Row.Cells[2].(string),
diff --git a/internal/ui/action.go b/internal/ui/action.go
index 3e4b8efd..f270e67b 100644
--- a/internal/ui/action.go
+++ b/internal/ui/action.go
@@ -5,6 +5,7 @@ package ui
import (
"sort"
+ "sync"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/tcell/v2"
@@ -12,9 +13,13 @@ import (
)
type (
+ // RangeFn represents a range iteration callback.
+ RangeFn func(tcell.Key, KeyAction)
+
// ActionHandler handles a keyboard command.
ActionHandler func(*tcell.EventKey) *tcell.EventKey
+ // ActionOpts tracks various action options.
ActionOpts struct {
Visible bool
Shared bool
@@ -30,8 +35,14 @@ type (
Opts ActionOpts
}
+ // KeyMap tracks key to action mappings.
+ KeyMap map[tcell.Key]KeyAction
+
// KeyActions tracks mappings between keystrokes and actions.
- KeyActions map[tcell.Key]KeyAction
+ KeyActions struct {
+ actions KeyMap
+ mx sync.RWMutex
+ }
)
// NewKeyAction returns a new keyboard action.
@@ -58,53 +69,134 @@ func NewKeyActionWithOpts(d string, a ActionHandler, opts ActionOpts) KeyAction
}
}
-func (a KeyActions) Reset(aa KeyActions) {
- a.Clear()
- a.Add(aa)
+// NewKeyActions returns a new instance.
+func NewKeyActions() *KeyActions {
+ return &KeyActions{
+ actions: make(map[tcell.Key]KeyAction),
+ }
}
-// Add sets up keyboard action listener.
-func (a KeyActions) Add(aa KeyActions) {
+// NewKeyActionsFromMap construct actions from key map.
+func NewKeyActionsFromMap(mm KeyMap) *KeyActions {
+ return &KeyActions{actions: mm}
+}
+
+// Get fetches an action given a key.
+func (a *KeyActions) Get(key tcell.Key) (KeyAction, bool) {
+ a.mx.RLock()
+ defer a.mx.RUnlock()
+
+ v, ok := a.actions[key]
+
+ return v, ok
+}
+
+// Len returns action mapping count.
+func (a *KeyActions) Len() int {
+ a.mx.RLock()
+ defer a.mx.RUnlock()
+
+ return len(a.actions)
+}
+
+// Reset clears out actions.
+func (a *KeyActions) Reset(aa *KeyActions) {
+ a.Clear()
+ a.Merge(aa)
+}
+
+// Range ranges over all actions and triggers a given function.
+func (a *KeyActions) Range(f RangeFn) {
+ var km KeyMap
+ a.mx.RLock()
+ {
+ km = a.actions
+ }
+ a.mx.RUnlock()
+
+ for k, v := range km {
+ f(k, v)
+ }
+}
+
+// Add adds a new key action.
+func (a *KeyActions) Add(k tcell.Key, ka KeyAction) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ a.actions[k] = ka
+}
+
+// Bulk bulk insert key mappings.
+func (a *KeyActions) Bulk(aa KeyMap) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
for k, v := range aa {
- a[k] = v
+ a.actions[k] = v
+ }
+}
+
+// Merge merges given actions into existing set.
+func (a *KeyActions) Merge(aa *KeyActions) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ for k, v := range aa.actions {
+ a.actions[k] = v
}
}
// Clear remove all actions.
-func (a KeyActions) Clear() {
- for k := range a {
- delete(a, k)
+func (a *KeyActions) Clear() {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ for k := range a.actions {
+ delete(a.actions, k)
}
}
// ClearDanger remove all dangerous actions.
-func (a KeyActions) ClearDanger() {
- for k, v := range a {
+func (a *KeyActions) ClearDanger() {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ for k, v := range a.actions {
if v.Opts.Dangerous {
- delete(a, k)
+ delete(a.actions, k)
}
}
}
// Set replace actions with new ones.
-func (a KeyActions) Set(aa KeyActions) {
- for k, v := range aa {
- a[k] = v
+func (a *KeyActions) Set(aa *KeyActions) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ for k, v := range aa.actions {
+ a.actions[k] = v
}
}
// Delete deletes actions by the given keys.
-func (a KeyActions) Delete(kk ...tcell.Key) {
+func (a *KeyActions) Delete(kk ...tcell.Key) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
for _, k := range kk {
- delete(a, k)
+ delete(a.actions, k)
}
}
// Hints returns a collection of hints.
-func (a KeyActions) Hints() model.MenuHints {
- kk := make([]int, 0, len(a))
- for k := range a {
- if !a[k].Opts.Shared {
+func (a *KeyActions) Hints() model.MenuHints {
+ a.mx.RLock()
+ defer a.mx.RUnlock()
+
+ kk := make([]int, 0, len(a.actions))
+ for k := range a.actions {
+ if !a.actions[k].Opts.Shared {
kk = append(kk, int(k))
}
}
@@ -116,13 +208,14 @@ func (a KeyActions) Hints() model.MenuHints {
hh = append(hh,
model.MenuHint{
Mnemonic: name,
- Description: a[tcell.Key(k)].Description,
- Visible: a[tcell.Key(k)].Opts.Visible,
+ Description: a.actions[tcell.Key(k)].Description,
+ Visible: a.actions[tcell.Key(k)].Opts.Visible,
},
)
} else {
log.Error().Msgf("Unable to locate KeyName for %#v", k)
}
}
+
return hh
}
diff --git a/internal/ui/action_test.go b/internal/ui/action_test.go
index de031ebd..60733aa8 100644
--- a/internal/ui/action_test.go
+++ b/internal/ui/action_test.go
@@ -12,11 +12,11 @@ import (
)
func TestKeyActionsHints(t *testing.T) {
- kk := ui.KeyActions{
+ kk := ui.NewKeyActionsFromMap(ui.KeyMap{
ui.KeyF: ui.NewKeyAction("fred", nil, true),
ui.KeyB: ui.NewKeyAction("blee", nil, true),
ui.KeyZ: ui.NewKeyAction("zorg", nil, false),
- }
+ })
hh := kk.Hints()
diff --git a/internal/ui/app.go b/internal/ui/app.go
index 4a3b3e27..e0c261ee 100644
--- a/internal/ui/app.go
+++ b/internal/ui/app.go
@@ -22,7 +22,7 @@ type App struct {
Main *Pages
flash *model.Flash
- actions KeyActions
+ actions *KeyActions
views map[string]tview.Primitive
cmdBuff *model.FishBuff
running bool
@@ -33,7 +33,7 @@ type App struct {
func NewApp(cfg *config.Config, context string) *App {
a := App{
Application: tview.NewApplication(),
- actions: make(KeyActions),
+ actions: NewKeyActions(),
Configurator: Configurator{Config: cfg, Styles: config.NewStyles()},
Main: NewPages(),
flash: model.NewFlash(model.DefaultFlashDelay),
@@ -139,13 +139,14 @@ func (a *App) Conn() client.Connection {
}
func (a *App) bindKeys() {
- a.actions = KeyActions{
+ a.actions = NewKeyActionsFromMap(KeyMap{
KeyColon: NewKeyAction("Cmd", a.activateCmd, false),
tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false),
+ tcell.KeyCtrlP: NewKeyAction("Persist", a.saveCmd, false),
tcell.KeyCtrlC: NewKeyAction("Quit", a.quitCmd, false),
tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false),
tcell.KeyCtrlQ: NewSharedKeyAction("Clear Filter", a.clearCmd, false),
- }
+ })
}
// BailOut exits the application.
@@ -156,6 +157,7 @@ func (a *App) BailOut() {
// ResetPrompt reset the prompt model and marks buffer as active.
func (a *App) ResetPrompt(m PromptModel) {
+ m.ClearText(false)
a.Prompt().SetModel(m)
a.SetFocus(a.Prompt())
m.SetActive(true)
@@ -166,6 +168,15 @@ func (a *App) ResetCmd() {
a.cmdBuff.Reset()
}
+func (a *App) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
+ if err := a.Config.Save(true); err != nil {
+ a.Flash().Err(err)
+ }
+ a.Flash().Info("current context config saved")
+
+ return nil
+}
+
// ActivateCmd toggle command mode.
func (a *App) ActivateCmd(b bool) {
a.cmdBuff.SetActive(b)
@@ -206,20 +217,17 @@ func (a *App) InCmdMode() bool {
// HasAction checks if key matches a registered binding.
func (a *App) HasAction(key tcell.Key) (KeyAction, bool) {
- act, ok := a.actions[key]
- return act, ok
+ return a.actions.Get(key)
}
// GetActions returns a collection of actions.
-func (a *App) GetActions() KeyActions {
+func (a *App) GetActions() *KeyActions {
return a.actions
}
// AddActions returns the application actions.
-func (a *App) AddActions(aa KeyActions) {
- for k, v := range aa {
- a.actions[k] = v
- }
+func (a *App) AddActions(aa *KeyActions) {
+ a.actions.Merge(aa)
}
// Views return the application root views.
diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go
index 36d5c6ff..47f96fb4 100644
--- a/internal/ui/app_test.go
+++ b/internal/ui/app_test.go
@@ -54,9 +54,9 @@ func TestAppGetActions(t *testing.T) {
a := ui.NewApp(mock.NewMockConfig(), "")
a.Init()
- a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}})
+ a.GetActions().Add(ui.KeyZ, ui.KeyAction{Description: "zorg"})
- assert.Equal(t, 6, len(a.GetActions()))
+ assert.Equal(t, 7, a.GetActions().Len())
}
func TestAppViews(t *testing.T) {
diff --git a/internal/ui/config.go b/internal/ui/config.go
index 70435617..487a1cc9 100644
--- a/internal/ui/config.go
+++ b/internal/ui/config.go
@@ -6,13 +6,14 @@ package ui
import (
"context"
"errors"
+ "io/fs"
"os"
"path/filepath"
"github.com/derailed/k9s/internal/model"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/config"
- "github.com/derailed/k9s/internal/render"
"github.com/fsnotify/fsnotify"
"github.com/rs/zerolog/log"
)
@@ -91,7 +92,7 @@ func (c *Configurator) RefreshCustomViews() error {
// SkinsDirWatcher watches for skin directory file changes.
func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) error {
- if _, err := os.Stat(config.AppSkinsDir); os.IsNotExist(err) {
+ if _, err := os.Stat(config.AppSkinsDir); errors.Is(err, fs.ErrNotExist) {
return err
}
w, err := fsnotify.NewWatcher()
@@ -139,7 +140,7 @@ func (c *Configurator) ConfigWatcher(ctx context.Context, s synchronizer) error
if evt.Has(fsnotify.Create) || evt.Has(fsnotify.Write) {
log.Debug().Msgf("ConfigWatcher file changed: %s", evt.Name)
if evt.Name == config.AppConfigFile {
- if err := c.Config.Load(evt.Name); err != nil {
+ if err := c.Config.Load(evt.Name, false); err != nil {
log.Error().Err(err).Msgf("k9s config reload failed")
s.Flash().Warn("k9s config reload failed. Check k9s logs!")
s.Logo().Warn("K9s config reload failed!")
@@ -190,14 +191,14 @@ func (c *Configurator) activeSkin() (string, bool) {
}
if ct, err := c.Config.K9s.ActiveContext(); err == nil && ct.Skin != "" {
- if _, err := os.Stat(config.SkinFileFromName(ct.Skin)); !os.IsNotExist(err) {
+ if _, err := os.Stat(config.SkinFileFromName(ct.Skin)); err == nil {
skin = ct.Skin
log.Debug().Msgf("[Skin] Loading context skin (%q) from %q", skin, c.Config.K9s.ActiveContextName())
}
}
if sk := c.Config.K9s.UI.Skin; skin == "" && sk != "" {
- if _, err := os.Stat(config.SkinFileFromName(sk)); !os.IsNotExist(err) {
+ if _, err := os.Stat(config.SkinFileFromName(sk)); err == nil {
skin = sk
log.Debug().Msgf("[Skin] Loading global skin (%q)", skin)
}
@@ -272,12 +273,12 @@ func (c *Configurator) updateStyles(f string) {
}
c.Styles.Update()
- render.ModColor = c.Styles.Frame().Status.ModifyColor.Color()
- render.AddColor = c.Styles.Frame().Status.AddColor.Color()
- render.ErrColor = c.Styles.Frame().Status.ErrorColor.Color()
- render.StdColor = c.Styles.Frame().Status.NewColor.Color()
- render.PendingColor = c.Styles.Frame().Status.PendingColor.Color()
- render.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color()
- render.KillColor = c.Styles.Frame().Status.KillColor.Color()
- render.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color()
+ model1.ModColor = c.Styles.Frame().Status.ModifyColor.Color()
+ model1.AddColor = c.Styles.Frame().Status.AddColor.Color()
+ model1.ErrColor = c.Styles.Frame().Status.ErrorColor.Color()
+ model1.StdColor = c.Styles.Frame().Status.NewColor.Color()
+ model1.PendingColor = c.Styles.Frame().Status.PendingColor.Color()
+ model1.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color()
+ model1.KillColor = c.Styles.Frame().Status.KillColor.Color()
+ model1.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color()
}
diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go
index a38d26a1..3e956949 100644
--- a/internal/ui/config_test.go
+++ b/internal/ui/config_test.go
@@ -13,7 +13,7 @@ import (
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/mock"
"github.com/derailed/k9s/internal/model"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tcell/v2"
"github.com/stretchr/testify/assert"
@@ -47,8 +47,8 @@ func TestSkinnedContext(t *testing.T) {
cfg.Config.K9s.UI = config.UI{Skin: "black-and-wtf"}
cfg.RefreshStyles(newMockSynchronizer())
assert.True(t, cfg.HasSkin())
- assert.Equal(t, tcell.ColorGhostWhite.TrueColor(), render.StdColor)
- assert.Equal(t, tcell.ColorWhiteSmoke.TrueColor(), render.ErrColor)
+ assert.Equal(t, tcell.ColorGhostWhite.TrueColor(), model1.StdColor)
+ assert.Equal(t, tcell.ColorWhiteSmoke.TrueColor(), model1.ErrColor)
}
func TestBenchConfig(t *testing.T) {
diff --git a/internal/ui/menu_test.go b/internal/ui/menu_test.go
index 599e2d2c..9a478803 100644
--- a/internal/ui/menu_test.go
+++ b/internal/ui/menu_test.go
@@ -27,16 +27,16 @@ func TestNewMenu(t *testing.T) {
func TestActionHints(t *testing.T) {
uu := map[string]struct {
- aa ui.KeyActions
+ aa *ui.KeyActions
e model.MenuHints
}{
"a": {
- aa: ui.KeyActions{
+ aa: ui.NewKeyActionsFromMap(ui.KeyMap{
ui.KeyB: ui.NewKeyAction("bleeB", nil, true),
ui.KeyA: ui.NewKeyAction("bleeA", nil, true),
ui.Key0: ui.NewKeyAction("zero", nil, true),
ui.Key1: ui.NewKeyAction("one", nil, false),
- },
+ }),
e: model.MenuHints{
{Mnemonic: "0", Description: "zero", Visible: true},
{Mnemonic: "1", Description: "one", Visible: false},
diff --git a/internal/ui/padding.go b/internal/ui/padding.go
index b57cdb1f..25cacdc5 100644
--- a/internal/ui/padding.go
+++ b/internal/ui/padding.go
@@ -7,6 +7,7 @@ import (
"strings"
"unicode"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
)
@@ -14,26 +15,27 @@ import (
type MaxyPad []int
// ComputeMaxColumns figures out column max size and necessary padding.
-func ComputeMaxColumns(pads MaxyPad, sortColName string, header render.Header, ee render.RowEvents) {
+func ComputeMaxColumns(pads MaxyPad, sortColName string, t *model1.TableData) {
const colPadding = 1
- for index, h := range header {
- pads[index] = len(h.Name)
- if h.Name == sortColName {
- pads[index] = len(h.Name) + 2
+ for i, n := range t.ColumnNames(true) {
+ pads[i] = len(n)
+ if n == sortColName {
+ pads[i] += 2
}
}
var row int
- for _, e := range ee {
- for index, field := range e.Row.Fields {
+ t.RowsRange(func(_ int, re model1.RowEvent) bool {
+ for index, field := range re.Row.Fields {
width := len(field) + colPadding
if index < len(pads) && width > pads[index] {
pads[index] = width
}
}
row++
- }
+ return true
+ })
}
// IsASCII checks if table cell has all ascii characters.
diff --git a/internal/ui/padding_test.go b/internal/ui/padding_test.go
index 51a0bcde..0dbcb87a 100644
--- a/internal/ui/padding_test.go
+++ b/internal/ui/padding_test.go
@@ -6,70 +6,74 @@ package ui
import (
"testing"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/model1"
"github.com/stretchr/testify/assert"
)
func TestMaxColumn(t *testing.T) {
uu := map[string]struct {
- t *render.TableData
+ t *model1.TableData
s string
e MaxyPad
}{
"ascii col 0": {
- &render.TableData{
- Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
- RowEvents: render.RowEvents{
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"hello", "world"},
+ model1.NewTableDataWithRows(
+ client.NewGVR("test"),
+ model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}},
+ model1.NewRowEventsWithEvts(
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"hello", "world"},
},
},
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"yo", "mama"},
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"yo", "mama"},
},
},
- },
- },
+ ),
+ ),
"A",
MaxyPad{6, 6},
},
"ascii col 1": {
- &render.TableData{
- Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
- RowEvents: render.RowEvents{
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"hello", "world"},
+ model1.NewTableDataWithRows(
+ client.NewGVR("test"),
+ model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}},
+ model1.NewRowEventsWithEvts(
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"hello", "world"},
},
},
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"yo", "mama"},
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"yo", "mama"},
},
},
- },
- },
+ ),
+ ),
"B",
MaxyPad{6, 6},
},
"non_ascii": {
- &render.TableData{
- Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
- RowEvents: render.RowEvents{
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"Hello World lord of ipsums 😅", "world"},
+ model1.NewTableDataWithRows(
+ client.NewGVR("test"),
+ model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}},
+ model1.NewRowEventsWithEvts(
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"Hello World lord of ipsums 😅", "world"},
},
},
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"o", "mama"},
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"o", "mama"},
},
},
- },
- },
+ ),
+ ),
"A",
MaxyPad{32, 6},
},
@@ -78,8 +82,8 @@ func TestMaxColumn(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
- pads := make(MaxyPad, len(u.t.Header))
- ComputeMaxColumns(pads, u.s, u.t.Header, u.t.RowEvents)
+ pads := make(MaxyPad, u.t.HeaderCount())
+ ComputeMaxColumns(pads, u.s, u.t)
assert.Equal(t, u.e, pads)
})
}
@@ -119,27 +123,28 @@ func TestPad(t *testing.T) {
}
func BenchmarkMaxColumn(b *testing.B) {
- table := render.TableData{
- Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}},
- RowEvents: render.RowEvents{
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"hello", "world"},
+ table := model1.NewTableDataWithRows(
+ client.NewGVR("test"),
+ model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}},
+ model1.NewRowEventsWithEvts(
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"hello", "world"},
},
},
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"yo", "mama"},
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"yo", "mama"},
},
},
- },
- }
+ ),
+ )
- pads := make(MaxyPad, len(table.Header))
+ pads := make(MaxyPad, table.HeaderCount())
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
- ComputeMaxColumns(pads, "A", table.Header, table.RowEvents)
+ ComputeMaxColumns(pads, "A", table)
}
}
diff --git a/internal/ui/select_table.go b/internal/ui/select_table.go
index 25f33c0f..d1df36c4 100644
--- a/internal/ui/select_table.go
+++ b/internal/ui/select_table.go
@@ -107,7 +107,7 @@ func (s *SelectTable) SelectRow(r, c int, broadcast bool) {
if !broadcast {
s.SetSelectionChangedFunc(nil)
}
- if c := s.model.Count(); c > 0 && r-1 > c {
+ if c := s.model.RowCount(); c > 0 && r-1 > c {
r = c + 1
}
defer s.SetSelectionChangedFunc(s.selectionChanged)
diff --git a/internal/ui/table.go b/internal/ui/table.go
index 66b04503..0af73164 100644
--- a/internal/ui/table.go
+++ b/internal/ui/table.go
@@ -5,15 +5,14 @@ package ui
import (
"context"
- "errors"
"fmt"
- "strings"
+ "sync"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
- "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/vul"
"github.com/derailed/tcell/v2"
@@ -27,10 +26,10 @@ const maxTruncate = 50
type (
// ColorerFunc represents a row colorer.
- ColorerFunc func(ns string, evt render.RowEvent) tcell.Color
+ ColorerFunc func(ns string, evt model1.RowEvent) tcell.Color
// DecorateFunc represents a row decorator.
- DecorateFunc func(*render.TableData)
+ DecorateFunc func(*model1.TableData)
// SelectedRowFunc a table selection callback.
SelectedRowFunc func(r int)
@@ -39,21 +38,22 @@ type (
// Table represents tabular data.
type Table struct {
gvr client.GVR
- sortCol SortColumn
+ sortCol model1.SortColumn
manualSort bool
- header render.Header
Path string
Extras string
*SelectTable
- actions KeyActions
+ actions *KeyActions
cmdBuff *model.FishBuff
styles *config.Styles
viewSetting *config.ViewSetting
- colorerFn render.ColorerFunc
+ colorerFn model1.ColorerFunc
decorateFn DecorateFunc
wide bool
toast bool
hasMetrics bool
+ ctx context.Context
+ mx sync.RWMutex
}
// NewTable returns a new table view.
@@ -65,12 +65,69 @@ func NewTable(gvr client.GVR) *Table {
marks: make(map[string]struct{}),
},
gvr: gvr,
- actions: make(KeyActions),
+ actions: NewKeyActions(),
cmdBuff: model.NewFishBuff('/', model.FilterBuffer),
- sortCol: SortColumn{asc: true},
+ sortCol: model1.SortColumn{ASC: true},
}
}
+func (t *Table) setSortCol(sc model1.SortColumn) {
+ t.mx.Lock()
+ defer t.mx.Unlock()
+
+ t.sortCol = sc
+}
+
+func (t *Table) toggleSortCol() {
+ t.mx.Lock()
+ defer t.mx.Unlock()
+
+ t.sortCol.ASC = !t.sortCol.ASC
+}
+
+func (t *Table) getSortCol() model1.SortColumn {
+ t.mx.RLock()
+ defer t.mx.RUnlock()
+
+ return t.sortCol
+}
+
+func (t *Table) setMSort(b bool) {
+ t.mx.Lock()
+ defer t.mx.Unlock()
+
+ t.manualSort = b
+}
+
+func (t *Table) getMSort() bool {
+ t.mx.RLock()
+ defer t.mx.RUnlock()
+
+ return t.manualSort
+}
+
+func (t *Table) setVs(vs *config.ViewSetting) {
+ t.mx.Lock()
+ defer t.mx.Unlock()
+
+ t.viewSetting = vs
+}
+
+func (t *Table) getVs() *config.ViewSetting {
+ t.mx.RLock()
+ defer t.mx.RUnlock()
+
+ return t.viewSetting
+}
+
+func (t *Table) GetContext() context.Context {
+ return t.ctx
+}
+
+func (t *Table) SetContext(ctx context.Context) {
+ t.ctx = ctx
+}
+
// Init initializes the component.
func (t *Table) Init(ctx context.Context) {
t.SetFixed(1, 0)
@@ -92,8 +149,9 @@ func (t *Table) Init(ctx context.Context) {
func (t *Table) GVR() client.GVR { return t.gvr }
// ViewSettingsChanged notifies listener the view configuration changed.
-func (t *Table) ViewSettingsChanged(settings config.ViewSetting) {
- t.viewSetting, t.manualSort = &settings, false
+func (t *Table) ViewSettingsChanged(vs config.ViewSetting) {
+ t.setVs(&vs)
+ t.setMSort(false)
t.Refresh()
}
@@ -129,7 +187,7 @@ func (t *Table) ToggleWide() {
}
// Actions returns active menu bindings.
-func (t *Table) Actions() KeyActions {
+func (t *Table) Actions() *KeyActions {
return t.actions
}
@@ -171,7 +229,7 @@ func (t *Table) ExtraHints() map[string]string {
}
// GetFilteredData fetch filtered tabular data.
-func (t *Table) GetFilteredData() *render.TableData {
+func (t *Table) GetFilteredData() *model1.TableData {
return t.filtered(t.GetModel().Peek())
}
@@ -181,67 +239,51 @@ func (t *Table) SetDecorateFn(f DecorateFunc) {
}
// SetColorerFn specifies the default colorer.
-func (t *Table) SetColorerFn(f render.ColorerFunc) {
+func (t *Table) SetColorerFn(f model1.ColorerFunc) {
t.colorerFn = f
}
// SetSortCol sets in sort column index and order.
func (t *Table) SetSortCol(name string, asc bool) {
- t.sortCol.name, t.sortCol.asc = name, asc
+ t.setSortCol(model1.SortColumn{Name: name, ASC: asc})
}
// Update table content.
-func (t *Table) Update(data *render.TableData, hasMetrics bool) {
- t.header = data.Header
+func (t *Table) Update(data *model1.TableData, hasMetrics bool) *model1.TableData {
if t.decorateFn != nil {
t.decorateFn(data)
}
t.hasMetrics = hasMetrics
- t.doUpdate(t.filtered(data))
- t.UpdateTitle()
+
+ return t.doUpdate(t.filtered(data))
}
-func (t *Table) doUpdate(data *render.TableData) {
- if client.IsAllNamespaces(data.Namespace) {
- t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false)
+func (t *Table) doUpdate(data *model1.TableData) *model1.TableData {
+ if client.IsAllNamespaces(data.GetNamespace()) {
+ t.actions.Add(
+ KeyShiftP,
+ NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false),
+ )
} else {
t.actions.Delete(KeyShiftP)
}
- cols := t.header.Columns(t.wide)
- if t.viewSetting != nil && len(t.viewSetting.Columns) > 0 {
- cols = t.viewSetting.Columns
- }
- custData := data.Customize(cols, t.wide)
- // The sortColumn settings in the configuration file are only used
- // if the sortCol has not been modified manually
- if t.viewSetting != nil && t.viewSetting.SortColumn != "" && !t.manualSort {
- tokens := strings.Split(t.viewSetting.SortColumn, ":")
- if custData.Header.IndexOf(tokens[0], false) >= 0 && !t.manualSort {
- t.sortCol.name, t.sortCol.asc = tokens[0], true
- if len(tokens) == 2 && tokens[1] == "desc" {
- t.sortCol.asc = false
- }
- }
- }
+ cdata, sortCol := data.Customize(t.getVs(), t.getSortCol(), t.getMSort(), true)
+ t.setSortCol(sortCol)
- if t.sortCol.name == "" && client.IsAllNamespaces(data.Namespace) {
- t.sortCol.name = "NAMESPACE"
- }
- if t.sortCol.name == "" || (t.sortCol.name == "NAMESPACE" && !client.IsAllNamespaces(data.Namespace)) && len(custData.Header) > 0 {
- if idx := custData.Header.IndexOf("NAME", false); idx >= 0 {
- t.sortCol.name = custData.Header[idx].Name
- } else {
- t.sortCol.name = custData.Header[0].Name
- }
- }
+ return cdata
+}
+func (t *Table) UpdateUI(cdata, data *model1.TableData) {
t.Clear()
fg := t.styles.Table().Header.FgColor.Color()
bg := t.styles.Table().Header.BgColor.Color()
var col int
- for _, h := range custData.Header {
+ for _, h := range cdata.Header() {
+ if !t.wide && h.Wide {
+ continue
+ }
if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
continue
}
@@ -258,38 +300,42 @@ func (t *Table) doUpdate(data *render.TableData) {
c.SetTextColor(fg)
col++
}
- colIndex := custData.Header.IndexOf(t.sortCol.name, false)
- custData.RowEvents.Sort(
- custData.Namespace,
- colIndex,
- custData.Header.IsTimeCol(colIndex),
- custData.Header.IsMetricsCol(colIndex),
- custData.Header.IsCapacityCol(colIndex),
- t.sortCol.asc,
- )
+ cdata.Sort(t.getSortCol())
+
+ pads := make(MaxyPad, cdata.HeaderCount())
+ ComputeMaxColumns(pads, t.getSortCol().Name, cdata)
+ cdata.RowsRange(func(row int, re model1.RowEvent) bool {
+ ore, ok := data.FindRow(re.Row.ID)
+ if !ok {
+ log.Error().Msgf("unable to find original re: %q", re.Row.ID)
+ return true
+ }
+ t.buildRow(row+1, re, ore, cdata.Header(), pads)
+
+ return true
+ })
- pads := make(MaxyPad, len(custData.Header))
- ComputeMaxColumns(pads, t.sortCol.name, custData.Header, custData.RowEvents)
- for row, re := range custData.RowEvents {
- idx, _ := data.RowEvents.FindIndex(re.Row.ID)
- t.buildRow(row+1, re, data.RowEvents[idx], custData.Header, pads)
- }
t.updateSelection(true)
+ t.UpdateTitle()
}
-func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads MaxyPad) {
- color := render.DefaultColorer
+func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads MaxyPad) {
+ color := model1.DefaultColorer
if t.colorerFn != nil {
color = t.colorerFn
}
marked := t.IsMarked(re.Row.ID)
var col int
+ ns := t.GetModel().GetNamespace()
for c, field := range re.Row.Fields {
if c >= len(h) {
log.Error().Msgf("field/header overflow detected for %q -- %d::%d. Check your mappings!", t.GVR(), c, len(h))
continue
}
+ if !t.wide && h[c].Wide {
+ continue
+ }
if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() {
continue
@@ -315,7 +361,7 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M
cell := tview.NewTableCell(field)
cell.SetExpansion(1)
cell.SetAlign(h[c].Align)
- fgColor := color(t.GetModel().GetNamespace(), t.header, ore)
+ fgColor := color(ns, h, &re)
cell.SetTextColor(fgColor)
if marked {
cell.SetTextColor(t.styles.Table().MarkColor.Color())
@@ -331,13 +377,14 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M
// SortColCmd designates a sorted column.
func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey {
- t.manualSort = true
- t.sortCol.asc = !t.sortCol.asc
- if t.sortCol.name != name {
- t.sortCol.asc = asc
+ sc := t.getSortCol()
+ sc.ASC = !sc.ASC
+ if sc.Name != name {
+ sc.ASC = asc
}
- t.sortCol.name = name
- t.manualSort = true
+ sc.Name = name
+ t.setSortCol(sc)
+ t.setMSort(true)
t.Refresh()
return nil
}
@@ -345,7 +392,7 @@ func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tce
// SortInvertCmd reverses sorting order.
func (t *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey {
- t.sortCol.asc = !t.sortCol.asc
+ t.toggleSortCol()
t.Refresh()
return nil
@@ -360,21 +407,23 @@ func (t *Table) ClearMarks() {
// Refresh update the table data.
func (t *Table) Refresh() {
data := t.model.Peek()
- if len(data.Header) == 0 {
+ if data.HeaderCount() == 0 {
return
}
// BOZO!! Really want to tell model reload now. Refactor!
- t.Update(data, t.hasMetrics)
+ cdata := t.Update(data, t.hasMetrics)
+ t.UpdateUI(cdata, data)
}
// GetSelectedRow returns the entire selected row or nil if nothing selected.
-func (t *Table) GetSelectedRow(path string) *render.Row {
+func (t *Table) GetSelectedRow(path string) *model1.Row {
data := t.model.Peek()
- i, ok := data.RowEvents.FindIndex(path)
+ re, ok := data.FindRow(path)
if !ok {
return nil
}
- return &data.RowEvents[i].Row
+
+ return &re.Row
}
// NameColIndex returns the index of the resource name column.
@@ -386,38 +435,25 @@ func (t *Table) NameColIndex() int {
if t.GetModel().ClusterWide() {
col++
}
+
return col
}
// AddHeaderCell configures a table cell header.
-func (t *Table) AddHeaderCell(col int, h render.HeaderColumn) {
- sortCol := h.Name == t.sortCol.name
- c := tview.NewTableCell(sortIndicator(sortCol, t.sortCol.asc, t.styles.Table(), h.Name))
+func (t *Table) AddHeaderCell(col int, h model1.HeaderColumn) {
+ sc := t.getSortCol()
+ sortCol := h.Name == sc.Name
+ c := tview.NewTableCell(sortIndicator(sortCol, sc.ASC, t.styles.Table(), h.Name))
c.SetExpansion(1)
c.SetAlign(h.Align)
t.SetCell(0, col, c)
}
-func (t *Table) filtered(data *render.TableData) *render.TableData {
- filtered := data
- if t.toast {
- filtered = filterToast(data)
- }
- if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.GetText()) {
- return filtered
- }
-
- q := t.cmdBuff.GetText()
- if f, ok := dao.HasFuzzySelector(q); ok {
- return fuzzyFilter(f, filtered)
- }
-
- filtered, err := rxFilter(q, dao.IsInverseSelector(q), filtered)
- if err != nil {
- log.Error().Err(errors.New("invalid filter expression")).Msg("Regexp")
- }
-
- return filtered
+func (t *Table) filtered(data *model1.TableData) *model1.TableData {
+ return data.Filter(model1.FilterOpts{
+ Toast: t.toast,
+ Filter: t.cmdBuff.GetText(),
+ })
}
// CmdBuff returns the associated command buffer.
@@ -470,7 +506,7 @@ func (t *Table) styleTitle() string {
}
buff := t.cmdBuff.GetText()
- if IsLabelSelector(buff) {
+ if internal.IsLabelSelector(buff) {
buff = render.Truncate(TrimLabelSelector(buff), maxTruncate)
} else if l := t.GetModel().GetLabelFilter(); l != "" {
buff = render.Truncate(l, maxTruncate)
diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go
index bd6ea481..479ed2d0 100644
--- a/internal/ui/table_helper.go
+++ b/internal/ui/table_helper.go
@@ -6,15 +6,11 @@ package ui
import (
"context"
"fmt"
- "regexp"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/config"
- "github.com/derailed/k9s/internal/render"
- "github.com/derailed/k9s/internal/view/cmd"
"github.com/rs/zerolog/log"
- "github.com/sahilm/fuzzy"
)
const (
@@ -40,11 +36,6 @@ const (
NoNSFmat = "%s-%d.csv"
)
-var (
- // LabelRx identifies a label query.
- LabelRx = regexp.MustCompile(`\A\-l`)
-)
-
func mustExtractStyles(ctx context.Context) *config.Styles {
styles, ok := ctx.Value(internal.KeyStyles).(*config.Styles)
if !ok {
@@ -63,15 +54,6 @@ func TrimCell(tv *SelectTable, row, col int) string {
return strings.TrimSpace(c.Text)
}
-// IsLabelSelector checks if query is a label query.
-func IsLabelSelector(s string) bool {
- if LabelRx.MatchString(s) {
- return true
- }
-
- return !strings.Contains(s, " ") && cmd.ToLabels(s) != nil
-}
-
// TrimLabelSelector extracts label query.
func TrimLabelSelector(s string) string {
if strings.Index(s, "-l") == 0 {
@@ -116,75 +98,3 @@ func formatCell(field string, padding int) string {
return field
}
-
-func filterToast(data *render.TableData) *render.TableData {
- validX := data.Header.IndexOf("VALID", true)
- if validX == -1 {
- return data
- }
-
- toast := render.TableData{
- Header: data.Header,
- RowEvents: make(render.RowEvents, 0, len(data.RowEvents)),
- Namespace: data.Namespace,
- }
- for _, re := range data.RowEvents {
- if re.Row.Fields[validX] != "" {
- toast.RowEvents = append(toast.RowEvents, re)
- }
- }
-
- return &toast
-}
-
-func rxFilter(q string, inverse bool, data *render.TableData) (*render.TableData, error) {
- if inverse {
- q = q[1:]
- }
- rx, err := regexp.Compile(`(?i)(` + q + `)`)
- if err != nil {
- return data, fmt.Errorf("%w -- %s", err, q)
- }
-
- filtered := render.TableData{
- Header: data.Header,
- RowEvents: make(render.RowEvents, 0, len(data.RowEvents)),
- Namespace: data.Namespace,
- }
- ageIndex := data.Header.IndexOf("AGE", true)
-
- const spacer = " "
- for _, re := range data.RowEvents {
- ff := re.Row.Fields
- if ageIndex >= 0 && ageIndex+1 <= len(ff) {
- ff = append(ff[0:ageIndex], ff[ageIndex+1:]...)
- }
- fields := strings.Join(ff, spacer)
- if (inverse && !rx.MatchString(fields)) ||
- ((!inverse) && rx.MatchString(fields)) {
- filtered.RowEvents = append(filtered.RowEvents, re)
- }
- }
-
- return &filtered, nil
-}
-
-func fuzzyFilter(q string, data *render.TableData) *render.TableData {
- q = strings.TrimSpace(q)
- ss := make([]string, 0, len(data.RowEvents))
- for _, re := range data.RowEvents {
- ss = append(ss, re.Row.ID)
- }
-
- filtered := render.TableData{
- Header: data.Header,
- RowEvents: make(render.RowEvents, 0, len(data.RowEvents)),
- Namespace: data.Namespace,
- }
- mm := fuzzy.Find(q, ss)
- for _, m := range mm {
- filtered.RowEvents = append(filtered.RowEvents, data.RowEvents[m.Index])
- }
-
- return &filtered
-}
diff --git a/internal/ui/table_helper_test.go b/internal/ui/table_helper_test.go
index 219ffaa3..7bec2d40 100644
--- a/internal/ui/table_helper_test.go
+++ b/internal/ui/table_helper_test.go
@@ -33,28 +33,6 @@ func TestTruncate(t *testing.T) {
}
}
-func TestIsLabelSelector(t *testing.T) {
- uu := map[string]struct {
- s string
- ok bool
- }{
- "empty": {s: ""},
- "cool": {s: "-l app=fred,env=blee", ok: true},
- "no-flag": {s: "app=fred,env=blee", ok: true},
- "no-space": {s: "-lapp=fred,env=blee", ok: true},
- "wrong-flag": {s: "-f app=fred,env=blee"},
- "missing-key": {s: "=fred"},
- "missing-val": {s: "fred="},
- }
-
- for k := range uu {
- u := uu[k]
- t.Run(k, func(t *testing.T) {
- assert.Equal(t, u.ok, IsLabelSelector(u.s))
- })
- }
-}
-
func TestTrimLabelSelector(t *testing.T) {
uu := map[string]struct {
sel, e string
diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go
index f6169abd..9b604d84 100644
--- a/internal/ui/table_test.go
+++ b/internal/ui/table_test.go
@@ -13,7 +13,7 @@ import (
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/ui"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -32,10 +32,11 @@ func TestTableUpdate(t *testing.T) {
v.Init(makeContext())
data := makeTableData()
- v.Update(data, false)
+ cdata := v.Update(data, false)
+ v.UpdateUI(cdata, data)
- assert.Equal(t, len(data.RowEvents)+1, v.GetRowCount())
- assert.Equal(t, len(data.Header), v.GetColumnCount())
+ assert.Equal(t, data.RowCount()+1, v.GetRowCount())
+ assert.Equal(t, data.HeaderCount(), v.GetColumnCount())
}
func TestTableSelection(t *testing.T) {
@@ -43,12 +44,14 @@ func TestTableSelection(t *testing.T) {
v.Init(makeContext())
m := &mockModel{}
v.SetModel(m)
- v.Update(m.Peek(), false)
+ data := m.Peek()
+ cdata := v.Update(data, false)
+ v.UpdateUI(cdata, data)
v.SelectRow(1, 0, true)
r := v.GetSelectedRow("r1")
if r != nil {
- assert.Equal(t, render.Row{ID: "r1", Fields: render.Fields{"blee", "duh", "fred"}}, *r)
+ assert.Equal(t, model1.Row{ID: "r1", Fields: model1.Fields{"blee", "duh", "fred"}}, *r)
}
assert.Equal(t, "r1", v.GetSelectedItem())
assert.Equal(t, "blee", v.GetSelectedCell(0))
@@ -71,9 +74,9 @@ func (t *mockModel) SetInstance(string) {}
func (t *mockModel) SetLabelFilter(string) {}
func (t *mockModel) GetLabelFilter() string { return "" }
func (t *mockModel) Empty() bool { return false }
-func (t *mockModel) Count() int { return 1 }
+func (t *mockModel) RowCount() int { return 1 }
func (t *mockModel) HasMetrics() bool { return true }
-func (t *mockModel) Peek() *render.TableData { return makeTableData() }
+func (t *mockModel) Peek() *model1.TableData { return makeTableData() }
func (t *mockModel) Refresh(context.Context) error { return nil }
func (t *mockModel) ClusterWide() bool { return false }
func (t *mockModel) GetNamespace() string { return "blee" }
@@ -97,30 +100,29 @@ func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) {
func (t *mockModel) InNamespace(string) bool { return true }
func (t *mockModel) SetRefreshRate(time.Duration) {}
-func makeTableData() *render.TableData {
- t := render.NewTableData()
- t.Namespace = ""
- t.Header = render.Header{
- render.HeaderColumn{Name: "A"},
- render.HeaderColumn{Name: "B"},
- render.HeaderColumn{Name: "C"},
- }
- t.RowEvents = render.RowEvents{
- render.RowEvent{
- Row: render.Row{
- ID: "r1",
- Fields: render.Fields{"blee", "duh", "fred"},
- },
+func makeTableData() *model1.TableData {
+ return model1.NewTableDataWithRows(
+ client.NewGVR("test"),
+ model1.Header{
+ model1.HeaderColumn{Name: "A"},
+ model1.HeaderColumn{Name: "B"},
+ model1.HeaderColumn{Name: "C"},
},
- render.RowEvent{
- Row: render.Row{
- ID: "r2",
- Fields: render.Fields{"blee", "duh", "zorg"},
+ model1.NewRowEventsWithEvts(
+ model1.RowEvent{
+ Row: model1.Row{
+ ID: "r1",
+ Fields: model1.Fields{"blee", "duh", "fred"},
+ },
},
- },
- }
-
- return t
+ model1.RowEvent{
+ Row: model1.Row{
+ ID: "r2",
+ Fields: model1.Fields{"blee", "duh", "zorg"},
+ },
+ },
+ ),
+ )
}
func makeContext() context.Context {
diff --git a/internal/ui/tree.go b/internal/ui/tree.go
index 10eb31e7..5af3046b 100644
--- a/internal/ui/tree.go
+++ b/internal/ui/tree.go
@@ -18,7 +18,7 @@ type KeyListenerFunc func()
type Tree struct {
*tview.TreeView
- actions KeyActions
+ actions *KeyActions
selectedItem string
cmdBuff *model.FishBuff
expandNodes bool
@@ -31,7 +31,7 @@ func NewTree() *Tree {
return &Tree{
TreeView: tview.NewTreeView(),
expandNodes: true,
- actions: make(KeyActions),
+ actions: NewKeyActions(),
cmdBuff: model.NewFishBuff('/', model.FilterBuffer),
}
}
@@ -75,7 +75,7 @@ func (t *Tree) SetKeyListenerFn(f KeyListenerFunc) {
}
// Actions returns active menu bindings.
-func (t *Tree) Actions() KeyActions {
+func (t *Tree) Actions() *KeyActions {
return t.actions
}
@@ -91,14 +91,14 @@ func (t *Tree) ExtraHints() map[string]string {
// BindKeys binds default mnemonics.
func (t *Tree) BindKeys() {
- t.Actions().Add(KeyActions{
+ t.Actions().Merge(NewKeyActionsFromMap(KeyMap{
KeySpace: NewKeyAction("Expand/Collapse", t.noopCmd, true),
KeyX: NewKeyAction("Expand/Collapse All", t.toggleCollapseCmd, true),
- })
+ }))
}
func (t *Tree) keyboard(evt *tcell.EventKey) *tcell.EventKey {
- if a, ok := t.actions[AsKey(evt)]; ok {
+ if a, ok := t.actions.Get(AsKey(evt)); ok {
return a.Action(evt)
}
diff --git a/internal/ui/types.go b/internal/ui/types.go
index 8013c5e7..534d084e 100644
--- a/internal/ui/types.go
+++ b/internal/ui/types.go
@@ -9,22 +9,11 @@ import (
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
-type (
- // SortFn represent a function that can sort columnar data.
- SortFn func(rows render.Rows, sortCol SortColumn)
-
- // SortColumn represents a sortable column.
- SortColumn struct {
- name string
- asc bool
- }
-)
-
// Namespaceable represents a namespaceable model.
type Namespaceable interface {
// ClusterWide returns true if the model represents resource in all namespaces.
@@ -63,11 +52,11 @@ type Tabular interface {
// Empty returns true if model has no data.
Empty() bool
- // Count returns the model data count.
- Count() int
+ // RowCount returns the model data count.
+ RowCount() int
// Peek returns current model data.
- Peek() *render.TableData
+ Peek() *model1.TableData
// Watch watches a given resource for changes.
Watch(context.Context) error
diff --git a/internal/view/actions.go b/internal/view/actions.go
index 6709b445..7d766198 100644
--- a/internal/view/actions.go
+++ b/internal/view/actions.go
@@ -57,13 +57,13 @@ func inScope(scopes []string, aliases map[string]struct{}) bool {
return false
}
-func hotKeyActions(r Runner, aa ui.KeyActions) error {
+func hotKeyActions(r Runner, aa *ui.KeyActions) error {
hh := config.NewHotKeys()
- for k, a := range aa {
+ aa.Range(func(k tcell.Key, a ui.KeyAction) {
if a.Opts.HotKey {
- delete(aa, k)
+ aa.Delete(k)
}
- }
+ })
var errs error
if err := hh.Load(r.App().Config.ContextHotkeysPath()); err != nil {
@@ -75,7 +75,7 @@ func hotKeyActions(r Runner, aa ui.KeyActions) error {
errs = errors.Join(errs, err)
continue
}
- if _, ok := aa[key]; ok {
+ if _, ok := aa.Get(key); ok {
if !hk.Override {
errs = errors.Join(errs, fmt.Errorf("duplicate hotkey found for %q in %q", hk.ShortCut, k))
continue
@@ -89,14 +89,14 @@ func hotKeyActions(r Runner, aa ui.KeyActions) error {
continue
}
- aa[key] = ui.NewKeyActionWithOpts(
+ aa.Add(key, ui.NewKeyActionWithOpts(
hk.Description,
gotoCmd(r, command, "", !hk.KeepHistory),
ui.ActionOpts{
Shared: true,
HotKey: true,
},
- )
+ ))
}
return errs
@@ -109,18 +109,23 @@ func gotoCmd(r Runner, cmd, path string, clearStack bool) ui.ActionHandler {
}
}
-func pluginActions(r Runner, aa ui.KeyActions) error {
+func pluginActions(r Runner, aa *ui.KeyActions) error {
pp := config.NewPlugins()
- for k, a := range aa {
+ aa.Range(func(k tcell.Key, a ui.KeyAction) {
if a.Opts.Plugin {
- delete(aa, k)
+ aa.Delete(k)
}
+ })
+
+ path, err := r.App().Config.ContextPluginsPath()
+ if err != nil {
+ return err
+ }
+ if err := pp.Load(path); err != nil {
+ return err
}
var errs error
- if err := pp.Load(r.App().Config.ContextPluginsPath()); err != nil {
- errs = errors.Join(errs, err)
- }
aliases := r.Aliases()
for k, plugin := range pp.Plugins {
if !inScope(plugin.Scopes, aliases) {
@@ -131,7 +136,7 @@ func pluginActions(r Runner, aa ui.KeyActions) error {
errs = errors.Join(errs, err)
continue
}
- if _, ok := aa[key]; ok {
+ if _, ok := aa.Get(key); ok {
if !plugin.Override {
errs = errors.Join(errs, fmt.Errorf("duplicate plugin key found for %q in %q", plugin.ShortCut, k))
continue
@@ -139,13 +144,14 @@ func pluginActions(r Runner, aa ui.KeyActions) error {
log.Info().Msgf("Action %q has been overridden by plugin in %q", plugin.ShortCut, k)
}
- aa[key] = ui.NewKeyActionWithOpts(
+ aa.Add(key, ui.NewKeyActionWithOpts(
plugin.Description,
pluginAction(r, plugin),
ui.ActionOpts{
Visible: true,
Plugin: true,
- })
+ },
+ ))
}
return errs
diff --git a/internal/view/alias.go b/internal/view/alias.go
index 7e654cba..496f2e5c 100644
--- a/internal/view/alias.go
+++ b/internal/view/alias.go
@@ -47,10 +47,10 @@ func (a *Alias) aliasContext(ctx context.Context) context.Context {
return context.WithValue(ctx, internal.KeyAliases, a.App().command.alias)
}
-func (a *Alias) bindKeys(aa ui.KeyActions) {
+func (a *Alias) bindKeys(aa *ui.KeyActions) {
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL)
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true),
ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.GetTable().SortColCmd("RESOURCE", true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Command", a.GetTable().SortColCmd("COMMAND", true), false),
diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go
index 15cd3936..6deba289 100644
--- a/internal/view/alias_test.go
+++ b/internal/view/alias_test.go
@@ -13,7 +13,7 @@ import (
"github.com/derailed/k9s/internal/config/mock"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view"
"github.com/derailed/tcell/v2"
@@ -93,9 +93,9 @@ func (t *mockModel) SetInstance(string) {}
func (t *mockModel) SetLabelFilter(string) {}
func (t *mockModel) GetLabelFilter() string { return "" }
func (t *mockModel) Empty() bool { return false }
-func (t *mockModel) Count() int { return 1 }
+func (t *mockModel) RowCount() int { return 1 }
func (t *mockModel) HasMetrics() bool { return true }
-func (t *mockModel) Peek() *render.TableData { return makeTableData() }
+func (t *mockModel) Peek() *model1.TableData { return makeTableData() }
func (t *mockModel) ClusterWide() bool { return false }
func (t *mockModel) GetNamespace() string { return "blee" }
func (t *mockModel) SetNamespace(string) {}
@@ -123,27 +123,27 @@ func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) {
func (t *mockModel) InNamespace(string) bool { return true }
func (t *mockModel) SetRefreshRate(time.Duration) {}
-func makeTableData() *render.TableData {
- return &render.TableData{
- Namespace: client.ClusterScope,
- Header: render.Header{
- render.HeaderColumn{Name: "RESOURCE"},
- render.HeaderColumn{Name: "COMMAND"},
- render.HeaderColumn{Name: "APIGROUP"},
+func makeTableData() *model1.TableData {
+ return model1.NewTableDataWithRows(
+ client.NewGVR("test"),
+ model1.Header{
+ model1.HeaderColumn{Name: "RESOURCE"},
+ model1.HeaderColumn{Name: "COMMAND"},
+ model1.HeaderColumn{Name: "APIGROUP"},
},
- RowEvents: render.RowEvents{
- render.RowEvent{
- Row: render.Row{
+ model1.NewRowEventsWithEvts(
+ model1.RowEvent{
+ Row: model1.Row{
ID: "r1",
- Fields: render.Fields{"blee", "duh", "fred"},
+ Fields: model1.Fields{"blee", "duh", "fred"},
},
},
- render.RowEvent{
- Row: render.Row{
+ model1.RowEvent{
+ Row: model1.Row{
ID: "r2",
- Fields: render.Fields{"fred", "duh", "zorg"},
+ Fields: model1.Fields{"fred", "duh", "zorg"},
},
},
- },
- }
+ ),
+ )
}
diff --git a/internal/view/app.go b/internal/view/app.go
index 1cfb2dcd..fef22311 100644
--- a/internal/view/app.go
+++ b/internal/view/app.go
@@ -243,14 +243,14 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {
}
func (a *App) bindKeys() {
- a.AddActions(ui.KeyActions{
+ a.AddActions(ui.NewKeyActionsFromMap(ui.KeyMap{
ui.KeyShift9: ui.NewSharedKeyAction("DumpGOR", a.dumpGOR, false),
tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false),
tcell.KeyCtrlG: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, false),
ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false),
tcell.KeyCtrlA: ui.NewSharedKeyAction("Aliases", a.aliasCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false),
- })
+ }))
}
func (a *App) dumpGOR(evt *tcell.EventKey) *tcell.EventKey {
@@ -483,7 +483,7 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
return err
}
}
- if err := a.Config.Save(); err != nil {
+ if err := a.Config.Save(true); err != nil {
log.Error().Err(err).Msg("config save failed!")
} else {
log.Debug().Msgf("Saved context config for: %q", name)
@@ -516,7 +516,7 @@ func (a *App) BailOut() {
}
}()
- if err := a.Config.Save(); err != nil {
+ if err := a.Config.Save(true); err != nil {
log.Error().Err(err).Msg("config save failed!")
}
@@ -721,7 +721,6 @@ func (a *App) inject(c model.Component, clearStack bool) error {
if clearStack {
a.Content.Stack.Clear()
}
-
a.Content.Push(c)
return nil
diff --git a/internal/view/app_test.go b/internal/view/app_test.go
index 924fb8f2..e1e932f0 100644
--- a/internal/view/app_test.go
+++ b/internal/view/app_test.go
@@ -15,5 +15,5 @@ func TestAppNew(t *testing.T) {
a := view.NewApp(mock.NewMockConfig())
_ = a.Init("blee", 10)
- assert.Equal(t, 11, len(a.GetActions()))
+ assert.Equal(t, 12, a.GetActions().Len())
}
diff --git a/internal/view/browser.go b/internal/view/browser.go
index 932fbbb6..b6045a6d 100644
--- a/internal/view/browser.go
+++ b/internal/view/browser.go
@@ -17,7 +17,7 @@ import (
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
"github.com/derailed/tcell/v2"
@@ -35,6 +35,7 @@ type Browser struct {
contextFn ContextFunc
cancelFn context.CancelFunc
mx sync.RWMutex
+ updating bool
}
// NewBrowser returns a new browser.
@@ -44,6 +45,18 @@ func NewBrowser(gvr client.GVR) ResourceViewer {
}
}
+func (b *Browser) setUpdating(f bool) {
+ b.mx.Lock()
+ defer b.mx.Unlock()
+ b.updating = f
+}
+
+func (b *Browser) getUpdating() bool {
+ b.mx.RLock()
+ defer b.mx.RUnlock()
+ return b.updating
+}
+
// Init watches all running pods in given namespace.
func (b *Browser) Init(ctx context.Context) error {
var err error
@@ -51,8 +64,8 @@ func (b *Browser) Init(ctx context.Context) error {
if err != nil {
return err
}
- colorerFn := render.DefaultColorer
- if r, ok := model.Registry[b.GVR().String()]; ok {
+ colorerFn := model1.DefaultColorer
+ if r, ok := model.Registry[b.GVR().String()]; ok && r.Renderer != nil {
colorerFn = r.Renderer.ColorerFunc()
}
b.GetTable().SetColorerFn(colorerFn)
@@ -118,8 +131,8 @@ func (b *Browser) suggestFilter() model.SuggestionFunc {
}
}
-func (b *Browser) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (b *Browser) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", b.resetCmd, false),
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", b.filterCmd, false),
tcell.KeyHelp: ui.NewSharedKeyAction("Help", b.helpCmd, false),
@@ -179,7 +192,7 @@ func (b *Browser) BufferChanged(_, _ string) {}
// BufferCompleted indicates input was accepted.
func (b *Browser) BufferCompleted(text, _ string) {
- if ui.IsLabelSelector(text) {
+ if internal.IsLabelSelector(text) {
b.GetModel().SetLabelFilter(ui.TrimLabelSelector(text))
} else {
b.GetModel().SetLabelFilter("")
@@ -191,26 +204,48 @@ func (b *Browser) BufferActive(state bool, k model.BufferKind) {
if state {
return
}
- if err := b.GetModel().Refresh(b.prepareContext()); err != nil {
+ if err := b.GetModel().Refresh(b.GetContext()); err != nil {
log.Error().Err(err).Msgf("Refresh failed for %s", b.GVR())
}
+ data := b.GetModel().Peek()
+ cdata := b.Update(data, b.App().Conn().HasMetrics())
b.app.QueueUpdateDraw(func() {
- b.Update(b.GetModel().Peek(), b.App().Conn().HasMetrics())
+ if b.getUpdating() {
+ return
+ }
+ b.setUpdating(true)
+ defer b.setUpdating(false)
+ b.UpdateUI(cdata, data)
if b.GetRowCount() > 1 {
b.App().filterHistory.Push(b.CmdBuff().GetText())
}
+
})
}
func (b *Browser) prepareContext() context.Context {
ctx := b.defaultContext()
- ctx, b.cancelFn = context.WithCancel(ctx)
+
+ b.mx.Lock()
+ {
+ if b.cancelFn != nil {
+ b.cancelFn()
+ }
+ ctx, b.cancelFn = context.WithCancel(ctx)
+ }
+ b.mx.Unlock()
+
if b.contextFn != nil {
ctx = b.contextFn(ctx)
}
if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" {
b.Path = path
}
+ b.mx.Lock()
+ {
+ b.SetContext(ctx)
+ }
+ b.mx.Unlock()
return ctx
}
@@ -237,7 +272,7 @@ func (b *Browser) Aliases() map[string]struct{} {
// Model Protocol...
// TableDataChanged notifies view new data is available.
-func (b *Browser) TableDataChanged(data *render.TableData) {
+func (b *Browser) TableDataChanged(data *model1.TableData) {
var cancel context.CancelFunc
b.mx.RLock()
cancel = b.cancelFn
@@ -247,9 +282,15 @@ func (b *Browser) TableDataChanged(data *render.TableData) {
return
}
+ cdata := b.Update(data, b.app.Conn().HasMetrics())
b.app.QueueUpdateDraw(func() {
+ if b.getUpdating() {
+ return
+ }
+ b.setUpdating(true)
+ defer b.setUpdating(false)
b.refreshActions()
- b.Update(data, b.app.Conn().HasMetrics())
+ b.UpdateUI(cdata, data)
})
}
@@ -287,14 +328,17 @@ func (b *Browser) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
if !b.CmdBuff().InCmdMode() {
+ hasFilter := !b.CmdBuff().Empty()
b.CmdBuff().ClearText(false)
- b.GetModel().SetLabelFilter("")
+ if hasFilter {
+ b.GetModel().SetLabelFilter("")
+ b.Refresh()
+ }
return b.App().PrevCmd(evt)
-
}
b.CmdBuff().Reset()
- if ui.IsLabelSelector(b.CmdBuff().GetText()) {
+ if internal.IsLabelSelector(b.CmdBuff().GetText()) {
b.Start()
}
b.Refresh()
@@ -308,7 +352,7 @@ func (b *Browser) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
}
b.CmdBuff().SetActive(false)
- if ui.IsLabelSelector(b.CmdBuff().GetText()) {
+ if internal.IsLabelSelector(b.CmdBuff().GetText()) {
b.Start()
return nil
}
@@ -471,7 +515,7 @@ func (b *Browser) defaultContext() context.Context {
ctx := context.WithValue(context.Background(), internal.KeyFactory, b.app.factory)
ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR())
ctx = context.WithValue(ctx, internal.KeyPath, b.Path)
- if ui.IsLabelSelector(b.CmdBuff().GetText()) {
+ if internal.IsLabelSelector(b.CmdBuff().GetText()) {
ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.CmdBuff().GetText()))
}
ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace()))
@@ -484,41 +528,41 @@ func (b *Browser) refreshActions() {
if b.App().Content.Top() != nil && b.App().Content.Top().Name() != b.Name() {
return
}
- aa := ui.KeyActions{
+ aa := ui.NewKeyActionsFromMap(ui.KeyMap{
ui.KeyC: ui.NewKeyAction("Copy", b.cpCmd, false),
tcell.KeyEnter: ui.NewKeyAction("View", b.enterCmd, false),
tcell.KeyCtrlR: ui.NewKeyAction("Refresh", b.refreshCmd, false),
- }
+ })
if b.app.ConOK() {
b.namespaceActions(aa)
if !b.app.Config.K9s.IsReadOnly() {
if client.Can(b.meta.Verbs, "edit") {
- aa[ui.KeyE] = ui.NewKeyActionWithOpts("Edit", b.editCmd,
+ aa.Add(ui.KeyE, ui.NewKeyActionWithOpts("Edit", b.editCmd,
ui.ActionOpts{
Visible: true,
Dangerous: true,
- })
+ }))
}
if client.Can(b.meta.Verbs, "delete") {
- aa[tcell.KeyCtrlD] = ui.NewKeyActionWithOpts("Delete", b.deleteCmd,
+ aa.Add(tcell.KeyCtrlD, ui.NewKeyActionWithOpts("Delete", b.deleteCmd,
ui.ActionOpts{
Visible: true,
Dangerous: true,
- })
+ }))
}
} else {
b.Actions().ClearDanger()
}
}
if !dao.IsK9sMeta(b.meta) {
- aa[ui.KeyY] = ui.NewKeyAction(yamlAction, b.viewCmd, true)
- aa[ui.KeyD] = ui.NewKeyAction("Describe", b.describeCmd, true)
+ aa.Add(ui.KeyY, ui.NewKeyAction(yamlAction, b.viewCmd, true))
+ aa.Add(ui.KeyD, ui.NewKeyAction("Describe", b.describeCmd, true))
}
for _, f := range b.bindKeysFn {
f(aa)
}
- b.Actions().Add(aa)
+ b.Actions().Merge(aa)
if err := pluginActions(b, b.Actions()); err != nil {
log.Warn().Msgf("Plugins load failed: %s", err)
@@ -528,25 +572,24 @@ func (b *Browser) refreshActions() {
log.Warn().Msgf("Hotkeys load failed: %s", err)
b.app.Logo().Warn("HotKeys load failed!")
}
-
b.app.Menu().HydrateMenu(b.Hints())
}
-func (b *Browser) namespaceActions(aa ui.KeyActions) {
+func (b *Browser) namespaceActions(aa *ui.KeyActions) {
if !b.meta.Namespaced || b.GetTable().Path != "" {
return
}
- aa[ui.KeyN] = ui.NewKeyAction("Copy Namespace", b.cpNsCmd, false)
+ aa.Add(ui.KeyN, ui.NewKeyAction("Copy Namespace", b.cpNsCmd, false))
b.namespaces = make(map[int]string, data.MaxFavoritesNS)
- aa[ui.Key0] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true)
+ aa.Add(ui.Key0, ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true))
b.namespaces[0] = client.NamespaceAll
index := 1
for _, ns := range b.app.Config.FavNamespaces() {
if ns == client.NamespaceAll {
continue
}
- aa[ui.NumKeys[index]] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true)
+ aa.Add(ui.NumKeys[index], ui.NewKeyAction(ns, b.switchNamespaceCmd, true))
b.namespaces[index] = ns
index++
}
diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go
index 98512a9a..3ff7cd83 100644
--- a/internal/view/cluster_info.go
+++ b/internal/view/cluster_info.go
@@ -111,14 +111,9 @@ func (c *ClusterInfo) warnCell(s string, w bool) string {
// ClusterInfoChanged notifies the cluster meta was changed.
func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) {
c.app.QueueUpdateDraw(func() {
- var ic = " ✏️"
- if c.app.Config.K9s.IsReadOnly() {
- ic = " 🔒"
- }
-
c.Clear()
c.layout()
- row := c.setCell(0, curr.Context+ic)
+ row := c.setCell(0, curr.Context)
row = c.setCell(row, curr.Cluster)
row = c.setCell(row, curr.User)
if curr.K9sLatest != "" {
diff --git a/internal/view/cm.go b/internal/view/cm.go
index 05c36da2..32d3e47c 100644
--- a/internal/view/cm.go
+++ b/internal/view/cm.go
@@ -28,10 +28,8 @@ func NewConfigMap(gvr client.GVR) ResourceViewer {
return &s
}
-func (s *ConfigMap) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
- ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true),
- })
+func (s *ConfigMap) bindKeys(aa *ui.KeyActions) {
+ aa.Add(ui.KeyU, ui.NewKeyAction("UsedBy", s.refCmd, true))
}
func (s *ConfigMap) refCmd(evt *tcell.EventKey) *tcell.EventKey {
diff --git a/internal/view/command.go b/internal/view/command.go
index 9a8d3355..bb437355 100644
--- a/internal/view/command.go
+++ b/internal/view/command.go
@@ -157,7 +157,7 @@ func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack bool) error {
if context, ok := p.HasContext(); ok {
if context != c.app.Config.ActiveContextName() {
- if err := c.app.Config.Save(); err != nil {
+ if err := c.app.Config.Save(true); err != nil {
log.Error().Err(err).Msg("config save failed!")
} else {
log.Debug().Msgf("Saved context config for: %q", context)
diff --git a/internal/view/container.go b/internal/view/container.go
index 3d25a78e..01f4f1e5 100644
--- a/internal/view/container.go
+++ b/internal/view/container.go
@@ -11,6 +11,7 @@ import (
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/port"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
@@ -38,25 +39,29 @@ func NewContainer(gvr client.GVR) ResourceViewer {
return &c
}
-func (c *Container) portForwardIndicator(data *render.TableData) {
+func (c *Container) portForwardIndicator(data *model1.TableData) {
ff := c.App().factory.Forwarders()
- col := data.IndexOfHeader("PF")
- for _, re := range data.RowEvents {
+ col, ok := data.IndexOfHeader("PF")
+ if !ok {
+ return
+ }
+ data.RowsRange(func(_ int, re model1.RowEvent) bool {
if ff.IsContainerForwarded(c.GetTable().Path, re.Row.ID) {
re.Row.Fields[col] = "[orange::b]Ⓕ"
}
- }
+ return true
+ })
}
-func (c *Container) decorateRows(data *render.TableData) {
+func (c *Container) decorateRows(data *model1.TableData) {
decorateCpuMemHeaderRows(c.App(), data)
}
// Name returns the component name.
func (c *Container) Name() string { return containerTitle }
-func (c *Container) bindDangerousKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (c *Container) bindDangerousKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyS: ui.NewKeyActionWithOpts(
"Shell",
c.shellCmd,
@@ -74,25 +79,25 @@ func (c *Container) bindDangerousKeys(aa ui.KeyActions) {
})
}
-func (c *Container) bindKeys(aa ui.KeyActions) {
+func (c *Container) bindKeys(aa *ui.KeyActions) {
aa.Delete(tcell.KeyCtrlSpace, ui.KeySpace)
if !c.App().Config.K9s.IsReadOnly() {
c.bindDangerousKeys(aa)
}
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyF: ui.NewKeyAction("Show PortForward", c.showPFCmd, true),
ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true),
ui.KeyShiftT: ui.NewKeyAction("Sort Restart", c.GetTable().SortColCmd("RESTARTS", false), false),
})
- aa.Add(resourceSorters(c.GetTable()))
+ aa.Merge(resourceSorters(c.GetTable()))
}
func (c *Container) k9sEnv() Env {
path := c.GetTable().GetSelectedItem()
row := c.GetTable().GetSelectedRow(path)
- env := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header, row)
+ env := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header(), row)
env["NAMESPACE"], env["POD"] = client.Namespaced(c.GetTable().Path)
return env
diff --git a/internal/view/context.go b/internal/view/context.go
index 22266194..4ba51bf9 100644
--- a/internal/view/context.go
+++ b/internal/view/context.go
@@ -37,11 +37,9 @@ func NewContext(gvr client.GVR) ResourceViewer {
return &c
}
-func (c *Context) bindKeys(aa ui.KeyActions) {
+func (c *Context) bindKeys(aa *ui.KeyActions) {
aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace)
- aa.Add(ui.KeyActions{
- ui.KeyR: ui.NewKeyAction("Rename", c.renameCmd, true),
- })
+ aa.Add(ui.KeyR, ui.NewKeyAction("Rename", c.renameCmd, true))
}
func (c *Context) renameCmd(evt *tcell.EventKey) *tcell.EventKey {
diff --git a/internal/view/cow.go b/internal/view/cow.go
index 6920f4da..75b71b11 100644
--- a/internal/view/cow.go
+++ b/internal/view/cow.go
@@ -19,7 +19,7 @@ import (
type Cow struct {
*tview.TextView
- actions ui.KeyActions
+ actions *ui.KeyActions
app *App
says string
}
@@ -29,7 +29,7 @@ func NewCow(app *App, says string) *Cow {
return &Cow{
TextView: tview.NewTextView(),
app: app,
- actions: make(ui.KeyActions),
+ actions: ui.NewKeyActions(),
says: says,
}
}
@@ -88,13 +88,11 @@ func cowTalk(says string, w int) string {
}
func (c *Cow) bindKeys() {
- c.actions.Set(ui.KeyActions{
- tcell.KeyEscape: ui.NewKeyAction("Back", c.resetCmd, false),
- })
+ c.actions.Add(tcell.KeyEscape, ui.NewKeyAction("Back", c.resetCmd, false))
}
func (c *Cow) keyboard(evt *tcell.EventKey) *tcell.EventKey {
- if a, ok := c.actions[ui.AsKey(evt)]; ok {
+ if a, ok := c.actions.Get(ui.AsKey(evt)); ok {
return a.Action(evt)
}
@@ -113,7 +111,7 @@ func (c *Cow) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
}
// Actions returns menu actions.
-func (c *Cow) Actions() ui.KeyActions {
+func (c *Cow) Actions() *ui.KeyActions {
return c.actions
}
diff --git a/internal/view/crd.go b/internal/view/crd.go
new file mode 100644
index 00000000..7ff1a1f9
--- /dev/null
+++ b/internal/view/crd.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright Authors of K9s
+
+package view
+
+import (
+ "github.com/derailed/k9s/internal/client"
+ "github.com/derailed/k9s/internal/ui"
+)
+
+// CRD represents a crd viewer.
+type CRD struct {
+ ResourceViewer
+}
+
+// NewCRD returns a new viewer.
+func NewCRD(gvr client.GVR) ResourceViewer {
+ s := CRD{
+ ResourceViewer: NewBrowser(gvr),
+ }
+ s.AddBindKeysFn(s.bindKeys)
+ s.GetTable().SetEnterFn(s.showCRD)
+
+ return &s
+}
+
+func (s *CRD) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
+ ui.KeyShiftV: ui.NewKeyAction("Sort Versions", s.GetTable().SortColCmd("VERSIONS", false), true),
+ ui.KeyShiftR: ui.NewKeyAction("Sort Group", s.GetTable().SortColCmd("GROUP", true), true),
+ ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.GetTable().SortColCmd("KIND", true), true),
+ })
+}
+
+func (s *CRD) showCRD(app *App, _ ui.Tabular, _ client.GVR, path string) {
+ _, crd := client.Namespaced(path)
+ app.gotoResource(crd, "", false)
+}
diff --git a/internal/view/cronjob.go b/internal/view/cronjob.go
index 90943a81..864a3381 100644
--- a/internal/view/cronjob.go
+++ b/internal/view/cronjob.go
@@ -71,8 +71,8 @@ func jobCtx(path, uid string) ContextFunc {
}
}
-func (c *CronJob) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (c *CronJob) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyT: ui.NewKeyAction("Trigger", c.triggerCmd, true),
ui.KeyS: ui.NewKeyAction("Suspend/Resume", c.toggleSuspendCmd, true),
ui.KeyShiftL: ui.NewKeyAction("Sort LastScheduled", c.GetTable().SortColCmd(lastScheduledCol, true), false),
diff --git a/internal/view/details.go b/internal/view/details.go
index 5ad7b4fb..c07c6171 100644
--- a/internal/view/details.go
+++ b/internal/view/details.go
@@ -28,7 +28,7 @@ type Details struct {
*tview.Flex
text *tview.TextView
- actions ui.KeyActions
+ actions *ui.KeyActions
app *App
title, subject string
cmdBuff *model.FishBuff
@@ -47,7 +47,7 @@ func NewDetails(app *App, title, subject, contentType string, searchable bool) *
app: app,
title: title,
subject: subject,
- actions: make(ui.KeyActions),
+ actions: ui.NewKeyActions(),
cmdBuff: model.NewFishBuff('/', model.FilterBuffer),
model: model.NewText(),
searchable: searchable,
@@ -132,7 +132,7 @@ func (d *Details) BufferActive(state bool, k model.BufferKind) {
}
func (d *Details) bindKeys() {
- d.actions.Set(ui.KeyActions{
+ d.actions.Bulk(ui.KeyMap{
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", d.filterCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Back", d.resetCmd, false),
tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false),
@@ -150,7 +150,7 @@ func (d *Details) bindKeys() {
}
func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey {
- if a, ok := d.actions[ui.AsKey(evt)]; ok {
+ if a, ok := d.actions.Get(ui.AsKey(evt)); ok {
return a.Action(evt)
}
@@ -181,7 +181,7 @@ func (d *Details) SetSubject(s string) {
}
// Actions returns menu actions.
-func (d *Details) Actions() ui.KeyActions {
+func (d *Details) Actions() *ui.KeyActions {
return d.actions
}
diff --git a/internal/view/dir.go b/internal/view/dir.go
index dae5ca72..bb34f682 100644
--- a/internal/view/dir.go
+++ b/internal/view/dir.go
@@ -60,8 +60,8 @@ func (d *Dir) dirContext(ctx context.Context) context.Context {
return context.WithValue(ctx, internal.KeyPath, d.path)
}
-func (d *Dir) bindDangerousKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (d *Dir) bindDangerousKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyA: ui.NewKeyActionWithOpts("Apply", d.applyCmd, ui.ActionOpts{
Visible: true,
Dangerous: true,
@@ -77,14 +77,14 @@ func (d *Dir) bindDangerousKeys(aa ui.KeyActions) {
})
}
-func (d *Dir) bindKeys(aa ui.KeyActions) {
+func (d *Dir) bindKeys(aa *ui.KeyActions) {
// !!BOZO!! Lame!
aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ)
if !d.App().Config.K9s.IsReadOnly() {
d.bindDangerousKeys(aa)
}
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyY: ui.NewKeyAction(yamlAction, d.viewCmd, true),
tcell.KeyEnter: ui.NewKeyAction("Goto", d.gotoCmd, true),
})
diff --git a/internal/view/dp.go b/internal/view/dp.go
index 11decf20..f9cd93d6 100644
--- a/internal/view/dp.go
+++ b/internal/view/dp.go
@@ -40,8 +40,8 @@ func NewDeploy(gvr client.GVR) ResourceViewer {
return &d
}
-func (d *Deploy) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (d *Deploy) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(readyCol, true), false),
ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(uptodateCol, true), false),
ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(availCol, true), false),
diff --git a/internal/view/ds.go b/internal/view/ds.go
index 4bb9dd6c..a9e24abd 100644
--- a/internal/view/ds.go
+++ b/internal/view/ds.go
@@ -33,8 +33,8 @@ func NewDaemonSet(gvr client.GVR) ResourceViewer {
return &d
}
-func (d *DaemonSet) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (d *DaemonSet) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyShiftD: ui.NewKeyAction("Sort Desired", d.GetTable().SortColCmd("DESIRED", true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Current", d.GetTable().SortColCmd("CURRENT", true), false),
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(readyCol, true), false),
diff --git a/internal/view/event.go b/internal/view/event.go
index 6de78f30..b75c975a 100644
--- a/internal/view/event.go
+++ b/internal/view/event.go
@@ -25,9 +25,9 @@ func NewEvent(gvr client.GVR) ResourceViewer {
return &e
}
-func (e *Event) bindKeys(aa ui.KeyActions) {
+func (e *Event) bindKeys(aa *ui.KeyActions) {
aa.Delete(tcell.KeyCtrlD, ui.KeyE, ui.KeyA)
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyShiftL: ui.NewKeyAction("Sort LastSeen", e.GetTable().SortColCmd("LAST SEEN", false), false),
ui.KeyShiftF: ui.NewKeyAction("Sort FirstSeen", e.GetTable().SortColCmd("FIRST SEEN", false), false),
ui.KeyShiftT: ui.NewKeyAction("Sort Type", e.GetTable().SortColCmd("TYPE", true), false),
diff --git a/internal/view/group.go b/internal/view/group.go
index 503b190c..0cfe42dd 100644
--- a/internal/view/group.go
+++ b/internal/view/group.go
@@ -26,9 +26,9 @@ func NewGroup(gvr client.GVR) ResourceViewer {
return &g
}
-func (g *Group) bindKeys(aa ui.KeyActions) {
+func (g *Group) bindKeys(aa *ui.KeyActions) {
aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace)
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
tcell.KeyEnter: ui.NewKeyAction("Rules", g.policyCmd, true),
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", g.GetTable().SortColCmd("KIND", true), false),
})
diff --git a/internal/view/helm_chart.go b/internal/view/helm_chart.go
index afa58e50..c3d595ba 100644
--- a/internal/view/helm_chart.go
+++ b/internal/view/helm_chart.go
@@ -37,9 +37,9 @@ func (c *HelmChart) chartContext(ctx context.Context) context.Context {
return ctx
}
-func (c *HelmChart) bindKeys(aa ui.KeyActions) {
+func (c *HelmChart) bindKeys(aa *ui.KeyActions) {
aa.Delete(tcell.KeyCtrlS)
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyR: ui.NewKeyAction("Releases", c.historyCmd, true),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false),
})
diff --git a/internal/view/helm_history.go b/internal/view/helm_history.go
index 0949d5dd..a2b5e5a9 100644
--- a/internal/view/helm_history.go
+++ b/internal/view/helm_history.go
@@ -53,13 +53,13 @@ func (h *History) HistoryContext(ctx context.Context) context.Context {
return ctx
}
-func (h *History) bindKeys(aa ui.KeyActions) {
+func (h *History) bindKeys(aa *ui.KeyActions) {
if !h.App().Config.K9s.IsReadOnly() {
h.bindDangerousKeys(aa)
}
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace, tcell.KeyCtrlD)
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyShiftN: ui.NewKeyAction("Sort Revision", h.GetTable().SortColCmd("REVISION", true), false),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", h.GetTable().SortColCmd("STATUS", true), false),
ui.KeyShiftA: ui.NewKeyAction("Sort Age", h.GetTable().SortColCmd("AGE", true), false),
@@ -81,17 +81,13 @@ func (h *History) getValsCmd(app *App, _ ui.Tabular, _ client.GVR, path string)
}
}
-func (h *History) bindDangerousKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
- ui.KeyR: ui.NewKeyActionWithOpts(
- "RollBackTo...",
- h.rollbackCmd,
- ui.ActionOpts{
- Visible: true,
- Dangerous: true,
- },
- ),
- })
+func (h *History) bindDangerousKeys(aa *ui.KeyActions) {
+ aa.Add(ui.KeyR, ui.NewKeyActionWithOpts("RollBackTo...", h.rollbackCmd,
+ ui.ActionOpts{
+ Visible: true,
+ Dangerous: true,
+ },
+ ))
}
func (h *History) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey {
diff --git a/internal/view/help.go b/internal/view/help.go
index 06665e7b..4347a43c 100644
--- a/internal/view/help.go
+++ b/internal/view/help.go
@@ -77,7 +77,7 @@ func (h *Help) StylesChanged(s *config.Styles) {
func (h *Help) bindKeys() {
h.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS, ui.KeySlash)
- h.Actions().Set(ui.KeyActions{
+ h.Actions().Bulk(ui.KeyMap{
tcell.KeyEscape: ui.NewKeyAction("Back", h.app.PrevCmd, true),
ui.KeyHelp: ui.NewKeyAction("Back", h.app.PrevCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Back", h.app.PrevCmd, false),
diff --git a/internal/view/helpers.go b/internal/view/helpers.go
index 70605969..8ec027a9 100644
--- a/internal/view/helpers.go
+++ b/internal/view/helpers.go
@@ -16,6 +16,7 @@ import (
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/view/cmd"
@@ -106,16 +107,16 @@ func k8sEnv(c *client.Config) Env {
}
}
-func defaultEnv(c *client.Config, path string, header render.Header, row *render.Row) Env {
+func defaultEnv(c *client.Config, path string, header model1.Header, row *model1.Row) Env {
env := k8sEnv(c)
env["NAMESPACE"], env["NAME"] = client.Namespaced(path)
if row == nil {
return env
}
- for _, col := range header.Columns(true) {
- i := header.IndexOf(col, true)
- if i >= 0 && i < len(row.Fields) {
- env["COL-"+col] = row.Fields[i]
+ for _, col := range header.ColumnNames(true) {
+ idx, ok := header.IndexOf(col, true)
+ if ok && idx < len(row.Fields) {
+ env["COL-"+col] = row.Fields[idx]
}
}
@@ -218,8 +219,8 @@ func fqn(ns, n string) string {
return ns + "/" + n
}
-func decorateCpuMemHeaderRows(app *App, data *render.TableData) {
- for colIndex, header := range data.Header {
+func decorateCpuMemHeaderRows(app *App, data *model1.TableData) {
+ for colIndex, header := range data.Header() {
var check string
if header.Name == "%CPU/L" {
check = "cpu"
@@ -230,26 +231,28 @@ func decorateCpuMemHeaderRows(app *App, data *render.TableData) {
if len(check) == 0 {
continue
}
- for _, re := range data.RowEvents {
+ data.RowsRange(func(_ int, re model1.RowEvent) bool {
if re.Row.Fields[colIndex] == render.NAValue {
- continue
+ return true
}
n, err := strconv.Atoi(re.Row.Fields[colIndex])
if err != nil {
- continue
+ return true
}
if n > 100 {
n = 100
}
severity := app.Config.K9s.Thresholds.LevelFor(check, n)
if severity == config.SeverityLow {
- continue
+ return true
}
color := app.Config.K9s.Thresholds.SeverityColor(check, n)
if len(color) > 0 {
re.Row.Fields[colIndex] = "[" + color + "::b]" + re.Row.Fields[colIndex]
}
- }
+
+ return true
+ })
}
}
diff --git a/internal/view/helpers_test.go b/internal/view/helpers_test.go
index 9ec347c8..5c2ddbe2 100644
--- a/internal/view/helpers_test.go
+++ b/internal/view/helpers_test.go
@@ -12,6 +12,7 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/mock"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/tcell/v2"
"github.com/rs/zerolog"
@@ -149,12 +150,12 @@ func TestK9sEnv(t *testing.T) {
KubeConfig: &cfg,
}
c := client.NewConfig(&flags)
- h := render.Header{
+ h := model1.Header{
{Name: "A"},
{Name: "B"},
{Name: "C"},
}
- r := render.Row{
+ r := model1.Row{
Fields: []string{"a1", "b1", "c1"},
}
env := defaultEnv(c, "fred/blee", h, &r)
diff --git a/internal/view/image_extender.go b/internal/view/image_extender.go
index b67764c6..bc8f2a7b 100644
--- a/internal/view/image_extender.go
+++ b/internal/view/image_extender.go
@@ -56,13 +56,11 @@ func NewImageExtender(r ResourceViewer) ResourceViewer {
return &s
}
-func (s *ImageExtender) bindKeys(aa ui.KeyActions) {
+func (s *ImageExtender) bindKeys(aa *ui.KeyActions) {
if s.App().Config.K9s.IsReadOnly() {
return
}
- aa.Add(ui.KeyActions{
- ui.KeyI: ui.NewKeyAction("Set Image", s.setImageCmd, false),
- })
+ aa.Add(ui.KeyI, ui.NewKeyAction("Set Image", s.setImageCmd, false))
}
func (s *ImageExtender) setImageCmd(evt *tcell.EventKey) *tcell.EventKey {
diff --git a/internal/view/img_scan.go b/internal/view/img_scan.go
index f4f5290b..58b1002d 100644
--- a/internal/view/img_scan.go
+++ b/internal/view/img_scan.go
@@ -41,10 +41,10 @@ func NewImageScan(gvr client.GVR) ResourceViewer {
// Name returns the component name.
func (s *ImageScan) Name() string { return imgScanTitle }
-func (c *ImageScan) bindKeys(aa ui.KeyActions) {
+func (c *ImageScan) bindKeys(aa *ui.KeyActions) {
aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlZ, tcell.KeyCtrlW)
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyShiftL: ui.NewKeyAction("Sort Lib", c.GetTable().SortColCmd("LIBRARY", false), true),
ui.KeyShiftS: ui.NewKeyAction("Sort Severity", c.GetTable().SortColCmd("SEVERITY", false), true),
ui.KeyShiftF: ui.NewKeyAction("Sort Fixed-in", c.GetTable().SortColCmd("FIXED-IN", false), true),
diff --git a/internal/view/live_view.go b/internal/view/live_view.go
index e97a0bc0..a928f2e2 100644
--- a/internal/view/live_view.go
+++ b/internal/view/live_view.go
@@ -31,7 +31,7 @@ type LiveView struct {
title string
model model.ResourceViewer
text *tview.TextView
- actions ui.KeyActions
+ actions *ui.KeyActions
app *App
cmdBuff *model.FishBuff
currentRegion, maxRegions int
@@ -48,7 +48,7 @@ func NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView {
text: tview.NewTextView(),
app: app,
title: title,
- actions: make(ui.KeyActions),
+ actions: ui.NewKeyActions(),
currentRegion: 0,
maxRegions: 0,
cmdBuff: model.NewFishBuff('/', model.FilterBuffer),
@@ -139,7 +139,7 @@ func (v *LiveView) BufferActive(state bool, k model.BufferKind) {
}
func (v *LiveView) bindKeys() {
- v.actions.Set(ui.KeyActions{
+ v.actions.Bulk(ui.KeyMap{
tcell.KeyEnter: ui.NewSharedKeyAction("Filter", v.filterCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Back", v.resetCmd, false),
tcell.KeyCtrlS: ui.NewKeyAction("Save", v.saveCmd, false),
@@ -153,19 +153,13 @@ func (v *LiveView) bindKeys() {
})
if !v.app.Config.K9s.IsReadOnly() {
- v.actions.Add(ui.KeyActions{
- ui.KeyE: ui.NewKeyAction("Edit", v.editCmd, true),
- })
+ v.actions.Add(ui.KeyE, ui.NewKeyAction("Edit", v.editCmd, true))
}
if v.title == yamlAction {
- v.actions.Add(ui.KeyActions{
- ui.KeyM: ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true),
- })
+ v.actions.Add(ui.KeyM, ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true))
}
if v.model != nil && v.model.GVR().IsDecodable() {
- v.actions.Add(ui.KeyActions{
- ui.KeyX: ui.NewKeyAction("Toggle Decode", v.toggleEncodedDecodedCmd, true),
- })
+ v.actions.Add(ui.KeyX, ui.NewKeyAction("Toggle Decode", v.toggleEncodedDecodedCmd, true))
}
}
@@ -210,7 +204,7 @@ func (v *LiveView) toggleRefreshCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (v *LiveView) keyboard(evt *tcell.EventKey) *tcell.EventKey {
- if a, ok := v.actions[ui.AsKey(evt)]; ok {
+ if a, ok := v.actions.Get(ui.AsKey(evt)); ok {
return a.Action(evt)
}
@@ -225,7 +219,7 @@ func (v *LiveView) StylesChanged(s *config.Styles) {
}
// Actions returns menu actions.
-func (v *LiveView) Actions() ui.KeyActions {
+func (v *LiveView) Actions() *ui.KeyActions {
return v.actions
}
diff --git a/internal/view/log.go b/internal/view/log.go
index a5df9bce..be01ed5a 100644
--- a/internal/view/log.go
+++ b/internal/view/log.go
@@ -242,7 +242,7 @@ func (l *Log) Stop() {
func (l *Log) Name() string { return logTitle }
func (l *Log) bindKeys() {
- l.logs.Actions().Set(ui.KeyActions{
+ l.logs.Actions().Bulk(ui.KeyMap{
ui.Key0: ui.NewKeyAction("tail", l.sinceCmd(-1), true),
ui.Key1: ui.NewKeyAction("head", l.sinceCmd(0), true),
ui.Key2: ui.NewKeyAction("1m", l.sinceCmd(60), true),
@@ -262,9 +262,7 @@ func (l *Log) bindKeys() {
ui.KeyC: ui.NewKeyAction("Copy", cpCmd(l.app.Flash(), l.logs.TextView), true),
})
if l.model.HasDefaultContainer() {
- l.logs.Actions().Set(ui.KeyActions{
- ui.KeyA: ui.NewKeyAction("Toggle AllContainers", l.toggleAllContainers, true),
- })
+ l.logs.Actions().Add(ui.KeyA, ui.NewKeyAction("Toggle AllContainers", l.toggleAllContainers, true))
}
}
diff --git a/internal/view/log_test.go b/internal/view/log_test.go
index 6b466cf8..5db3fcaf 100644
--- a/internal/view/log_test.go
+++ b/internal/view/log_test.go
@@ -5,7 +5,9 @@ package view_test
import (
"bytes"
+ "errors"
"fmt"
+ "io/fs"
"os"
"testing"
@@ -139,7 +141,7 @@ func TestAllContainerKeyBinding(t *testing.T) {
t.Run(k, func(t *testing.T) {
v := view.NewLog(client.NewGVR("v1/pods"), u.opts)
assert.NoError(t, v.Init(makeContext()))
- _, got := v.Logs().Actions()[ui.KeyA]
+ _, got := v.Logs().Actions().Get(ui.KeyA)
assert.Equal(t, u.e, got)
})
}
@@ -154,7 +156,7 @@ func makeApp() *view.App {
func ensureDumpDir(n string) error {
config.AppDumpsDir = n
- if _, err := os.Stat(n); os.IsNotExist(err) {
+ if _, err := os.Stat(n); errors.Is(err, fs.ErrNotExist) {
return os.MkdirAll(n, 0700)
}
if err := os.RemoveAll(n); err != nil {
diff --git a/internal/view/logger.go b/internal/view/logger.go
index 7e0526cf..7fc649f0 100644
--- a/internal/view/logger.go
+++ b/internal/view/logger.go
@@ -17,7 +17,7 @@ import (
type Logger struct {
*tview.TextView
- actions ui.KeyActions
+ actions *ui.KeyActions
app *App
title, subject string
cmdBuff *model.FishBuff
@@ -28,7 +28,7 @@ func NewLogger(app *App) *Logger {
return &Logger{
TextView: tview.NewTextView(),
app: app,
- actions: make(ui.KeyActions),
+ actions: ui.NewKeyActions(),
cmdBuff: model.NewFishBuff('/', model.FilterBuffer),
}
}
@@ -69,7 +69,7 @@ func (l *Logger) BufferActive(state bool, k model.BufferKind) {
}
func (l *Logger) bindKeys() {
- l.actions.Set(ui.KeyActions{
+ l.actions.Bulk(ui.KeyMap{
tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false),
tcell.KeyCtrlS: ui.NewKeyAction("Save", l.saveCmd, false),
ui.KeyC: ui.NewKeyAction("Copy", cpCmd(l.app.Flash(), l.TextView), true),
@@ -79,7 +79,7 @@ func (l *Logger) bindKeys() {
}
func (l *Logger) keyboard(evt *tcell.EventKey) *tcell.EventKey {
- if a, ok := l.actions[ui.AsKey(evt)]; ok {
+ if a, ok := l.actions.Get(ui.AsKey(evt)); ok {
return a.Action(evt)
}
@@ -99,7 +99,7 @@ func (l *Logger) SetSubject(s string) {
}
// Actions returns menu actions.
-func (l *Logger) Actions() ui.KeyActions {
+func (l *Logger) Actions() *ui.KeyActions {
return l.actions
}
diff --git a/internal/view/logs_extender.go b/internal/view/logs_extender.go
index 6c20a818..95e45211 100644
--- a/internal/view/logs_extender.go
+++ b/internal/view/logs_extender.go
@@ -29,8 +29,8 @@ func NewLogsExtender(v ResourceViewer, f LogOptionsFunc) ResourceViewer {
}
// BindKeys injects new menu actions.
-func (l *LogsExtender) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (l *LogsExtender) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyL: ui.NewKeyAction("Logs", l.logsCmd(false), true),
ui.KeyP: ui.NewKeyAction("Logs Previous", l.logsCmd(true), true),
})
diff --git a/internal/view/node.go b/internal/view/node.go
index daf9ac4d..31a20015 100644
--- a/internal/view/node.go
+++ b/internal/view/node.go
@@ -39,8 +39,8 @@ func (n *Node) nodeContext(ctx context.Context) context.Context {
return context.WithValue(ctx, internal.KeyPodCounting, !n.App().Config.K9s.DisablePodCounting)
}
-func (n *Node) bindDangerousKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (n *Node) bindDangerousKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyC: ui.NewKeyActionWithOpts(
"Cordon",
n.toggleCordonCmd(true),
@@ -72,18 +72,16 @@ func (n *Node) bindDangerousKeys(aa ui.KeyActions) {
return
}
if ct.FeatureGates.NodeShell {
- aa.Add(ui.KeyActions{
- ui.KeyS: ui.NewKeyAction("Shell", n.sshCmd, true),
- })
+ aa.Add(ui.KeyS, ui.NewKeyAction("Shell", n.sshCmd, true))
}
}
-func (n *Node) bindKeys(aa ui.KeyActions) {
+func (n *Node) bindKeys(aa *ui.KeyActions) {
if !n.App().Config.K9s.IsReadOnly() {
n.bindDangerousKeys(aa)
}
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyY: ui.NewKeyAction(yamlAction, n.yamlCmd, true),
ui.KeyShiftR: ui.NewKeyAction("Sort ROLE", n.GetTable().SortColCmd("ROLE", true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", n.GetTable().SortColCmd(cpuCol, false), false),
diff --git a/internal/view/ns.go b/internal/view/ns.go
index e86432fd..1eac09db 100644
--- a/internal/view/ns.go
+++ b/internal/view/ns.go
@@ -5,8 +5,7 @@ package view
import (
"github.com/derailed/k9s/internal/client"
- "github.com/derailed/k9s/internal/config/data"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tcell/v2"
)
@@ -33,8 +32,8 @@ func NewNamespace(gvr client.GVR) ResourceViewer {
return &n
}
-func (n *Namespace) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (n *Namespace) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyU: ui.NewKeyAction("Use", n.useNsCmd, true),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", n.GetTable().SortColCmd(statusCol, true), false),
})
@@ -70,32 +69,37 @@ func (n *Namespace) useNamespace(fqn string) {
}
}
-func (n *Namespace) decorate(td *render.TableData) {
- if n.App().Conn() == nil || len(td.RowEvents) == 0 {
+func (n *Namespace) decorate(td *model1.TableData) {
+ if n.App().Conn() == nil || td.RowCount() == 0 {
return
}
-
// checks if all ns is in the list if not add it.
- if _, ok := td.RowEvents.FindIndex(client.NamespaceAll); !ok {
- td.RowEvents = append(td.RowEvents,
- render.RowEvent{
- Kind: render.EventUnchanged,
- Row: render.Row{
- ID: client.NamespaceAll,
- Fields: render.Fields{client.NamespaceAll, "Active", "", "", ""},
- },
+ if _, ok := td.FindRow(client.NamespaceAll); !ok {
+ td.AddRow(model1.RowEvent{
+ Kind: model1.EventUnchanged,
+ Row: model1.Row{
+ ID: client.NamespaceAll,
+ Fields: model1.Fields{client.NamespaceAll, "Active", "", "", ""},
},
+ },
)
}
- for _, re := range td.RowEvents {
- if data.InList(n.App().Config.FavNamespaces(), re.Row.ID) {
- re.Row.Fields[0] += favNSIndicator
- re.Kind = render.EventUnchanged
- }
- if n.App().Config.ActiveNamespace() == re.Row.ID {
- re.Row.Fields[0] += defaultNSIndicator
- re.Kind = render.EventUnchanged
- }
+ favs := make(map[string]struct{})
+ for _, ns := range n.App().Config.FavNamespaces() {
+ favs[ns] = struct{}{}
}
+ ans := n.App().Config.ActiveNamespace()
+ td.RowsRange(func(i int, re model1.RowEvent) bool {
+ _, n := client.Namespaced(re.Row.ID)
+ if _, ok := favs[n]; ok {
+ re.Row.Fields[0] += favNSIndicator
+ }
+ if ans == re.Row.ID {
+ re.Row.Fields[0] += defaultNSIndicator
+ }
+ re.Kind = model1.EventUnchanged
+ td.SetRow(i, re)
+ return true
+ })
}
diff --git a/internal/view/pf.go b/internal/view/pf.go
index 40ded9d6..a92a1824 100644
--- a/internal/view/pf.go
+++ b/internal/view/pf.go
@@ -51,8 +51,8 @@ func (p *PortForward) portForwardContext(ctx context.Context) context.Context {
return ctx
}
-func (p *PortForward) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (p *PortForward) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
tcell.KeyEnter: ui.NewKeyAction("View Benchmarks", p.showBenchCmd, true),
ui.KeyB: ui.NewKeyAction("Benchmark Run/Stop", p.toggleBenchCmd, true),
tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, true),
diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go
index 627395f7..2e19cc26 100644
--- a/internal/view/pf_extender.go
+++ b/internal/view/pf_extender.go
@@ -36,8 +36,8 @@ func NewPortForwardExtender(r ResourceViewer) ResourceViewer {
return &p
}
-func (p *PortForwardExtender) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (p *PortForwardExtender) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyF: ui.NewKeyAction("Show PortForward", p.showPFCmd, true),
ui.KeyShiftF: ui.NewKeyAction("Port-Forward", p.portFwdCmd, true),
})
diff --git a/internal/view/picker.go b/internal/view/picker.go
index b16042be..56e02751 100644
--- a/internal/view/picker.go
+++ b/internal/view/picker.go
@@ -38,7 +38,7 @@ func (p *Picker) Init(ctx context.Context) error {
}
pickerView := app.Styles.Views().Picker
- p.actions[tcell.KeyEscape] = ui.NewKeyAction("Back", app.PrevCmd, true)
+ p.actions.Add(tcell.KeyEscape, ui.NewKeyAction("Back", app.PrevCmd, true))
p.SetBorder(true)
p.SetMainTextColor(pickerView.MainColor.Color())
@@ -48,7 +48,7 @@ func (p *Picker) Init(ctx context.Context) error {
p.SetTitle(" [aqua::b]Containers Picker ")
p.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey {
- if a, ok := p.actions[evt.Key()]; ok {
+ if a, ok := p.actions.Get(evt.Key()); ok {
a.Action(evt)
evt = nil
}
diff --git a/internal/view/pod.go b/internal/view/pod.go
index de1fc1fe..cdd2a924 100644
--- a/internal/view/pod.go
+++ b/internal/view/pod.go
@@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
+ "io/fs"
"os"
"strings"
@@ -14,6 +15,7 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
@@ -34,6 +36,7 @@ const (
osBetaSelector = "beta." + osSelector
trUpload = "Upload"
trDownload = "Download"
+ pfIndicator = "[orange::b]Ⓕ"
)
// Pod represents a pod viewer.
@@ -58,20 +61,25 @@ func NewPod(gvr client.GVR) ResourceViewer {
return &p
}
-func (p *Pod) portForwardIndicator(data *render.TableData) {
+func (p *Pod) portForwardIndicator(data *model1.TableData) {
ff := p.App().factory.Forwarders()
- col := data.IndexOfHeader("PF")
- for _, re := range data.RowEvents {
- if ff.IsPodForwarded(re.Row.ID) {
- re.Row.Fields[col] = "[orange::b]Ⓕ"
- }
+ defer decorateCpuMemHeaderRows(p.App(), data)
+ idx, ok := data.IndexOfHeader("PF")
+ if !ok {
+ return
}
- decorateCpuMemHeaderRows(p.App(), data)
+
+ data.RowsRange(func(_ int, re model1.RowEvent) bool {
+ if ff.IsPodForwarded(re.Row.ID) {
+ re.Row.Fields[idx] = pfIndicator
+ }
+ return true
+ })
}
-func (p *Pod) bindDangerousKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (p *Pod) bindDangerousKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
tcell.KeyCtrlK: ui.NewKeyActionWithOpts(
"Kill",
p.killCmd,
@@ -110,12 +118,12 @@ func (p *Pod) bindDangerousKeys(aa ui.KeyActions) {
})
}
-func (p *Pod) bindKeys(aa ui.KeyActions) {
+func (p *Pod) bindKeys(aa *ui.KeyActions) {
if !p.App().Config.K9s.IsReadOnly() {
p.bindDangerousKeys(aa)
}
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyO: ui.NewKeyAction("Show Node", p.showNode, true),
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(readyCol, true), false),
ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd("RESTARTS", false), false),
@@ -123,7 +131,7 @@ func (p *Pod) bindKeys(aa ui.KeyActions) {
ui.KeyShiftI: ui.NewKeyAction("Sort IP", p.GetTable().SortColCmd("IP", true), false),
ui.KeyShiftO: ui.NewKeyAction("Sort Node", p.GetTable().SortColCmd("NODE", true), false),
})
- aa.Add(resourceSorters(p.GetTable()))
+ aa.Merge(resourceSorters(p.GetTable()))
}
func (p *Pod) logOptions(prev bool) (*dao.LogOptions, error) {
@@ -307,7 +315,7 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey {
if !download {
local = from
}
- if _, err := os.Stat(local); !download && os.IsNotExist(err) {
+ if _, err := os.Stat(local); !download && errors.Is(err, fs.ErrNotExist) {
p.App().Flash().Err(err)
return false
}
@@ -555,13 +563,13 @@ func osFromSelector(s map[string]string) (string, bool) {
return os, ok
}
-func resourceSorters(t *Table) ui.KeyActions {
- return ui.KeyActions{
+func resourceSorters(t *Table) *ui.KeyActions {
+ return ui.NewKeyActionsFromMap(ui.KeyMap{
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", t.SortColCmd(cpuCol, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", t.SortColCmd(memCol, false), false),
ui.KeyShiftX: ui.NewKeyAction("Sort CPU/R", t.SortColCmd("%CPU/R", false), false),
ui.KeyShiftZ: ui.NewKeyAction("Sort MEM/R", t.SortColCmd("%MEM/R", false), false),
tcell.KeyCtrlX: ui.NewKeyAction("Sort CPU/L", t.SortColCmd("%CPU/L", false), false),
tcell.KeyCtrlQ: ui.NewKeyAction("Sort MEM/L", t.SortColCmd("%MEM/L", false), false),
- }
+ })
}
diff --git a/internal/view/policy.go b/internal/view/policy.go
index 28fee83e..b412b388 100644
--- a/internal/view/policy.go
+++ b/internal/view/policy.go
@@ -46,9 +46,9 @@ func (p *Policy) subjectCtx(ctx context.Context) context.Context {
return context.WithValue(ctx, internal.KeySubjectName, p.subjectName)
}
-func (p *Policy) bindKeys(aa ui.KeyActions) {
+func (p *Policy) bindKeys(aa *ui.KeyActions) {
aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace)
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.GetTable().SortColCmd(nameCol, true), false),
ui.KeyShiftA: ui.NewKeyAction("Sort Api-Group", p.GetTable().SortColCmd("API-GROUP", true), false),
ui.KeyShiftB: ui.NewKeyAction("Sort Binding", p.GetTable().SortColCmd("BINDING", true), false),
diff --git a/internal/view/popeye.go b/internal/view/popeye.go
index ca5c2737..e3c6355b 100644
--- a/internal/view/popeye.go
+++ b/internal/view/popeye.go
@@ -3,114 +3,114 @@
package view
-import (
- "context"
- "fmt"
- "strconv"
- "time"
+// import (
+// "context"
+// "fmt"
+// "strconv"
+// "time"
- "github.com/derailed/k9s/internal"
- "github.com/derailed/k9s/internal/client"
- "github.com/derailed/k9s/internal/render"
- "github.com/derailed/k9s/internal/ui"
- "github.com/derailed/tcell/v2"
-)
+// "github.com/derailed/k9s/internal"
+// "github.com/derailed/k9s/internal/client"
+// "github.com/derailed/k9s/internal/render"
+// "github.com/derailed/k9s/internal/ui"
+// "github.com/derailed/tcell/v2"
+// )
-// Popeye represents a sanitizer view.
-type Popeye struct {
- ResourceViewer
-}
+// // Popeye represents a sanitizer view.
+// type Popeye struct {
+// ResourceViewer
+// }
-// NewPopeye returns a new view.
-func NewPopeye(gvr client.GVR) ResourceViewer {
- p := Popeye{
- ResourceViewer: NewBrowser(gvr),
- }
- p.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen)
- p.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone))
- p.GetTable().SetSortCol("SCORE%", true)
- p.GetTable().SetDecorateFn(p.decorateRows)
- p.AddBindKeysFn(p.bindKeys)
+// // NewPopeye returns a new view.
+// func NewPopeye(gvr client.GVR) ResourceViewer {
+// p := Popeye{
+// ResourceViewer: NewBrowser(gvr),
+// }
+// p.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen)
+// p.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone))
+// p.GetTable().SetSortCol("SCORE%", true)
+// p.GetTable().SetDecorateFn(p.decorateRows)
+// p.AddBindKeysFn(p.bindKeys)
- return &p
-}
+// return &p
+// }
-// Init initializes the view.
-func (p *Popeye) Init(ctx context.Context) error {
- if err := p.ResourceViewer.Init(ctx); err != nil {
- return err
- }
- p.GetTable().GetModel().SetRefreshRate(5 * time.Second)
+// // Init initializes the view.
+// func (p *Popeye) Init(ctx context.Context) error {
+// if err := p.ResourceViewer.Init(ctx); err != nil {
+// return err
+// }
+// p.GetTable().GetModel().SetRefreshRate(5 * time.Second)
- return nil
-}
+// return nil
+// }
-func (p *Popeye) decorateRows(data *render.TableData) {
- var sum int
- for _, re := range data.RowEvents {
- n, err := strconv.Atoi(re.Row.Fields[1])
- if err != nil {
- continue
- }
- sum += n
- }
- score, letter := 0, render.NAValue
- if len(data.RowEvents) > 0 {
- score = sum / len(data.RowEvents)
- letter = grade(score)
- }
- p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, letter)
-}
+// func (p *Popeye) decorateRows(data *model1.TableData) {
+// var sum int
+// for _, re := range data.RowEvents {
+// n, err := strconv.Atoi(re.Row.Fields[1])
+// if err != nil {
+// continue
+// }
+// sum += n
+// }
+// score, letter := 0, render.NAValue
+// if len(data.RowEvents) > 0 {
+// score = sum / len(data.RowEvents)
+// letter = grade(score)
+// }
+// p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, letter)
+// }
-func (p *Popeye) bindKeys(aa ui.KeyActions) {
- aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
- aa.Add(ui.KeyActions{
- tcell.KeyEnter: ui.NewKeyAction("Goto", p.gotoCmd, true),
- ui.KeyShiftR: ui.NewKeyAction("Sort Resource", p.GetTable().SortColCmd("RESOURCE", true), false),
- ui.KeyShiftS: ui.NewKeyAction("Sort Score", p.GetTable().SortColCmd("SCORE%", true), false),
- ui.KeyShiftO: ui.NewKeyAction("Sort OK", p.GetTable().SortColCmd("OK", true), false),
- ui.KeyShiftI: ui.NewKeyAction("Sort Info", p.GetTable().SortColCmd("INFO", true), false),
- ui.KeyShiftW: ui.NewKeyAction("Sort Warning", p.GetTable().SortColCmd("WARNING", true), false),
- ui.KeyShiftE: ui.NewKeyAction("Sort Error", p.GetTable().SortColCmd("ERROR", true), false),
- })
-}
+// func (p *Popeye) bindKeys(aa ui.KeyActions) {
+// aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
+// aa.Add(ui.KeyActions{
+// tcell.KeyEnter: ui.NewKeyAction("Goto", p.gotoCmd, true),
+// ui.KeyShiftR: ui.NewKeyAction("Sort Resource", p.GetTable().SortColCmd("RESOURCE", true), false),
+// ui.KeyShiftS: ui.NewKeyAction("Sort Score", p.GetTable().SortColCmd("SCORE%", true), false),
+// ui.KeyShiftO: ui.NewKeyAction("Sort OK", p.GetTable().SortColCmd("OK", true), false),
+// ui.KeyShiftI: ui.NewKeyAction("Sort Info", p.GetTable().SortColCmd("INFO", true), false),
+// ui.KeyShiftW: ui.NewKeyAction("Sort Warning", p.GetTable().SortColCmd("WARNING", true), false),
+// ui.KeyShiftE: ui.NewKeyAction("Sort Error", p.GetTable().SortColCmd("ERROR", true), false),
+// })
+// }
-func (p *Popeye) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
- path := p.GetTable().GetSelectedItem()
- if path == "" {
- return evt
- }
- v := NewSanitizer(client.NewGVR("sanitizer"))
- v.SetContextFn(sanitizerCtx(path))
- if err := p.App().inject(v, false); err != nil {
- p.App().Flash().Err(err)
- }
+// func (p *Popeye) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
+// path := p.GetTable().GetSelectedItem()
+// if path == "" {
+// return evt
+// }
+// v := NewSanitizer(client.NewGVR("sanitizer"))
+// v.SetContextFn(sanitizerCtx(path))
+// if err := p.App().inject(v, false); err != nil {
+// p.App().Flash().Err(err)
+// }
- return nil
-}
+// return nil
+// }
-func sanitizerCtx(path string) ContextFunc {
- return func(ctx context.Context) context.Context {
- ctx = context.WithValue(ctx, internal.KeyPath, path)
- return ctx
- }
-}
+// func sanitizerCtx(path string) ContextFunc {
+// return func(ctx context.Context) context.Context {
+// ctx = context.WithValue(ctx, internal.KeyPath, path)
+// return ctx
+// }
+// }
-// Helpers...
+// // Helpers...
-func grade(score int) string {
- switch {
- case score >= 90:
- return "A"
- case score >= 80:
- return "B"
- case score >= 70:
- return "C"
- case score >= 60:
- return "D"
- case score >= 50:
- return "E"
- default:
- return "F"
- }
-}
+// func grade(score int) string {
+// switch {
+// case score >= 90:
+// return "A"
+// case score >= 80:
+// return "B"
+// case score >= 70:
+// return "C"
+// case score >= 60:
+// return "D"
+// case score >= 50:
+// return "E"
+// default:
+// return "F"
+// }
+// }
diff --git a/internal/view/priorityclass.go b/internal/view/priorityclass.go
index fc89ef50..7f9ef909 100644
--- a/internal/view/priorityclass.go
+++ b/internal/view/priorityclass.go
@@ -25,10 +25,8 @@ func NewPriorityClass(gvr client.GVR) ResourceViewer {
return &s
}
-func (s *PriorityClass) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
- ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true),
- })
+func (s *PriorityClass) bindKeys(aa *ui.KeyActions) {
+ aa.Add(ui.KeyU, ui.NewKeyAction("UsedBy", s.refCmd, true))
}
func (s *PriorityClass) refCmd(evt *tcell.EventKey) *tcell.EventKey {
diff --git a/internal/view/pulse.go b/internal/view/pulse.go
index c323f64d..6d36e7a5 100644
--- a/internal/view/pulse.go
+++ b/internal/view/pulse.go
@@ -64,7 +64,7 @@ type Pulse struct {
gvr client.GVR
model *model.Pulse
cancelFn context.CancelFunc
- actions ui.KeyActions
+ actions *ui.KeyActions
charts []Graphable
}
@@ -73,7 +73,7 @@ func NewPulse(gvr client.GVR) ResourceViewer {
return &Pulse{
Grid: tview.NewGrid(),
model: model.NewPulse(gvr.String()),
- actions: make(ui.KeyActions),
+ actions: ui.NewKeyActions(),
}
}
@@ -207,15 +207,15 @@ func (p *Pulse) PulseFailed(err error) {
}
func (p *Pulse) bindKeys() {
- p.actions.Add(ui.KeyActions{
+ p.actions.Merge(ui.NewKeyActionsFromMap(ui.KeyMap{
tcell.KeyEnter: ui.NewKeyAction("Goto", p.enterCmd, true),
tcell.KeyTab: ui.NewKeyAction("Next", p.nextFocusCmd(1), true),
tcell.KeyBacktab: ui.NewKeyAction("Prev", p.nextFocusCmd(-1), true),
- })
+ }))
for i, v := range p.charts {
t := cases.Title(language.Und, cases.NoLower).String(client.NewGVR(v.ID()).R())
- p.actions[ui.NumKeys[i]] = ui.NewKeyAction(t, p.sparkFocusCmd(i), true)
+ p.actions.Add(ui.NumKeys[i], ui.NewKeyAction(t, p.sparkFocusCmd(i), true))
}
}
@@ -224,7 +224,7 @@ func (p *Pulse) keyboard(evt *tcell.EventKey) *tcell.EventKey {
if key == tcell.KeyRune {
key = tcell.Key(evt.Rune())
}
- if a, ok := p.actions[key]; ok {
+ if a, ok := p.actions.Get(key); ok {
return a.Action(evt)
}
@@ -289,7 +289,7 @@ func (p *Pulse) GetTable() *Table {
}
// Actions returns active menu bindings.
-func (p *Pulse) Actions() ui.KeyActions {
+func (p *Pulse) Actions() *ui.KeyActions {
return p.actions
}
diff --git a/internal/view/pvc.go b/internal/view/pvc.go
index 486fb463..27d15749 100644
--- a/internal/view/pvc.go
+++ b/internal/view/pvc.go
@@ -25,8 +25,8 @@ func NewPersistentVolumeClaim(gvr client.GVR) ResourceViewer {
return &v
}
-func (p *PersistentVolumeClaim) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (p *PersistentVolumeClaim) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyU: ui.NewKeyAction("UsedBy", p.refCmd, true),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd("STATUS", true), false),
ui.KeyShiftV: ui.NewKeyAction("Sort Volume", p.GetTable().SortColCmd("VOLUME", true), false),
diff --git a/internal/view/rbac.go b/internal/view/rbac.go
index 2ea21464..55831228 100644
--- a/internal/view/rbac.go
+++ b/internal/view/rbac.go
@@ -29,11 +29,9 @@ func NewRbac(gvr client.GVR) ResourceViewer {
return &r
}
-func (r *Rbac) bindKeys(aa ui.KeyActions) {
+func (r *Rbac) bindKeys(aa *ui.KeyActions) {
aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace)
- aa.Add(ui.KeyActions{
- ui.KeyShiftA: ui.NewKeyAction("Sort API-Group", r.GetTable().SortColCmd("API-GROUP", true), false),
- })
+ aa.Add(ui.KeyShiftA, ui.NewKeyAction("Sort API-Group", r.GetTable().SortColCmd("API-GROUP", true), false))
}
func showRules(app *App, _ ui.Tabular, gvr client.GVR, path string) {
diff --git a/internal/view/reference.go b/internal/view/reference.go
index 5519eae9..2f08cc7b 100644
--- a/internal/view/reference.go
+++ b/internal/view/reference.go
@@ -38,10 +38,10 @@ func (r *Reference) Init(ctx context.Context) error {
return nil
}
-func (r *Reference) bindKeys(aa ui.KeyActions) {
+func (r *Reference) bindKeys(aa *ui.KeyActions) {
aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlZ)
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
tcell.KeyEnter: ui.NewKeyAction("Goto", r.gotoCmd, true),
ui.KeyShiftV: ui.NewKeyAction("Sort GVR", r.GetTable().SortColCmd("GVR", true), false),
})
diff --git a/internal/view/registrar.go b/internal/view/registrar.go
index cde177b9..d199a449 100644
--- a/internal/view/registrar.go
+++ b/internal/view/registrar.go
@@ -5,7 +5,6 @@ package view
import (
"github.com/derailed/k9s/internal/client"
- "github.com/derailed/k9s/internal/ui"
)
func loadCustomViewers() MetaViewers {
@@ -15,7 +14,7 @@ func loadCustomViewers() MetaViewers {
appsViewers(m)
rbacViewers(m)
batchViewers(m)
- extViewers(m)
+ crdViewers(m)
helmViewers(m)
return m
@@ -91,9 +90,10 @@ func miscViewers(vv MetaViewers) {
vv[client.NewGVR("pulses")] = MetaViewer{
viewerFn: NewPulse,
}
- vv[client.NewGVR("popeye")] = MetaViewer{
- viewerFn: NewPopeye,
- }
+ // !!BOZO!! Popeye
+ // vv[client.NewGVR("popeye")] = MetaViewer{
+ // viewerFn: NewPopeye,
+ // }
vv[client.NewGVR("sanitizer")] = MetaViewer{
viewerFn: NewSanitizer,
}
@@ -153,13 +153,8 @@ func batchViewers(vv MetaViewers) {
}
}
-func extViewers(vv MetaViewers) {
+func crdViewers(vv MetaViewers) {
vv[client.NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions")] = MetaViewer{
- enterFn: showCRD,
+ viewerFn: NewCRD,
}
}
-
-func showCRD(app *App, _ ui.Tabular, _ client.GVR, path string) {
- _, crd := client.Namespaced(path)
- app.gotoResource(crd, "", false)
-}
diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go
index 1e83ced8..668e9bad 100644
--- a/internal/view/restart_extender.go
+++ b/internal/view/restart_extender.go
@@ -29,16 +29,16 @@ func NewRestartExtender(v ResourceViewer) ResourceViewer {
}
// BindKeys creates additional menu actions.
-func (r *RestartExtender) bindKeys(aa ui.KeyActions) {
+func (r *RestartExtender) bindKeys(aa *ui.KeyActions) {
if r.App().Config.K9s.IsReadOnly() {
return
}
- aa.Add(ui.KeyActions{
- ui.KeyR: ui.NewKeyActionWithOpts("Restart", r.restartCmd, ui.ActionOpts{
+ aa.Add(ui.KeyR, ui.NewKeyActionWithOpts("Restart", r.restartCmd,
+ ui.ActionOpts{
Visible: true,
Dangerous: true,
- }),
- })
+ },
+ ))
}
func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey {
diff --git a/internal/view/rs.go b/internal/view/rs.go
index 3650b8ff..86e2c595 100644
--- a/internal/view/rs.go
+++ b/internal/view/rs.go
@@ -29,8 +29,8 @@ func NewReplicaSet(gvr client.GVR) ResourceViewer {
return &r
}
-func (r *ReplicaSet) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (r *ReplicaSet) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyShiftD: ui.NewKeyAction("Sort Desired", r.GetTable().SortColCmd("DESIRED", true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Current", r.GetTable().SortColCmd("CURRENT", true), false),
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", r.GetTable().SortColCmd(readyCol, true), false),
diff --git a/internal/view/sa.go b/internal/view/sa.go
index c1433c2c..63145e7c 100644
--- a/internal/view/sa.go
+++ b/internal/view/sa.go
@@ -29,8 +29,8 @@ func NewServiceAccount(gvr client.GVR) ResourceViewer {
return &s
}
-func (s *ServiceAccount) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (s *ServiceAccount) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true),
tcell.KeyEnter: ui.NewKeyAction("Rules", s.policyCmd, true),
})
diff --git a/internal/view/sanitizer.go b/internal/view/sanitizer.go
index 51388ceb..9ec116d7 100644
--- a/internal/view/sanitizer.go
+++ b/internal/view/sanitizer.go
@@ -110,7 +110,7 @@ func (s *Sanitizer) ExtraHints() map[string]string {
func (s *Sanitizer) SetInstance(string) {}
func (s *Sanitizer) bindKeys() {
- s.Actions().Add(ui.KeyActions{
+ 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),
@@ -209,7 +209,7 @@ func (s *Sanitizer) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
func (s *Sanitizer) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if s.CmdBuff().IsActive() {
- if ui.IsLabelSelector(s.CmdBuff().GetText()) {
+ if internal.IsLabelSelector(s.CmdBuff().GetText()) {
s.Start()
}
s.CmdBuff().SetActive(false)
@@ -238,16 +238,16 @@ func (s *Sanitizer) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
func (s *Sanitizer) filter(root *xray.TreeNode) *xray.TreeNode {
q := s.CmdBuff().GetText()
- if s.CmdBuff().Empty() || ui.IsLabelSelector(q) {
+ if s.CmdBuff().Empty() || internal.IsLabelSelector(q) {
return root
}
s.UpdateTitle()
- if f, ok := dao.HasFuzzySelector(q); ok {
+ if f, ok := internal.IsFuzzySelector(q); ok {
return root.Filter(f, fuzzyFilter)
}
- if dao.IsInverseSelector(q) {
+ if internal.IsInverseSelector(q) {
return root.Filter(q, rxInverseFilter)
}
@@ -427,7 +427,7 @@ func (s *Sanitizer) styleTitle() string {
if buff == "" {
return title
}
- if ui.IsLabelSelector(buff) {
+ if internal.IsLabelSelector(buff) {
buff = ui.TrimLabelSelector(buff)
}
diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go
index c30e289c..e402bd5a 100644
--- a/internal/view/scale_extender.go
+++ b/internal/view/scale_extender.go
@@ -31,17 +31,16 @@ func NewScaleExtender(r ResourceViewer) ResourceViewer {
return &s
}
-func (s *ScaleExtender) bindKeys(aa ui.KeyActions) {
+func (s *ScaleExtender) bindKeys(aa *ui.KeyActions) {
if s.App().Config.K9s.IsReadOnly() {
return
}
- aa.Add(ui.KeyActions{
- ui.KeyS: ui.NewKeyActionWithOpts("Scale", s.scaleCmd,
- ui.ActionOpts{
- Visible: true,
- Dangerous: true,
- }),
- })
+ aa.Add(ui.KeyS, ui.NewKeyActionWithOpts("Scale", s.scaleCmd,
+ ui.ActionOpts{
+ Visible: true,
+ Dangerous: true,
+ },
+ ))
}
func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey {
diff --git a/internal/view/secret.go b/internal/view/secret.go
index e37cdb76..d59e77ee 100644
--- a/internal/view/secret.go
+++ b/internal/view/secret.go
@@ -28,8 +28,8 @@ func NewSecret(gvr client.GVR) ResourceViewer {
return &s
}
-func (s *Secret) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (s *Secret) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyX: ui.NewKeyAction("Decode", s.decodeCmd, true),
ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true),
})
diff --git a/internal/view/sts.go b/internal/view/sts.go
index e9e04fca..816dabb9 100644
--- a/internal/view/sts.go
+++ b/internal/view/sts.go
@@ -80,10 +80,8 @@ func (s *StatefulSet) logOptions(prev bool) (*dao.LogOptions, error) {
return &opts, nil
}
-func (s *StatefulSet) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
- ui.KeyShiftR: ui.NewKeyAction("Sort Ready", s.GetTable().SortColCmd(readyCol, true), false),
- })
+func (s *StatefulSet) bindKeys(aa *ui.KeyActions) {
+ aa.Add(ui.KeyShiftR, ui.NewKeyAction("Sort Ready", s.GetTable().SortColCmd(readyCol, true), false))
}
func (s *StatefulSet) showPods(app *App, _ ui.Tabular, _ client.GVR, path string) {
diff --git a/internal/view/svc.go b/internal/view/svc.go
index 4abf37d7..15171168 100644
--- a/internal/view/svc.go
+++ b/internal/view/svc.go
@@ -46,8 +46,8 @@ func NewService(gvr client.GVR) ResourceViewer {
// Protocol...
-func (s *Service) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
+func (s *Service) bindKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
ui.KeyB: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true),
ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd("TYPE", true), false),
})
diff --git a/internal/view/table.go b/internal/view/table.go
index 48bb9c9b..d0f60d37 100644
--- a/internal/view/table.go
+++ b/internal/view/table.go
@@ -93,7 +93,7 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
return evt
}
- if a, ok := t.Actions()[ui.AsKey(evt)]; ok && !t.app.Content.IsTopDialog() {
+ if a, ok := t.Actions().Get(ui.AsKey(evt)); ok && !t.app.Content.IsTopDialog() {
return a.Action(evt)
}
@@ -119,7 +119,7 @@ func (t *Table) EnvFn() EnvFunc {
func (t *Table) defaultEnv() Env {
path := t.GetSelectedItem()
row := t.GetSelectedRow(path)
- env := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header, row)
+ env := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header(), row)
env["FILTER"] = t.CmdBuff().GetText()
if env["FILTER"] == "" {
env["NAMESPACE"], env["FILTER"] = client.Namespaced(path)
@@ -186,7 +186,7 @@ func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
}
func (t *Table) bindKeys() {
- t.Actions().Add(ui.KeyActions{
+ t.Actions().Bulk(ui.KeyMap{
ui.KeyHelp: ui.NewKeyAction("Help", t.App().helpCmd, true),
ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false),
tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Mark Range", t.markSpanCmd, false),
diff --git a/internal/view/table_helper.go b/internal/view/table_helper.go
index 820f8754..34dfc81e 100644
--- a/internal/view/table_helper.go
+++ b/internal/view/table_helper.go
@@ -13,7 +13,7 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
- "github.com/derailed/k9s/internal/render"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/ui"
"github.com/rs/zerolog/log"
)
@@ -41,8 +41,8 @@ func computeFilename(dumpPath, ns, title, path string) (string, error) {
return strings.ToLower(filepath.Join(dir, fName)), nil
}
-func saveTable(dir, title, path string, data *render.TableData) (string, error) {
- ns := data.Namespace
+func saveTable(dir, title, path string, data *model1.TableData) (string, error) {
+ ns := data.GetNamespace()
if client.IsClusterWide(ns) {
ns = client.NamespaceAll
}
@@ -65,15 +65,12 @@ func saveTable(dir, title, path string, data *render.TableData) (string, error)
}()
w := csv.NewWriter(out)
- if err := w.Write(data.Header.Columns(true)); err != nil {
- return "", err
- }
+ _ = w.Write(data.ColumnNames(true))
- for _, re := range data.RowEvents {
- if err := w.Write(re.Row.Fields); err != nil {
- return "", err
- }
- }
+ data.RowsRange(func(_ int, re model1.RowEvent) bool {
+ _ = w.Write(re.Row.Fields)
+ return true
+ })
w.Flush()
if err := w.Error(); err != nil {
return "", err
diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go
index d87f9e90..38231eae 100644
--- a/internal/view/table_int_test.go
+++ b/internal/view/table_int_test.go
@@ -5,6 +5,8 @@ package view
import (
"context"
+ "errors"
+ "io/fs"
"os"
"testing"
"time"
@@ -15,6 +17,7 @@ import (
"github.com/derailed/k9s/internal/config/mock"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
+ "github.com/derailed/k9s/internal/model1"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tview"
@@ -41,28 +44,30 @@ func TestTableNew(t *testing.T) {
v := NewTable(client.NewGVR("test"))
assert.NoError(t, v.Init(makeContext()))
- data := render.NewTableData()
- data.Header = render.Header{
- render.HeaderColumn{Name: "NAMESPACE"},
- render.HeaderColumn{Name: "NAME", Align: tview.AlignRight},
- render.HeaderColumn{Name: "FRED"},
- render.HeaderColumn{Name: "AGE", Time: true, Decorator: render.AgeDecorator},
- }
- data.RowEvents = render.RowEvents{
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"ns1", "a", "10", "3m"},
- },
+ data := model1.NewTableDataWithRows(
+ client.NewGVR("test"),
+ model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "FRED"},
+ model1.HeaderColumn{Name: "AGE", Time: true, Decorator: render.AgeDecorator},
},
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"ns1", "b", "15", "1m"},
+ model1.NewRowEventsWithEvts(
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"ns1", "a", "10", "3m"},
+ },
},
- },
- }
- data.Namespace = ""
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"ns1", "b", "15", "1m"},
+ },
+ },
+ ),
+ )
+ cdata := v.Update(data, false)
+ v.UpdateUI(cdata, data)
- v.Update(data, false)
assert.Equal(t, 3, v.GetRowCount())
}
@@ -71,6 +76,7 @@ func TestTableViewFilter(t *testing.T) {
assert.NoError(t, v.Init(makeContext()))
v.SetModel(&mockTableModel{})
v.Refresh()
+
v.CmdBuff().SetActive(true)
v.CmdBuff().SetText("blee", "")
@@ -80,7 +86,7 @@ func TestTableViewFilter(t *testing.T) {
func TestTableViewSort(t *testing.T) {
v := NewTable(client.NewGVR("test"))
assert.NoError(t, v.Init(makeContext()))
- v.SetModel(&mockTableModel{})
+ v.SetModel(new(mockTableModel))
uu := map[string]struct {
sortCol string
@@ -130,9 +136,9 @@ func (t *mockTableModel) SetInstance(string) {}
func (t *mockTableModel) SetLabelFilter(string) {}
func (t *mockTableModel) GetLabelFilter() string { return "" }
func (t *mockTableModel) Empty() bool { return false }
-func (t *mockTableModel) Count() int { return 1 }
+func (t *mockTableModel) RowCount() int { return 1 }
func (t *mockTableModel) HasMetrics() bool { return true }
-func (t *mockTableModel) Peek() *render.TableData { return makeTableData() }
+func (t *mockTableModel) Peek() *model1.TableData { return makeTableData() }
func (t *mockTableModel) Refresh(context.Context) error { return nil }
func (t *mockTableModel) ClusterWide() bool { return false }
func (t *mockTableModel) GetNamespace() string { return "blee" }
@@ -160,39 +166,39 @@ func (t *mockTableModel) ToYAML(ctx context.Context, path string) (string, error
func (t *mockTableModel) InNamespace(string) bool { return true }
func (t *mockTableModel) SetRefreshRate(time.Duration) {}
-func makeTableData() *render.TableData {
- t := render.NewTableData()
- t.Header = render.Header{
- render.HeaderColumn{Name: "NAMESPACE"},
- render.HeaderColumn{Name: "NAME", Align: tview.AlignRight},
- render.HeaderColumn{Name: "FRED"},
- render.HeaderColumn{Name: "AGE", Time: true},
- }
- t.RowEvents = render.RowEvents{
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"ns1", "r3", "10", "3y125d"},
- },
+func makeTableData() *model1.TableData {
+ return model1.NewTableDataWithRows(
+ client.NewGVR("test"),
+ model1.Header{
+ model1.HeaderColumn{Name: "NAMESPACE"},
+ model1.HeaderColumn{Name: "NAME", Align: tview.AlignRight},
+ model1.HeaderColumn{Name: "FRED"},
+ model1.HeaderColumn{Name: "AGE", Time: true},
},
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"ns1", "r2", "15", "2y12d"},
+ model1.NewRowEventsWithEvts(
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"ns1", "r3", "10", "3y125d"},
+ },
},
- Deltas: render.DeltaRow{"", "", "20", ""},
- },
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"ns1", "r1", "20", "19h"},
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"ns1", "r2", "15", "2y12d"},
+ },
+ Deltas: model1.DeltaRow{"", "", "20", ""},
},
- },
- render.RowEvent{
- Row: render.Row{
- Fields: render.Fields{"ns1", "r0", "15", "10s"},
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"ns1", "r1", "20", "19h"},
+ },
},
- },
- }
-
- return t
+ model1.RowEvent{
+ Row: model1.Row{
+ Fields: model1.Fields{"ns1", "r0", "15", "10s"},
+ },
+ },
+ ),
+ )
}
func makeContext() context.Context {
@@ -201,31 +207,9 @@ func makeContext() context.Context {
return context.WithValue(ctx, internal.KeyStyles, a.Styles)
}
-// type ks struct{}
-
-// func (k ks) CurrentContextName() (string, error) {
-// return "test", nil
-// }
-
-// func (k ks) CurrentClusterName() (string, error) {
-// return "test", nil
-// }
-
-// func (k ks) CurrentNamespaceName() (string, error) {
-// return "test", nil
-// }
-
-// func (k ks) ContextNames() (map[string]struct{}, error) {
-// return map[string]struct{}{"test": {}}, nil
-// }
-
-// func (k ks) NamespaceNames(nn []v1.Namespace) []string {
-// return []string{"test"}
-// }
-
func ensureDumpDir(n string) error {
config.AppDumpsDir = n
- if _, err := os.Stat(n); os.IsNotExist(err) {
+ if _, err := os.Stat(n); errors.Is(err, fs.ErrNotExist) {
return os.Mkdir(n, 0700)
}
if err := os.RemoveAll(n); err != nil {
diff --git a/internal/view/types.go b/internal/view/types.go
index 070db98c..3f9d6862 100644
--- a/internal/view/types.go
+++ b/internal/view/types.go
@@ -40,7 +40,7 @@ type (
ContextFunc func(context.Context) context.Context
// BindKeysFunc adds new menu actions.
- BindKeysFunc func(ui.KeyActions)
+ BindKeysFunc func(*ui.KeyActions)
)
// ActionExtender enhances a given viewer by adding new menu actions.
@@ -60,7 +60,7 @@ type Viewer interface {
model.Component
// Actions returns active menu bindings.
- Actions() ui.KeyActions
+ Actions() *ui.KeyActions
// App returns an app handle.
App() *App
diff --git a/internal/view/user.go b/internal/view/user.go
index 12477d9c..94f55fd8 100644
--- a/internal/view/user.go
+++ b/internal/view/user.go
@@ -27,9 +27,9 @@ func NewUser(gvr client.GVR) ResourceViewer {
return &u
}
-func (u *User) bindKeys(aa ui.KeyActions) {
+func (u *User) bindKeys(aa *ui.KeyActions) {
aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace, tcell.KeyCtrlD, ui.KeyE)
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
tcell.KeyEnter: ui.NewKeyAction("Rules", u.policyCmd, true),
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", u.GetTable().SortColCmd("KIND", true), false),
})
diff --git a/internal/view/value_extender.go b/internal/view/value_extender.go
index 795da9f7..8d3a244c 100644
--- a/internal/view/value_extender.go
+++ b/internal/view/value_extender.go
@@ -30,10 +30,8 @@ func NewValueExtender(r ResourceViewer) ResourceViewer {
return &p
}
-func (v *ValueExtender) bindKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
- ui.KeyV: ui.NewKeyAction("Values", v.valuesCmd, true),
- })
+func (v *ValueExtender) bindKeys(aa *ui.KeyActions) {
+ aa.Add(ui.KeyV, ui.NewKeyAction("Values", v.valuesCmd, true))
}
func (v *ValueExtender) valuesCmd(evt *tcell.EventKey) *tcell.EventKey {
@@ -72,10 +70,7 @@ func showValues(ctx context.Context, app *App, path string, gvr client.GVR) {
}
v := NewLiveView(app, "Values", vm)
- v.actions.Add(ui.KeyActions{
- ui.KeyV: ui.NewKeyAction("Toggle All Values", toggleValuesCmd, true),
- })
-
+ v.actions.Add(ui.KeyV, ui.NewKeyAction("Toggle All Values", toggleValuesCmd, true))
if err := v.app.inject(v, false); err != nil {
v.app.Flash().Err(err)
}
diff --git a/internal/view/vul_extender.go b/internal/view/vul_extender.go
index 5c1774d6..ebc373b6 100644
--- a/internal/view/vul_extender.go
+++ b/internal/view/vul_extender.go
@@ -25,9 +25,9 @@ func NewVulnerabilityExtender(r ResourceViewer) ResourceViewer {
return &v
}
-func (v *VulnerabilityExtender) bindKeys(aa ui.KeyActions) {
+func (v *VulnerabilityExtender) bindKeys(aa *ui.KeyActions) {
if v.App().Config.K9s.ImageScans.Enable {
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyV: ui.NewKeyAction("Show Vulnerabilities", v.showVulCmd, true),
ui.KeyShiftV: ui.NewKeyAction("Sort Vulnerabilities", v.GetTable().SortColCmd("VS", true), false),
})
diff --git a/internal/view/workload.go b/internal/view/workload.go
index 0f66508e..4f0ba02f 100644
--- a/internal/view/workload.go
+++ b/internal/view/workload.go
@@ -36,18 +36,14 @@ func NewWorkload(gvr client.GVR) ResourceViewer {
return &w
}
-func (w *Workload) bindDangerousKeys(aa ui.KeyActions) {
- aa.Add(ui.KeyActions{
- ui.KeyE: ui.NewKeyActionWithOpts(
- "Edit",
- w.editCmd,
+func (w *Workload) bindDangerousKeys(aa *ui.KeyActions) {
+ aa.Bulk(ui.KeyMap{
+ ui.KeyE: ui.NewKeyActionWithOpts("Edit", w.editCmd,
ui.ActionOpts{
Visible: true,
Dangerous: true,
}),
- tcell.KeyCtrlD: ui.NewKeyActionWithOpts(
- "Delete",
- w.deleteCmd,
+ tcell.KeyCtrlD: ui.NewKeyActionWithOpts("Delete", w.deleteCmd,
ui.ActionOpts{
Visible: true,
Dangerous: true,
@@ -55,12 +51,12 @@ func (w *Workload) bindDangerousKeys(aa ui.KeyActions) {
})
}
-func (w *Workload) bindKeys(aa ui.KeyActions) {
+func (w *Workload) bindKeys(aa *ui.KeyActions) {
if !w.App().Config.K9s.IsReadOnly() {
w.bindDangerousKeys(aa)
}
- aa.Add(ui.KeyActions{
+ aa.Bulk(ui.KeyMap{
ui.KeyShiftK: ui.NewKeyAction("Sort Kind", w.GetTable().SortColCmd("KIND", true), false),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", w.GetTable().SortColCmd(statusCol, true), false),
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", w.GetTable().SortColCmd("READY", true), false),
@@ -114,7 +110,7 @@ func (w *Workload) defaultContext(gvr client.GVR, fqn string) context.Context {
if fqn != "" {
ctx = context.WithValue(ctx, internal.KeyPath, fqn)
}
- if ui.IsLabelSelector(w.GetTable().CmdBuff().GetText()) {
+ if internal.IsLabelSelector(w.GetTable().CmdBuff().GetText()) {
ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(w.GetTable().CmdBuff().GetText()))
}
ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(w.App().Config.ActiveNamespace()))
@@ -208,34 +204,3 @@ func (w *Workload) yamlCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
-
-// func (w *Workload) editCmd(evt *tcell.EventKey) *tcell.EventKey {
-// path := w.GetTable().GetSelectedItem()
-// if path == "" {
-// return evt
-// }
-// gvr, fqn, ok := parsePath(path)
-// if !ok {
-// w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path))
-// return evt
-// }
-
-// w.Stop()
-// defer w.Start()
-// {
-// ns, n := client.Namespaced(fqn)
-// args := make([]string, 0, 10)
-// args = append(args, "edit")
-// args = append(args, gvr.R())
-// args = append(args, "-n", ns)
-// args = append(args, "--context", w.App().Config.K9s.CurrentContext)
-// if cfg := w.App().Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
-// args = append(args, "--kubeconfig", *cfg)
-// }
-// if err := runK(w.App(), shellOpts{args: append(args, n)}); err != nil {
-// w.App().Flash().Errf("Edit exec failed: %s", err)
-// }
-// }
-
-// return evt
-// }
diff --git a/internal/view/xray.go b/internal/view/xray.go
index 8bab5c70..34e9e0cc 100644
--- a/internal/view/xray.go
+++ b/internal/view/xray.go
@@ -117,7 +117,7 @@ func (x *Xray) ExtraHints() map[string]string {
func (x *Xray) SetInstance(string) {}
func (x *Xray) bindKeys() {
- x.Actions().Add(ui.KeyActions{
+ x.Actions().Bulk(ui.KeyMap{
ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", x.activateCmd, false),
tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", x.resetCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Goto", x.gotoCmd, true),
@@ -130,7 +130,7 @@ func (x *Xray) keyEntered() {
}
func (x *Xray) refreshActions() {
- aa := make(ui.KeyActions)
+ aa := ui.NewKeyActions()
defer func() {
if err := pluginActions(x, aa); err != nil {
@@ -140,7 +140,7 @@ func (x *Xray) refreshActions() {
log.Warn().Err(err).Msg("HotKeys load failed")
}
- x.Actions().Add(aa)
+ x.Actions().Merge(aa)
x.app.Menu().HydrateMenu(x.Hints())
}()
@@ -162,14 +162,16 @@ func (x *Xray) refreshActions() {
}
if client.Can(x.meta.Verbs, "edit") {
- aa[ui.KeyE] = ui.NewKeyAction("Edit", x.editCmd, true)
+ aa.Add(ui.KeyE, ui.NewKeyAction("Edit", x.editCmd, true))
}
if client.Can(x.meta.Verbs, "delete") {
- aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", x.deleteCmd, true)
+ aa.Add(tcell.KeyCtrlD, ui.NewKeyAction("Delete", x.deleteCmd, true))
}
if !dao.IsK9sMeta(x.meta) {
- aa[ui.KeyY] = ui.NewKeyAction(yamlAction, x.viewCmd, true)
- aa[ui.KeyD] = ui.NewKeyAction("Describe", x.describeCmd, true)
+ aa.Bulk(ui.KeyMap{
+ ui.KeyY: ui.NewKeyAction(yamlAction, x.viewCmd, true),
+ ui.KeyD: ui.NewKeyAction("Describe", x.describeCmd, true),
+ })
}
switch gvr {
@@ -177,16 +179,20 @@ func (x *Xray) refreshActions() {
x.Actions().Delete(tcell.KeyEnter)
case "containers":
x.Actions().Delete(tcell.KeyEnter)
- aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true)
- aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true)
- aa[ui.KeyP] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true)
+ aa.Bulk(ui.KeyMap{
+ ui.KeyS: ui.NewKeyAction("Shell", x.shellCmd, true),
+ ui.KeyL: ui.NewKeyAction("Logs", x.logsCmd(false), true),
+ ui.KeyP: ui.NewKeyAction("Logs Previous", x.logsCmd(true), true),
+ })
case "v1/pods":
- aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true)
- aa[ui.KeyA] = ui.NewKeyAction("Attach", x.attachCmd, true)
- aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true)
- aa[ui.KeyP] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true)
+ aa.Bulk(ui.KeyMap{
+ ui.KeyS: ui.NewKeyAction("Shell", x.shellCmd, true),
+ ui.KeyA: ui.NewKeyAction("Attach", x.attachCmd, true),
+ ui.KeyL: ui.NewKeyAction("Logs", x.logsCmd(false), true),
+ ui.KeyP: ui.NewKeyAction("Logs Previous", x.logsCmd(true), true),
+ })
}
- x.Actions().Add(aa)
+ x.Actions().Merge(aa)
}
// GetSelectedPath returns the current selection as string.
@@ -454,7 +460,7 @@ func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if x.CmdBuff().IsActive() {
- if ui.IsLabelSelector(x.CmdBuff().GetText()) {
+ if internal.IsLabelSelector(x.CmdBuff().GetText()) {
x.Start()
}
x.CmdBuff().SetActive(false)
@@ -477,16 +483,16 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
func (x *Xray) filter(root *xray.TreeNode) *xray.TreeNode {
q := x.CmdBuff().GetText()
- if x.CmdBuff().Empty() || ui.IsLabelSelector(q) {
+ if x.CmdBuff().Empty() || internal.IsLabelSelector(q) {
return root
}
x.UpdateTitle()
- if f, ok := dao.HasFuzzySelector(q); ok {
+ if f, ok := internal.IsFuzzySelector(q); ok {
return root.Filter(f, fuzzyFilter)
}
- if dao.IsInverseSelector(q) {
+ if internal.IsInverseSelector(q) {
return root.Filter(q, rxInverseFilter)
}
@@ -661,7 +667,7 @@ func (x *Xray) styleTitle() string {
if buff == "" {
return title
}
- if ui.IsLabelSelector(buff) {
+ if internal.IsLabelSelector(buff) {
buff = ui.TrimLabelSelector(buff)
}
diff --git a/internal/xray/pod.go b/internal/xray/pod.go
index 69bb293f..dbcbf920 100644
--- a/internal/xray/pod.go
+++ b/internal/xray/pod.go
@@ -65,7 +65,6 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error {
func (p *Pod) validate(node *TreeNode, po v1.Pod) error {
var re render.Pod
-
phase := re.Phase(&po)
ss := po.Status.ContainerStatuses
cr, _, _ := re.Statuses(ss)
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index a8b389e7..090c2e65 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,6 +1,6 @@
name: k9s
base: core20
-version: 'v0.31.9'
+version: 'v0.32.0'
summary: K9s is a CLI to view and manage your Kubernetes clusters.
description: |
K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session.