K9s/release v0.32.0 (#2577)
* [Perf] improved load perf and ui updates * [Bug] Fix #2557 * [Maint] refactor + spring cleaning up * [Bug] Fix #2569 * [Maint] Refactor + cleanup * [Bug] Fix #2560 * [Maint] Refactor + cleanup * Release v0.32.0mine
parent
82ba6f9f37
commit
0d16531016
2
Makefile
2
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}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
|
||||
|
||||
# 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
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
package dao
|
||||
|
||||
// !!BOZO!!
|
||||
// !!BOZO!! Popeye
|
||||
// import (
|
||||
// "bytes"
|
||||
// "context"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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++
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"},
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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...
|
||||
|
|
@ -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
|
||||
|
|
@ -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"}}},
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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", "<none>"}, r.Fields[:3])
|
||||
assert.Equal(t, model1.Fields{"default", "dictionary1", "<none>"}, r.Fields[:3])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue