From 0d1653101696aaed67984d0188407503426c20e0 Mon Sep 17 00:00:00 2001 From: Fernand Galiana Date: Sat, 2 Mar 2024 10:18:47 -0700 Subject: [PATCH] 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.0 --- Makefile | 2 +- change_logs/release_v0.32.0.md | 73 +++ cmd/root.go | 4 +- internal/client/client.go | 60 +-- internal/client/config.go | 2 +- internal/config/alias.go | 4 +- internal/config/config.go | 20 +- internal/config/config_test.go | 32 +- internal/config/data/config.go | 9 + internal/config/data/context.go | 7 + internal/config/data/context_int_test.go | 81 +++ internal/config/data/dir.go | 28 +- internal/config/data/helpers.go | 4 +- internal/config/data/ns.go | 15 + internal/config/files.go | 8 +- internal/config/hotkey.go | 6 +- internal/config/k9s.go | 10 +- internal/config/k9s_int_test.go | 2 +- internal/config/k9s_test.go | 4 +- internal/config/mock/test_helpers.go | 4 +- internal/config/plugin.go | 3 +- internal/config/views.go | 25 +- internal/dao/cluster.go | 4 +- internal/dao/cm.go | 13 + internal/dao/crd.go | 34 -- internal/dao/dp.go | 5 - internal/dao/ds.go | 7 +- internal/dao/helpers.go | 62 ++- internal/dao/log_items.go | 5 +- internal/dao/ns.go | 20 +- internal/dao/pod.go | 49 +- internal/dao/popeye.go | 2 +- internal/dao/registry.go | 48 +- internal/dao/secret.go | 8 +- internal/dao/sts.go | 5 - internal/dao/table.go | 46 +- internal/helpers.go | 46 ++ internal/helpers_test.go | 33 ++ internal/model/cluster.go | 2 +- internal/model/cluster_info.go | 2 - internal/model/describe.go | 3 +- internal/model/pulse_health.go | 11 +- internal/model/registry.go | 24 +- internal/model/rev_values.go | 3 +- internal/model/stack.go | 1 + internal/model/table.go | 122 +---- internal/model/table_int_test.go | 60 +-- internal/model/table_test.go | 10 +- internal/model/text.go | 4 +- internal/model/types.go | 19 +- internal/model/values.go | 3 +- internal/model/yaml.go | 2 +- internal/{render => model1}/color.go | 13 +- internal/model1/color_test.go | 66 +++ internal/{render => model1}/delta.go | 6 +- internal/model1/delta_test.go | 266 ++++++++++ internal/model1/fields.go | 41 ++ internal/{render => model1}/header.go | 50 +- internal/{render => model1}/header_test.go | 189 ++++--- internal/model1/helpers.go | 166 +++++++ internal/model1/helpers_test.go | 89 ++++ internal/model1/row.go | 92 ++++ internal/{render => model1}/row_event.go | 181 ++++--- internal/model1/row_event_test.go | 543 +++++++++++++++++++++ internal/{render => model1}/row_test.go | 170 +++---- internal/model1/rows.go | 61 +++ internal/model1/table_data.go | 496 +++++++++++++++++++ internal/model1/table_data_test.go | 404 +++++++++++++++ internal/model1/test_helper_test.go | 17 + internal/model1/types.go | 61 +++ internal/render/alias.go | 13 +- internal/render/alias_test.go | 35 +- internal/render/base.go | 10 +- internal/render/benchmark.go | 44 +- internal/render/benchmark_int_test.go | 13 +- internal/render/cm.go | 56 +++ internal/render/color_test.go | 66 --- internal/render/container.go | 75 ++- internal/render/container_test.go | 7 +- internal/render/context.go | 25 +- internal/render/context_test.go | 9 +- internal/render/cr.go | 15 +- internal/render/cr_test.go | 5 +- internal/render/crb.go | 21 +- internal/render/crb_test.go | 5 +- internal/render/crd.go | 37 +- internal/render/crd_test.go | 6 +- internal/render/cronjob.go | 38 +- internal/render/cronjob_test.go | 5 +- internal/render/delta_test.go | 266 ---------- internal/render/dir.go | 13 +- internal/render/dp.go | 47 +- internal/render/dp_test.go | 7 +- internal/render/ds.go | 33 +- internal/render/ds_test.go | 5 +- internal/render/ep.go | 19 +- internal/render/ep_test.go | 5 +- internal/render/ev.go | 27 +- internal/render/ev_test.go | 6 +- internal/render/generic.go | 23 +- internal/render/generic_test.go | 63 +-- internal/render/helm/chart.go | 29 +- internal/render/helm/history.go | 25 +- internal/render/helpers.go | 133 ++--- internal/render/helpers_test.go | 149 ++---- internal/render/img_scan.go | 35 +- internal/render/job.go | 31 +- internal/render/job_test.go | 5 +- internal/render/node.go | 47 +- internal/render/node_test.go | 7 +- internal/render/np.go | 31 +- internal/render/np_test.go | 5 +- internal/render/ns.go | 39 +- internal/render/ns_test.go | 45 +- internal/render/pdb.go | 31 +- internal/render/pdb_test.go | 5 +- internal/render/pod.go | 103 ++-- internal/render/pod_test.go | 143 +++--- internal/render/policy.go | 43 +- internal/render/policy_test.go | 5 +- internal/render/popeye.go | 319 ++++++------ internal/render/port_forward_test.go | 5 +- internal/render/portforward.go | 31 +- internal/render/pv.go | 57 ++- internal/render/pv_test.go | 9 +- internal/render/pvc.go | 29 +- internal/render/pvc_test.go | 5 +- internal/render/rbac.go | 19 +- internal/render/reference.go | 17 +- internal/render/reference_test.go | 5 +- internal/render/ro.go | 19 +- internal/render/ro_test.go | 5 +- internal/render/rob.go | 25 +- internal/render/rob_test.go | 5 +- internal/render/row.go | 231 --------- internal/render/row_event_test.go | 539 -------------------- internal/render/rs.go | 33 +- internal/render/rs_test.go | 5 +- internal/render/sa.go | 21 +- internal/render/sa_test.go | 5 +- internal/render/sc.go | 25 +- internal/render/sc_test.go | 5 +- internal/render/screen_dump.go | 21 +- internal/render/screen_dump_test.go | 5 +- internal/render/secret.go | 58 +++ internal/render/sts.go | 33 +- internal/render/sts_test.go | 5 +- internal/render/subject.go | 21 +- internal/render/svc.go | 29 +- internal/render/svc_test.go | 7 +- internal/render/table_data.go | 150 ------ internal/render/table_data_test.go | 396 --------------- internal/render/testdata/p1.json | 146 ++++++ internal/render/workload.go | 37 +- internal/ui/action.go | 139 +++++- internal/ui/action_test.go | 4 +- internal/ui/app.go | 30 +- internal/ui/app_test.go | 4 +- internal/ui/config.go | 27 +- internal/ui/config_test.go | 6 +- internal/ui/menu_test.go | 6 +- internal/ui/padding.go | 18 +- internal/ui/padding_test.go | 105 ++-- internal/ui/select_table.go | 2 +- internal/ui/table.go | 248 ++++++---- internal/ui/table_helper.go | 90 ---- internal/ui/table_helper_test.go | 22 - internal/ui/table_test.go | 62 +-- internal/ui/tree.go | 12 +- internal/ui/types.go | 19 +- internal/view/actions.go | 38 +- internal/view/alias.go | 4 +- internal/view/alias_test.go | 38 +- internal/view/app.go | 9 +- internal/view/app_test.go | 2 +- internal/view/browser.go | 103 ++-- internal/view/cluster_info.go | 7 +- internal/view/cm.go | 6 +- internal/view/command.go | 2 +- internal/view/container.go | 27 +- internal/view/context.go | 6 +- internal/view/cow.go | 12 +- internal/view/crd.go | 38 ++ internal/view/cronjob.go | 4 +- internal/view/details.go | 10 +- internal/view/dir.go | 8 +- internal/view/dp.go | 4 +- internal/view/ds.go | 4 +- internal/view/event.go | 4 +- internal/view/group.go | 4 +- internal/view/helm_chart.go | 4 +- internal/view/helm_history.go | 22 +- internal/view/help.go | 2 +- internal/view/helpers.go | 27 +- internal/view/helpers_test.go | 5 +- internal/view/image_extender.go | 6 +- internal/view/img_scan.go | 4 +- internal/view/live_view.go | 22 +- internal/view/log.go | 6 +- internal/view/log_test.go | 6 +- internal/view/logger.go | 10 +- internal/view/logs_extender.go | 4 +- internal/view/node.go | 12 +- internal/view/ns.go | 46 +- internal/view/pf.go | 4 +- internal/view/pf_extender.go | 4 +- internal/view/picker.go | 4 +- internal/view/pod.go | 38 +- internal/view/policy.go | 4 +- internal/view/popeye.go | 196 ++++---- internal/view/priorityclass.go | 6 +- internal/view/pulse.go | 14 +- internal/view/pvc.go | 4 +- internal/view/rbac.go | 6 +- internal/view/reference.go | 4 +- internal/view/registrar.go | 19 +- internal/view/restart_extender.go | 10 +- internal/view/rs.go | 4 +- internal/view/sa.go | 4 +- internal/view/sanitizer.go | 12 +- internal/view/scale_extender.go | 15 +- internal/view/secret.go | 4 +- internal/view/sts.go | 6 +- internal/view/svc.go | 4 +- internal/view/table.go | 6 +- internal/view/table_helper.go | 19 +- internal/view/table_int_test.go | 132 +++-- internal/view/types.go | 4 +- internal/view/user.go | 4 +- internal/view/value_extender.go | 11 +- internal/view/vul_extender.go | 4 +- internal/view/workload.go | 49 +- internal/view/xray.go | 46 +- internal/xray/pod.go | 1 - snap/snapcraft.yaml | 2 +- 235 files changed, 5583 insertions(+), 4556 deletions(-) create mode 100644 change_logs/release_v0.32.0.md create mode 100644 internal/config/data/context_int_test.go create mode 100644 internal/dao/cm.go create mode 100644 internal/helpers.go create mode 100644 internal/helpers_test.go rename internal/{render => model1}/color.go (74%) create mode 100644 internal/model1/color_test.go rename internal/{render => model1}/delta.go (97%) create mode 100644 internal/model1/delta_test.go create mode 100644 internal/model1/fields.go rename internal/{render => model1}/header.go (82%) rename internal/{render => model1}/header_test.go (53%) create mode 100644 internal/model1/helpers.go create mode 100644 internal/model1/helpers_test.go create mode 100644 internal/model1/row.go rename internal/{render => model1}/row_event.go (54%) create mode 100644 internal/model1/row_event_test.go rename internal/{render => model1}/row_test.go (78%) create mode 100644 internal/model1/rows.go create mode 100644 internal/model1/table_data.go create mode 100644 internal/model1/table_data_test.go create mode 100644 internal/model1/test_helper_test.go create mode 100644 internal/model1/types.go create mode 100644 internal/render/cm.go delete mode 100644 internal/render/color_test.go delete mode 100644 internal/render/delta_test.go delete mode 100644 internal/render/row.go delete mode 100644 internal/render/row_event_test.go create mode 100644 internal/render/secret.go delete mode 100644 internal/render/table_data.go delete mode 100644 internal/render/table_data_test.go create mode 100644 internal/render/testdata/p1.json create mode 100644 internal/view/crd.go diff --git a/Makefile b/Makefile index 2382ab3354..36c29177ce 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H: else DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") endif -VERSION ?= v0.31.9 +VERSION ?= v0.32.0 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/change_logs/release_v0.32.0.md b/change_logs/release_v0.32.0.md new file mode 100644 index 0000000000..e35c3b61f5 --- /dev/null +++ b/change_logs/release_v0.32.0.md @@ -0,0 +1,73 @@ + + +# Release v0.32.0 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for K9s! +I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev +and see if we're happier with some of the fixes! +If you've filed an issue please help me verify and close. + +Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! +Also big thanks to all that have allocated their own time to help others on both slack and on this repo!! + +As you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, +please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +A lot of refactors, perf improvements (crossing fingers+toes!) and general spring cleaning items in this release. +Thus I expect a bit of `disturbance in the farce` given the major code churns, so please beware! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## A Word From Our Sponsors... + +To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!! + +* [Justin Reid](https://github.com/jmreid) +* [Danni](https://github.com/danninov) +* [Robert Krahn](https://github.com/rksm) +* [Hao Ke](https://github.com/kehao95) +* [PH](https://github.com/raphael-com-ph) + +> Sponsorship cancellations since the last release: **9!!** 🥹 + +--- + +## Resolved Issues + +* [#2569](https://github.com/derailed/k9s/issues/2569) k9s panics on start if the main config file (config.yml) is owned by root +* [#2568](https://github.com/derailed/k9s/issues/2568) kube context in running k9s is no longer sticky, during kubectx context switch +* [#2560](https://github.com/derailed/k9s/issues/2560) Namespace/Settings keeps resetting +* [#2557](https://github.com/derailed/k9s/issues/2557) [Feature]: Sort CRDs by their group +* [#1462](https://github.com/derailed/k9s/issues/1462) k9s running very slowly when opening namespace with 13k pods (maybe??) + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2564](https://github.com/derailed/k9s/pull/2564) Add everforest skins +* [#2558](https://github.com/derailed/k9s/pull/2558) feat: sort by role in node list view +* [#2554](https://github.com/derailed/k9s/pull/2554) Added context to the debug command for debug-container plugin +* [#2554](https://github.com/derailed/k9s/pull/2554) Correctly respect the KUBECACHEDIR env var +* [#2546](https://github.com/derailed/k9s/pull/2546) Use configured log fgColor to print log markers + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index 2ffc3ea1d1..5181c8bd7a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -134,7 +134,7 @@ func loadConfiguration() (*config.Config, error) { errs = errors.Join(errs, err) } - if err := k9sCfg.Load(config.AppConfigFile); err != nil { + if err := k9sCfg.Load(config.AppConfigFile, false); err != nil { errs = errors.Join(errs, err) } k9sCfg.K9s.Override(k9sFlags) @@ -151,7 +151,7 @@ func loadConfiguration() (*config.Config, error) { } log.Info().Msg("✅ Kubernetes connectivity") - if err := k9sCfg.Save(); err != nil { + if err := k9sCfg.Save(false); err != nil { log.Error().Err(err).Msg("Config save") errs = errors.Join(errs, err) } diff --git a/internal/client/client.go b/internal/client/client.go index f76467a297..e12f900530 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -212,64 +212,26 @@ func (a *APIClient) ServerVersion() (*version.Info, error) { return info, nil } -func (a *APIClient) IsValidNamespace(ns string) bool { - if IsClusterWide(ns) || ns == NotNamespaced { - return true - } - - 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) +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) } - 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() - 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{}) + nn, err := a.ValidNamespaceNames() if err != nil { return false, err } - cnss[n] = struct{}{} - a.cache.Add(cacheNSKey, cnss, cacheExpiry) + _, ok := nn[n] - return true, nil + return ok, nil } // ValidNamespaceNames returns all available namespaces. @@ -283,6 +245,12 @@ func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) { return nss, nil } } + + ok, err := a.CanI(ClusterScope, "v1/namespaces", "", []string{ListVerb}) + if !ok || err != nil { + return nil, fmt.Errorf("user not authorized to list all namespaces") + } + dial, err := a.Dial() if err != nil { return nil, err diff --git a/internal/client/config.go b/internal/client/config.go index 0dafc59e64..65c3d6d777 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -20,7 +20,7 @@ const ( defaultCallTimeoutDuration time.Duration = 15 * time.Second // UsePersistentConfig caches client config to avoid reloads. - UsePersistentConfig = false + UsePersistentConfig = true ) // Config tracks a kubernetes configuration. diff --git a/internal/config/alias.go b/internal/config/alias.go index 35cd8e788c..be53f4027f 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -4,7 +4,9 @@ package config import ( + "errors" "fmt" + "io/fs" "os" "sync" @@ -136,7 +138,7 @@ func (a *Aliases) LoadFile(path string) error { if path == "" { return nil } - if _, err := os.Stat(path); os.IsNotExist(err) { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { return nil } diff --git a/internal/config/config.go b/internal/config/config.go index 0390bf5e6a..5f7c2471d1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ package config import ( "errors" "fmt" + "io/fs" "os" "github.com/derailed/k9s/internal/client" @@ -52,14 +53,13 @@ func (c *Config) ContextAliasesPath() string { } // ContextPluginsPath returns a context specific plugins file spec. -func (c *Config) ContextPluginsPath() string { +func (c *Config) ContextPluginsPath() (string, error) { ct, err := c.K9s.ActiveContext() if err != nil { - log.Error().Err(err).Msgf("active context load failed") - return "" + return "", err } - return AppContextPluginsFile(ct.GetClusterName(), c.K9s.activeContextName) + return AppContextPluginsFile(ct.GetClusterName(), c.K9s.activeContextName), nil } // Refine the configuration based on cli args. @@ -209,9 +209,9 @@ func (c *Config) Merge(c1 *Config) { } // Load loads K9s configuration from file. -func (c *Config) Load(path string) error { - if _, err := os.Stat(path); os.IsNotExist(err) { - if err := c.Save(); err != nil { +func (c *Config) Load(path string, force bool) error { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + if err := c.Save(force); err != nil { return err } } @@ -234,12 +234,12 @@ func (c *Config) Load(path string) error { } // Save configuration to disk. -func (c *Config) Save() error { +func (c *Config) Save(force bool) error { c.Validate() - if err := c.K9s.Save(); err != nil { + if err := c.K9s.Save(force); err != nil { return err } - if _, err := os.Stat(AppConfigFile); os.IsNotExist(err) { + if _, err := os.Stat(AppConfigFile); errors.Is(err, fs.ErrNotExist) { return c.SaveFile(AppConfigFile) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 10c66cb670..3910eb5c74 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -4,6 +4,7 @@ package config_test import ( + "errors" "fmt" "os" "path/filepath" @@ -58,7 +59,7 @@ func TestConfigSave(t *testing.T) { c.K9s.Override(u.k9sFlags) assert.NoError(t, c.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags))) } - assert.NoError(t, c.Save()) + assert.NoError(t, c.Save(true)) bb, err := os.ReadFile(config.AppConfigFile) assert.NoError(t, err) ee, err := os.ReadFile("testdata/configs/default.yaml") @@ -265,16 +266,19 @@ func TestContextAliasesPath(t *testing.T) { func TestContextPluginsPath(t *testing.T) { uu := map[string]struct { - ct string - e string + ct, e string + err error }{ - "empty": {}, + "empty": { + err: errors.New(`no context found for: ""`), + }, "happy": { ct: "ct-1-1", e: "/tmp/test/cl-1/ct-1-1/plugins.yaml", }, "not-exists": { - ct: "fred", + ct: "fred", + err: errors.New(`no context found for: "fred"`), }, } @@ -283,7 +287,11 @@ func TestContextPluginsPath(t *testing.T) { t.Run(k, func(t *testing.T) { c := mock.NewMockConfig() _, _ = c.K9s.ActivateContext(u.ct) - assert.Equal(t, u.e, c.ContextPluginsPath()) + s, err := c.ContextPluginsPath() + if err != nil { + assert.Equal(t, u.err, err) + } + assert.Equal(t, u.e, s) }) } } @@ -309,7 +317,7 @@ Invalid type. Expected: boolean, given: string`, u := uu[k] t.Run(k, func(t *testing.T) { cfg := config.NewConfig(nil) - if err := cfg.Load(u.f); err != nil { + if err := cfg.Load(u.f, true); err != nil { assert.Equal(t, u.err, err.Error()) } }) @@ -520,14 +528,14 @@ func TestConfigValidate(t *testing.T) { cfg := mock.NewMockConfig() cfg.SetConnection(mock.NewMockConnection()) - assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml")) + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) cfg.Validate() } func TestConfigLoad(t *testing.T) { cfg := mock.NewMockConfig() - assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml")) + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) assert.Equal(t, 2, cfg.K9s.RefreshRate) assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount) assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize) @@ -536,13 +544,13 @@ func TestConfigLoad(t *testing.T) { func TestConfigLoadCrap(t *testing.T) { cfg := mock.NewMockConfig() - assert.NotNil(t, cfg.Load("testdata/configs/k9s_not_there.yaml")) + assert.NotNil(t, cfg.Load("testdata/configs/k9s_not_there.yaml", true)) } func TestConfigSaveFile(t *testing.T) { cfg := mock.NewMockConfig() - assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml")) + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) cfg.K9s.RefreshRate = 100 cfg.K9s.ReadOnly = true @@ -561,7 +569,7 @@ func TestConfigSaveFile(t *testing.T) { func TestConfigReset(t *testing.T) { cfg := mock.NewMockConfig() - assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml")) + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) cfg.Reset() cfg.Validate() diff --git a/internal/config/data/config.go b/internal/config/data/config.go index 130bf15b36..a03020f809 100644 --- a/internal/config/data/config.go +++ b/internal/config/data/config.go @@ -26,6 +26,15 @@ func NewConfig(ct *api.Context) *Config { } } +func (c *Config) Merge(c1 *Config) { + if c1 == nil { + return + } + if c.Context != nil && c1.Context != nil { + c.Context.merge(c1.Context) + } +} + // Validate ensures config is in norms. func (c *Config) Validate(conn client.Connection, ks KubeSettings) { c.mx.Lock() diff --git a/internal/config/data/context.go b/internal/config/data/context.go index 32a777918d..591f072f7b 100644 --- a/internal/config/data/context.go +++ b/internal/config/data/context.go @@ -56,6 +56,13 @@ func NewContextFromKubeConfig(ks KubeSettings) (*Context, error) { return NewContextFromConfig(ct), nil } +func (c *Context) merge(old *Context) { + if old == nil { + return + } + c.Namespace.merge(old.Namespace) + +} func (c *Context) GetClusterName() string { c.mx.RLock() defer c.mx.RUnlock() diff --git a/internal/config/data/context_int_test.go b/internal/config/data/context_int_test.go new file mode 100644 index 0000000000..1a37ff99a5 --- /dev/null +++ b/internal/config/data/context_int_test.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_contextMerge(t *testing.T) { + uu := map[string]struct { + c1, c2, e *Context + }{ + "empty": {}, + "nil": { + c1: &Context{ + Namespace: &Namespace{ + Active: "ns1", + Favorites: []string{"ns1", "ns2", "ns3"}, + }, + }, + e: &Context{ + Namespace: &Namespace{ + Active: "ns1", + Favorites: []string{"ns1", "ns2", "ns3"}, + }, + }, + }, + "deltas": { + c1: &Context{ + Namespace: &Namespace{ + Active: "ns1", + Favorites: []string{"ns1", "ns2", "ns3"}, + }, + }, + c2: &Context{ + Namespace: &Namespace{ + Active: "ns10", + Favorites: []string{"ns10", "ns11", "ns12"}, + }, + }, + e: &Context{ + Namespace: &Namespace{ + Active: "ns1", + Favorites: []string{"ns1", "ns2", "ns3", "ns10", "ns11", "ns12"}, + }, + }, + }, + "deltas-locked": { + c1: &Context{ + Namespace: &Namespace{ + Active: "ns1", + LockFavorites: true, + Favorites: []string{"ns1", "ns2", "ns3"}, + }, + }, + c2: &Context{ + Namespace: &Namespace{ + Active: "ns10", + Favorites: []string{"ns10", "ns11", "ns12"}, + }, + }, + e: &Context{ + Namespace: &Namespace{ + Active: "ns1", + LockFavorites: true, + Favorites: []string{"ns1", "ns2", "ns3"}, + }, + }, + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + u.c1.merge(u.c2) + assert.Equal(t, u.e, u.c1) + }) + } +} diff --git a/internal/config/data/dir.go b/internal/config/data/dir.go index b945706f24..3b045578f9 100644 --- a/internal/config/data/dir.go +++ b/internal/config/data/dir.go @@ -6,6 +6,7 @@ package data import ( "errors" "fmt" + "io/fs" "os" "path/filepath" "sync" @@ -34,20 +35,21 @@ func (d *Dir) Load(n string, ct *api.Context) (*Config, error) { if ct == nil { return nil, errors.New("api.Context must not be nil") } - var ( - path = filepath.Join(d.root, SanitizeContextSubpath(ct.Cluster, n), MainConfigFile) - cfg *Config - err error - ) - if f, e := os.Stat(path); os.IsNotExist(e) || f.Size() == 0 { + var path = filepath.Join(d.root, SanitizeContextSubpath(ct.Cluster, n), MainConfigFile) + + f, err := os.Stat(path) + if errors.Is(err, fs.ErrPermission) { + return nil, err + } + if errors.Is(err, fs.ErrNotExist) || (f != nil && f.Size() == 0) { log.Debug().Msgf("Context config not found! Generating... %q", path) - cfg, err = d.genConfig(path, ct) - } else { - log.Debug().Msgf("Found existing context config: %q", path) - cfg, err = d.loadConfig(path) + return d.genConfig(path, ct) + } + if err != nil { + return nil, err } - return cfg, err + return d.loadConfig(path) } func (d *Dir) genConfig(path string, ct *api.Context) (*Config, error) { @@ -60,6 +62,10 @@ func (d *Dir) genConfig(path string, ct *api.Context) (*Config, error) { } func (d *Dir) Save(path string, c *Config) error { + if cfg, err := d.loadConfig(path); err == nil { + c.Merge(cfg) + } + d.mx.Lock() defer d.mx.Unlock() diff --git a/internal/config/data/helpers.go b/internal/config/data/helpers.go index 59f043870c..ae6cc6e9c6 100644 --- a/internal/config/data/helpers.go +++ b/internal/config/data/helpers.go @@ -4,6 +4,8 @@ package data import ( + "errors" + "io/fs" "os" "path/filepath" "regexp" @@ -38,7 +40,7 @@ func EnsureDirPath(path string, mod os.FileMode) error { // EnsureFullPath ensures a directory exist from the given path. func EnsureFullPath(path string, mod os.FileMode) error { - if _, err := os.Stat(path); os.IsNotExist(err) { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { if err = os.MkdirAll(path, mod); err != nil { return err } diff --git a/internal/config/data/ns.go b/internal/config/data/ns.go index 1926289460..57f9d3e678 100644 --- a/internal/config/data/ns.go +++ b/internal/config/data/ns.go @@ -38,6 +38,21 @@ func NewActiveNamespace(n string) *Namespace { } } +func (n *Namespace) merge(old *Namespace) { + n.mx.Lock() + defer n.mx.Unlock() + + if n.LockFavorites { + return + } + for _, fav := range old.Favorites { + if InList(n.Favorites, fav) { + continue + } + n.Favorites = append(n.Favorites, fav) + } +} + // Validate validates a namespace is setup correctly. func (n *Namespace) Validate(c client.Connection) { n.mx.RLock() diff --git a/internal/config/files.go b/internal/config/files.go index b4b1e02bdb..2b246d5172 100644 --- a/internal/config/files.go +++ b/internal/config/files.go @@ -5,6 +5,8 @@ package config import ( _ "embed" + "errors" + "io/fs" "os" "path/filepath" @@ -238,7 +240,7 @@ func EnsureBenchmarksCfgFile(cluster, context string) (string, error) { if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil { return "", err } - if _, err := os.Stat(f); os.IsNotExist(err) { + if _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) { return f, os.WriteFile(f, benchmarkTpl, data.DefaultFileMod) } @@ -251,7 +253,7 @@ func EnsureAliasesCfgFile() (string, error) { if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil { return "", err } - if _, err := os.Stat(f); os.IsNotExist(err) { + if _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) { return f, os.WriteFile(f, aliasesTpl, data.DefaultFileMod) } @@ -264,7 +266,7 @@ func EnsureHotkeysCfgFile() (string, error) { if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil { return "", err } - if _, err := os.Stat(f); os.IsNotExist(err) { + if _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) { return f, os.WriteFile(f, hotkeysTpl, data.DefaultFileMod) } diff --git a/internal/config/hotkey.go b/internal/config/hotkey.go index 91801eb8b7..65a651e40a 100644 --- a/internal/config/hotkey.go +++ b/internal/config/hotkey.go @@ -4,7 +4,9 @@ package config import ( + "errors" "fmt" + "io/fs" "os" "github.com/derailed/k9s/internal/config/data" @@ -38,7 +40,7 @@ func (h HotKeys) Load(path string) error { if err := h.LoadHotKeys(AppHotKeysFile); err != nil { return err } - if _, err := os.Stat(path); os.IsNotExist(err) { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { return nil } @@ -47,7 +49,7 @@ func (h HotKeys) Load(path string) error { // LoadHotKeys loads plugins from a given file. func (h HotKeys) LoadHotKeys(path string) error { - if _, err := os.Stat(path); os.IsNotExist(err) { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { return nil } bb, err := os.ReadFile(path) diff --git a/internal/config/k9s.go b/internal/config/k9s.go index a1b45558b9..953fb7ca3b 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -4,7 +4,10 @@ package config import ( + "errors" "fmt" + "io/fs" + "os" "path/filepath" "sync" @@ -67,7 +70,7 @@ func (k *K9s) resetConnection(conn client.Connection) { } // Save saves the k9s config to disk. -func (k *K9s) Save() error { +func (k *K9s) Save(force bool) error { if k.getActiveConfig() == nil { log.Warn().Msgf("Save failed. no active config detected") return nil @@ -77,8 +80,11 @@ func (k *K9s) Save() error { data.SanitizeContextSubpath(k.activeConfig.Context.GetClusterName(), k.getActiveContextName()), data.MainConfigFile, ) + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) || force { + return k.dir.Save(path, k.getActiveConfig()) + } - return k.dir.Save(path, k.getActiveConfig()) + return nil } // Merge merges k9s configs. diff --git a/internal/config/k9s_int_test.go b/internal/config/k9s_int_test.go index 0bee7fcce3..f950502e57 100644 --- a/internal/config/k9s_int_test.go +++ b/internal/config/k9s_int_test.go @@ -119,7 +119,7 @@ func Test_screenDumpDirOverride(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { cfg := NewConfig(nil) - assert.NoError(t, cfg.Load("testdata/configs/k9s.yaml")) + assert.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true)) cfg.K9s.manualScreenDumpDir = &u.dir assert.Equal(t, u.e, cfg.K9s.AppScreenDumpDir()) diff --git a/internal/config/k9s_test.go b/internal/config/k9s_test.go index 69e76189c8..d74c59c39d 100644 --- a/internal/config/k9s_test.go +++ b/internal/config/k9s_test.go @@ -136,13 +136,13 @@ func TestContextScreenDumpDir(t *testing.T) { _, err := cfg.K9s.ActivateContext("ct-1-1") assert.NoError(t, err) - assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml")) + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) assert.Equal(t, "/tmp/k9s-test/screen-dumps/cl-1/ct-1-1", cfg.K9s.ContextScreenDumpDir()) } func TestAppScreenDumpDir(t *testing.T) { cfg := mock.NewMockConfig() - assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml")) + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) assert.Equal(t, "/tmp/k9s-test/screen-dumps", cfg.K9s.AppScreenDumpDir()) } diff --git a/internal/config/mock/test_helpers.go b/internal/config/mock/test_helpers.go index c5a3b69a75..eae5709d43 100644 --- a/internal/config/mock/test_helpers.go +++ b/internal/config/mock/test_helpers.go @@ -4,7 +4,9 @@ package mock import ( + "errors" "fmt" + "io/fs" "os" "strings" @@ -21,7 +23,7 @@ import ( ) func EnsureDir(d string) error { - if _, err := os.Stat(d); os.IsNotExist(err) { + if _, err := os.Stat(d); errors.Is(err, fs.ErrNotExist) { return os.MkdirAll(d, 0700) } if err := os.RemoveAll(d); err != nil { diff --git a/internal/config/plugin.go b/internal/config/plugin.go index e0992fe5d6..e57aa53a85 100644 --- a/internal/config/plugin.go +++ b/internal/config/plugin.go @@ -6,6 +6,7 @@ package config import ( "errors" "fmt" + "io/fs" "os" "path/filepath" "strings" @@ -94,7 +95,7 @@ func (p Plugins) loadPluginDir(dir string) error { } func (p *Plugins) load(path string) error { - if _, err := os.Stat(path); os.IsNotExist(err) { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { return nil } bb, err := os.ReadFile(path) diff --git a/internal/config/views.go b/internal/config/views.go index 1bab44c80d..cfa1b7caa1 100644 --- a/internal/config/views.go +++ b/internal/config/views.go @@ -4,8 +4,11 @@ package config import ( + "errors" "fmt" + "io/fs" "os" + "strings" "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/json" @@ -25,6 +28,26 @@ type ViewSetting struct { SortColumn string `yaml:"sortColumn"` } +func (v *ViewSetting) HasCols() bool { + return len(v.Columns) > 0 +} + +func (v *ViewSetting) IsBlank() bool { + return v == nil || len(v.Columns) == 0 +} + +func (v *ViewSetting) SortCol() (string, bool, error) { + if v == nil || v.SortColumn == "" { + return "", false, fmt.Errorf("no sort column specified") + } + tt := strings.Split(v.SortColumn, ":") + if len(tt) < 2 { + return "", false, fmt.Errorf("invalid sort column spec: %q. must be col-name:asc|desc", v.SortColumn) + } + + return tt[0], tt[1] == "desc", nil +} + // CustomView represents a collection of view customization. type CustomView struct { Views map[string]ViewSetting `yaml:"views"` @@ -48,7 +71,7 @@ func (v *CustomView) Reset() { // Load loads view configurations. func (v *CustomView) Load(path string) error { - if _, err := os.Stat(path); os.IsNotExist(err) { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { return nil } bb, err := os.ReadFile(path) diff --git a/internal/dao/cluster.go b/internal/dao/cluster.go index 6dcc8d25dd..dab4fbf158 100644 --- a/internal/dao/cluster.go +++ b/internal/dao/cluster.go @@ -38,7 +38,7 @@ var ( _ RefScanner = (*DaemonSet)(nil) _ RefScanner = (*Job)(nil) _ RefScanner = (*CronJob)(nil) - _ RefScanner = (*Pod)(nil) + // _ RefScanner = (*Pod)(nil) ) func scanners() map[string]RefScanner { @@ -48,7 +48,7 @@ func scanners() map[string]RefScanner { "apps/v1/daemonsets": &DaemonSet{}, "batch/v1/jobs": &Job{}, "batch/v1/cronjobs": &CronJob{}, - "v1/pods": &Pod{}, + // "v1/pods": &Pod{}, } } diff --git a/internal/dao/cm.go b/internal/dao/cm.go new file mode 100644 index 0000000000..0910d70495 --- /dev/null +++ b/internal/dao/cm.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +var ( + _ Accessor = (*ConfigMap)(nil) +) + +// ConfigMap represents a configmap resource. +type ConfigMap struct { + Resource +} diff --git a/internal/dao/crd.go b/internal/dao/crd.go index 16f06076d7..5f8455a27c 100644 --- a/internal/dao/crd.go +++ b/internal/dao/crd.go @@ -3,15 +3,6 @@ package dao -import ( - "context" - - "github.com/derailed/k9s/internal" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" -) - var ( _ Accessor = (*CustomResourceDefinition)(nil) _ Nuker = (*CustomResourceDefinition)(nil) @@ -21,28 +12,3 @@ var ( type CustomResourceDefinition struct { Resource } - -// IsHappy check for happy deployments. -func (c *CustomResourceDefinition) IsHappy(crd v1.CustomResourceDefinition) bool { - versions := make([]string, 0, 3) - for _, v := range crd.Spec.Versions { - if v.Served && !v.Deprecated { - versions = append(versions, v.Name) - break - } - } - - return len(versions) > 0 -} - -// List returns a collection of nodes. -func (c *CustomResourceDefinition) List(ctx context.Context, _ string) ([]runtime.Object, error) { - strLabel, ok := ctx.Value(internal.KeyLabels).(string) - labelSel := labels.Everything() - if sel, e := labels.ConvertSelectorToLabelsMap(strLabel); ok && e == nil { - labelSel = sel.AsSelector() - } - - const gvr = "apiextensions.k8s.io/v1/customresourcedefinitions" - return c.getFactory().List(gvr, "-", false, labelSel) -} diff --git a/internal/dao/dp.go b/internal/dao/dp.go index 4f88d4a02a..f3273399e9 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -49,11 +49,6 @@ func (d *Deployment) ListImages(ctx context.Context, fqn string) ([]string, erro return render.ExtractImages(&dp.Spec.Template.Spec), nil } -// IsHappy check for happy deployments. -func (d *Deployment) IsHappy(dp appsv1.Deployment) bool { - return dp.Status.Replicas == dp.Status.AvailableReplicas -} - // Scale a Deployment. func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error { ns, n := client.Namespaced(path) diff --git a/internal/dao/ds.go b/internal/dao/ds.go index 0e2d9430a0..b0bf8c0b69 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -51,11 +51,6 @@ func (d *DaemonSet) ListImages(ctx context.Context, fqn string) ([]string, error return render.ExtractImages(&ds.Spec.Template.Spec), nil } -// IsHappy check for happy deployments. -func (d *DaemonSet) IsHappy(ds appsv1.DaemonSet) bool { - return ds.Status.DesiredNumberScheduled == ds.Status.CurrentNumberScheduled -} - // Restart a DaemonSet rollout. func (d *DaemonSet) Restart(ctx context.Context, path string) error { o, err := d.getFactory().Get("apps/v1/daemonsets", path, true, labels.Everything()) @@ -140,7 +135,7 @@ func podLogs(ctx context.Context, sel map[string]string, opts *LogOptions) ([]Lo } opts.MultiPods = true - po := Pod{} + var po Pod po.Init(f, client.NewGVR("v1/pods")) outs := make([]LogChan, 0, len(oo)) diff --git a/internal/dao/helpers.go b/internal/dao/helpers.go index f4818f4863..552d31378f 100644 --- a/internal/dao/helpers.go +++ b/internal/dao/helpers.go @@ -6,47 +6,65 @@ 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" ) -func inList(ll []string, s string) bool { - for _, l := range ll { - if l == s { - return true +// 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 } } - return false + log.Warn().Msg(defaultContainer + " container not found. " + defaultContainerAnnotation + " annotation will be ignored") + + return "", false } -// IsInverseSelector checks if inverse char has been provided. -func IsInverseSelector(s string) bool { - if s == "" { - 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 inverseRx.MatchString(s) + + return FQN(u.GetNamespace(), u.GetName()) } -// HasFuzzySelector checks if query is fuzzy. -func HasFuzzySelector(s string) (string, bool) { - mm := fuzzyRx.FindStringSubmatch(s) - if len(mm) != 2 { - return "", false +// FQN returns a fully qualified resource name. +func FQN(ns, n string) string { + if ns == "" { + return n } + return ns + "/" + n +} - return mm[1], true +func inList(ll []string, s string) bool { + for _, l := range ll { + if l == s { + return true + } + } + return false } func toPerc(v1, v2 float64) float64 { diff --git a/internal/dao/log_items.go b/internal/dao/log_items.go index d8cd1707cf..898c0c842f 100644 --- a/internal/dao/log_items.go +++ b/internal/dao/log_items.go @@ -10,6 +10,7 @@ import ( "strings" "sync" + "github.com/derailed/k9s/internal" "github.com/sahilm/fuzzy" ) @@ -174,7 +175,7 @@ func (l *LogItems) Filter(index int, q string, showTime bool) ([]int, [][]int, e if q == "" { return nil, nil, nil } - if f, ok := HasFuzzySelector(q); ok { + if f, ok := internal.IsFuzzySelector(q); ok { mm, ii := l.fuzzyFilter(index, f, showTime) return mm, ii, nil } @@ -200,7 +201,7 @@ func (l *LogItems) fuzzyFilter(index int, q string, showTime bool) ([]int, [][]i func (l *LogItems) filterLogs(index int, q string, showTime bool) ([]int, [][]int, error) { var invert bool - if IsInverseSelector(q) { + if internal.IsInverseSelector(q) { invert = true q = q[1:] } diff --git a/internal/dao/ns.go b/internal/dao/ns.go index 4daec5f9bc..33a63dced0 100644 --- a/internal/dao/ns.go +++ b/internal/dao/ns.go @@ -3,27 +3,11 @@ package dao -import ( - "context" - - "k8s.io/apimachinery/pkg/runtime" -) - var ( - _ Accessor = (*Pod)(nil) + _ Accessor = (*Namespace)(nil) ) // Namespace represents a namespace resource. type Namespace struct { - Generic -} - -// List returns a collection of namespaces. -func (n *Namespace) List(ctx context.Context, ns string) ([]runtime.Object, error) { - oo, err := n.Generic.List(ctx, ns) - if err != nil { - return nil, err - } - - return oo, nil + Resource } diff --git a/internal/dao/pod.go b/internal/dao/pod.go index e9c6e09f46..4b03d66ed7 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -37,9 +37,8 @@ var ( ) const ( - logRetryCount = 20 - logRetryWait = 1 * time.Second - defaultContainerAnnotation = "kubectl.kubernetes.io/default-container" + logRetryCount = 20 + logRetryWait = 1 * time.Second ) // Pod represents a pod resource. @@ -47,17 +46,6 @@ type Pod struct { Resource } -// IsHappy check for happy deployments. -func (p *Pod) IsHappy(po v1.Pod) bool { - for _, c := range po.Status.Conditions { - if c.Status == v1.ConditionFalse { - return false - } - } - - return true -} - // Get returns a resource instance if found, else an error. func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) { o, err := p.Resource.Get(ctx, path) @@ -425,24 +413,6 @@ func MetaFQN(m metav1.ObjectMeta) string { return FQN(m.Namespace, m.Name) } -// FQN returns a fully qualified resource name. -func FQN(ns, n string) string { - if ns == "" { - return n - } - return ns + "/" + n -} - -func extractFQN(o runtime.Object) string { - u, ok := o.(*unstructured.Unstructured) - if !ok { - log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o)) - return client.NA - } - - return FQN(u.GetNamespace(), u.GetName()) -} - // GetPodSpec returns a pod spec given a resource. func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) { pod, err := p.GetInstance(path) @@ -500,21 +470,6 @@ func (p *Pod) isControlled(path string) (string, bool, error) { return "", false, nil } -// GetDefaultContainer returns a container name if specified in an annotation. -func GetDefaultContainer(m metav1.ObjectMeta, spec v1.PodSpec) (string, bool) { - defaultContainer, ok := m.Annotations[defaultContainerAnnotation] - if ok { - for _, container := range spec.Containers { - if container.Name == defaultContainer { - return defaultContainer, true - } - } - log.Warn().Msg(defaultContainer + " container not found. " + defaultContainerAnnotation + " annotation will be ignored") - } - - return "", false -} - func (p *Pod) Sanitize(ctx context.Context, ns string) (int, error) { oo, err := p.Resource.List(ctx, ns) if err != nil { diff --git a/internal/dao/popeye.go b/internal/dao/popeye.go index ec1aa801d3..7cb08e9a27 100644 --- a/internal/dao/popeye.go +++ b/internal/dao/popeye.go @@ -3,7 +3,7 @@ package dao -// !!BOZO!! +// !!BOZO!! Popeye // import ( // "bytes" // "context" diff --git a/internal/dao/registry.go b/internal/dao/registry.go index 4060d4e9b0..3aaf1bf5ac 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -89,29 +89,32 @@ func NewMeta() *Meta { // Customize here for non resource types or types with metrics or logs. func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) { m := Accessors{ - client.NewGVR("workloads"): &Workload{}, - client.NewGVR("contexts"): &Context{}, - client.NewGVR("containers"): &Container{}, - client.NewGVR("scans"): &ImageScan{}, - client.NewGVR("screendumps"): &ScreenDump{}, - client.NewGVR("benchmarks"): &Benchmark{}, - client.NewGVR("portforwards"): &PortForward{}, - client.NewGVR("v1/services"): &Service{}, - client.NewGVR("v1/pods"): &Pod{}, - client.NewGVR("v1/nodes"): &Node{}, - client.NewGVR("apps/v1/deployments"): &Deployment{}, - client.NewGVR("apps/v1/daemonsets"): &DaemonSet{}, - client.NewGVR("apps/v1/statefulsets"): &StatefulSet{}, - client.NewGVR("apps/v1/replicasets"): &ReplicaSet{}, - client.NewGVR("batch/v1/cronjobs"): &CronJob{}, - client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{}, - client.NewGVR("batch/v1/jobs"): &Job{}, - client.NewGVR("v1/namespaces"): &Namespace{}, - // !!BOZO!! + client.NewGVR("workloads"): &Workload{}, + client.NewGVR("contexts"): &Context{}, + client.NewGVR("containers"): &Container{}, + client.NewGVR("scans"): &ImageScan{}, + client.NewGVR("screendumps"): &ScreenDump{}, + client.NewGVR("benchmarks"): &Benchmark{}, + client.NewGVR("portforwards"): &PortForward{}, + client.NewGVR("dir"): &Dir{}, + client.NewGVR("v1/services"): &Service{}, + client.NewGVR("v1/pods"): &Pod{}, + client.NewGVR("v1/nodes"): &Node{}, + client.NewGVR("v1/namespaces"): &Namespace{}, + client.NewGVR("v1/configmap"): &ConfigMap{}, + client.NewGVR("v1/secrets"): &Secret{}, + client.NewGVR("apps/v1/deployments"): &Deployment{}, + client.NewGVR("apps/v1/daemonsets"): &DaemonSet{}, + client.NewGVR("apps/v1/statefulsets"): &StatefulSet{}, + client.NewGVR("apps/v1/replicasets"): &ReplicaSet{}, + client.NewGVR("batch/v1/cronjobs"): &CronJob{}, + client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{}, + client.NewGVR("batch/v1/jobs"): &Job{}, + client.NewGVR("helm"): &HelmChart{}, + client.NewGVR("helm-history"): &HelmHistory{}, + client.NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions"): &CustomResourceDefinition{}, + // !!BOZO!! Popeye //client.NewGVR("popeye"): &Popeye{}, - client.NewGVR("helm"): &HelmChart{}, - client.NewGVR("helm-history"): &HelmHistory{}, - client.NewGVR("dir"): &Dir{}, } r, ok := m[gvr] @@ -369,6 +372,7 @@ func loadPreferred(f Factory, m ResourceMetas) error { if err != nil { return err } + dial.Invalidate() rr, err := dial.ServerPreferredResources() if err != nil { log.Debug().Err(err).Msgf("Failed to load preferred resources") diff --git a/internal/dao/secret.go b/internal/dao/secret.go index 8cc47868c6..49dcaa3000 100644 --- a/internal/dao/secret.go +++ b/internal/dao/secret.go @@ -15,13 +15,13 @@ import ( // Secret represents a secret K8s resource. type Secret struct { - Table + Resource decode bool } // Describe describes a secret that can be encoded or decoded. func (s *Secret) Describe(path string) (string, error) { - encodedDescription, err := s.Table.Describe(path) + encodedDescription, err := s.Generic.Describe(path) if err != nil { return "", err @@ -51,13 +51,13 @@ func (s *Secret) Decode(encodedDescription, path string) (string, error) { dataEndIndex := strings.Index(encodedDescription, "====") if dataEndIndex == -1 { - return "", fmt.Errorf("Unable to find data section in secret description") + return "", fmt.Errorf("unable to find data section in secret description") } dataEndIndex += 4 if dataEndIndex >= len(encodedDescription) { - return "", fmt.Errorf("Data section in secret description is invalid") + return "", fmt.Errorf("data section in secret description is invalid") } // Remove the encoded part from k8s's describe API diff --git a/internal/dao/sts.go b/internal/dao/sts.go index f562adef7c..0137640957 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -50,11 +50,6 @@ func (s *StatefulSet) ListImages(ctx context.Context, fqn string) ([]string, err return render.ExtractImages(&sts.Spec.Template.Spec), nil } -// IsHappy check for happy sts. -func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool { - return sts.Status.Replicas == sts.Status.ReadyReplicas -} - // Scale a StatefulSet. func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error { ns, n := client.Namespaced(path) diff --git a/internal/dao/table.go b/internal/dao/table.go index c2569da183..6936772f82 100644 --- a/internal/dao/table.go +++ b/internal/dao/table.go @@ -16,7 +16,9 @@ import ( "k8s.io/client-go/rest" ) -// BOZO!! Figure out how to convert to table def and use factory. +const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" + +var genScheme = runtime.NewScheme() // Table retrieves K8s resources as tabular data. type Table struct { @@ -25,19 +27,19 @@ type Table struct { // Get returns a given resource. func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { - a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) - _, codec := t.codec() - - c, err := t.getClient() + f, p := t.codec() + c, err := t.getClient(f) if err != nil { return nil, err } + ns, n := client.Namespaced(path) + a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) req := c.Get(). SetHeader("Accept", a). Name(n). Resource(t.gvr.R()). - VersionedParams(&metav1.TableOptions{}, codec) + VersionedParams(&metav1.TableOptions{}, p) if ns != client.ClusterScope { req = req.Namespace(ns) } @@ -48,18 +50,24 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { // List all Resources in a given namespace. func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { labelSel, _ := ctx.Value(internal.KeyLabels).(string) - a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) - _, codec := t.codec() + fieldSel, _ := ctx.Value(internal.KeyFields).(string) - c, err := t.getClient() + f, p := t.codec() + c, err := t.getClient(f) if err != nil { return nil, err } + a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) o, err := c.Get(). SetHeader("Accept", a). Namespace(ns). Resource(t.gvr.R()). - VersionedParams(&metav1.ListOptions{LabelSelector: labelSel}, codec). + VersionedParams(&metav1.ListOptions{ + LabelSelector: labelSel, + FieldSelector: fieldSel, + ResourceVersion: "0", + ResourceVersionMatch: v1.ResourceVersionMatchNotOlderThan, + }, p). Do(ctx).Get() if err != nil { return nil, err @@ -71,9 +79,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { // ---------------------------------------------------------------------------- // Helpers... -const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" - -func (t *Table) getClient() (*rest.RESTClient, error) { +func (t *Table) getClient(f serializer.CodecFactory) (*rest.RESTClient, error) { cfg, err := t.Client().RestConfig() if err != nil { return nil, err @@ -84,8 +90,7 @@ func (t *Table) getClient() (*rest.RESTClient, error) { if t.gvr.G() == "" { cfg.APIPath = "/api" } - codec, _ := t.codec() - cfg.NegotiatedSerializer = codec.WithoutConversion() + cfg.NegotiatedSerializer = f.WithoutConversion() crRestClient, err := rest.RESTClientFor(cfg) if err != nil { @@ -96,11 +101,12 @@ func (t *Table) getClient() (*rest.RESTClient, error) { } func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) { - scheme := runtime.NewScheme() + var tt metav1.Table + opts := metav1.TableOptions{IncludeObject: v1.IncludeObject} gv := t.gvr.GV() - metav1.AddToGroupVersion(scheme, gv) - scheme.AddKnownTypes(gv, &metav1.Table{}, &metav1.TableOptions{IncludeObject: v1.IncludeObject}) - scheme.AddKnownTypes(metav1.SchemeGroupVersion, &metav1.Table{}, &metav1.TableOptions{IncludeObject: v1.IncludeObject}) + metav1.AddToGroupVersion(genScheme, gv) + genScheme.AddKnownTypes(gv, &tt, &opts) + genScheme.AddKnownTypes(metav1.SchemeGroupVersion, &tt, &opts) - return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) + return serializer.NewCodecFactory(genScheme), runtime.NewParameterCodec(genScheme) } diff --git a/internal/helpers.go b/internal/helpers.go new file mode 100644 index 0000000000..aa1d43e50f --- /dev/null +++ b/internal/helpers.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package internal + +import ( + "regexp" + "strings" + + "github.com/derailed/k9s/internal/view/cmd" +) + +var ( + inverseRx = regexp.MustCompile(`\A\!`) + fuzzyRx = regexp.MustCompile(`\A-f\s?([\w-]+)\b`) + labelRx = regexp.MustCompile(`\A\-l`) +) + +// Helpers... + +// IsInverseSelector checks if inverse char has been provided. +func IsInverseSelector(s string) bool { + if s == "" { + return false + } + return inverseRx.MatchString(s) +} + +// IsLabelSelector checks if query is a label query. +func IsLabelSelector(s string) bool { + if labelRx.MatchString(s) { + return true + } + + return !strings.Contains(s, " ") && cmd.ToLabels(s) != nil +} + +// IsFuzzySelector checks if query is fuzzy. +func IsFuzzySelector(s string) (string, bool) { + mm := fuzzyRx.FindStringSubmatch(s) + if len(mm) != 2 { + return "", false + } + + return mm[1], true +} diff --git a/internal/helpers_test.go b/internal/helpers_test.go new file mode 100644 index 0000000000..7ac6bc9985 --- /dev/null +++ b/internal/helpers_test.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package internal_test + +import ( + "testing" + + "github.com/derailed/k9s/internal" + "github.com/stretchr/testify/assert" +) + +func TestIsLabelSelector(t *testing.T) { + uu := map[string]struct { + s string + ok bool + }{ + "empty": {s: ""}, + "cool": {s: "-l app=fred,env=blee", ok: true}, + "no-flag": {s: "app=fred,env=blee", ok: true}, + "no-space": {s: "-lapp=fred,env=blee", ok: true}, + "wrong-flag": {s: "-f app=fred,env=blee"}, + "missing-key": {s: "=fred"}, + "missing-val": {s: "fred="}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.ok, internal.IsLabelSelector(u.s)) + }) + } +} diff --git a/internal/model/cluster.go b/internal/model/cluster.go index 3182a0e13c..e598217e92 100644 --- a/internal/model/cluster.go +++ b/internal/model/cluster.go @@ -108,7 +108,7 @@ func (c *Cluster) Metrics(ctx context.Context, mx *client.ClusterMetrics) error } } if nn == nil { - return errors.New("Unable to fetch nodes list") + return errors.New("unable to fetch nodes list") } if len(nn.Items) > 0 { c.cache.Add(clusterNodesKey, nn, clusterCacheExpiry) diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index 5f1653be38..62f6e7f1dd 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -143,8 +143,6 @@ func (c *ClusterInfo) Refresh() { var mx client.ClusterMetrics if err := c.cluster.Metrics(ctx, &mx); err == nil { data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral - } else { - log.Warn().Err(err).Msgf("Cluster metrics failed") } } data.K9sVer = c.version diff --git a/internal/model/describe.go b/internal/model/describe.go index 55b525bfb4..2d5d845a62 100644 --- a/internal/model/describe.go +++ b/internal/model/describe.go @@ -12,6 +12,7 @@ import ( "time" backoff "github.com/cenkalti/backoff/v4" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" @@ -66,7 +67,7 @@ func (d *Describe) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if f, ok := dao.HasFuzzySelector(q); ok { + if f, ok := internal.IsFuzzySelector(q); ok { return d.fuzzyFilter(strings.TrimSpace(f), lines) } return rxFilter(q, lines) diff --git a/internal/model/pulse_health.go b/internal/model/pulse_health.go index 3cf71c23de..0f1891bb97 100644 --- a/internal/model/pulse_health.go +++ b/internal/model/pulse_health.go @@ -10,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/health" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -123,14 +124,14 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check, if !ok { return nil, fmt.Errorf("expecting a meta table but got %T", oo[0]) } - rows := make(render.Rows, len(table.Rows)) - re, _ := meta.Renderer.(Generic) + rows := make(model1.Rows, len(table.Rows)) + re, _ := meta.Renderer.(model1.Generic) re.SetTable(ns, table) for i, row := range table.Rows { if err := re.Render(row, ns, &rows[i]); err != nil { return nil, err } - if !render.Happy(ns, re.Header(ns), rows[i]) { + if !model1.IsValid(ns, re.Header(ns), rows[i]) { c.Inc(health.S2) continue } @@ -140,12 +141,12 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check, return c, nil } c.Total(int64(len(oo))) - rr, re := make(render.Rows, len(oo)), meta.Renderer + rr, re := make(model1.Rows, len(oo)), meta.Renderer for i, o := range oo { if err := re.Render(o, ns, &rr[i]); err != nil { return nil, err } - if !render.Happy(ns, re.Header(ns), rr[i]) { + if !model1.IsValid(ns, re.Header(ns), rr[i]) { c.Inc(health.S2) continue } diff --git a/internal/model/registry.go b/internal/model/registry.go index 55db15f832..4ad382da5a 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -82,7 +82,7 @@ var Registry = map[string]ResourceMeta{ DAO: &dao.Alias{}, Renderer: &render.Alias{}, }, - // !!BOZO!! + // !!BOZO!! Popeye //"popeye": { // DAO: &dao.Popeye{}, // Renderer: &render.Popeye{}, @@ -102,8 +102,17 @@ var Registry = map[string]ResourceMeta{ TreeRenderer: &xray.Pod{}, }, "v1/namespaces": { + DAO: &dao.Namespace{}, Renderer: &render.Namespace{}, }, + "v1/secrets": { + DAO: &dao.Secret{}, + Renderer: &render.Secret{}, + }, + "v1/configmaps": { + DAO: &dao.ConfigMap{}, + Renderer: &render.ConfigMap{}, + }, "v1/nodes": { DAO: &dao.Node{}, Renderer: &render.Node{}, @@ -113,6 +122,10 @@ var Registry = map[string]ResourceMeta{ Renderer: &render.Service{}, TreeRenderer: &xray.Service{}, }, + "v1/events": { + DAO: &dao.Table{}, + Renderer: &render.Event{}, + }, "v1/serviceaccounts": { Renderer: &render.ServiceAccount{}, }, @@ -122,14 +135,6 @@ var Registry = map[string]ResourceMeta{ "v1/persistentvolumeclaims": { Renderer: &render.PersistentVolumeClaim{}, }, - "v1/events": { - DAO: &dao.Table{}, - Renderer: &render.Event{}, - }, - "v1/secrets": { - DAO: &dao.Secret{}, - Renderer: &render.Generic{}, - }, // Apps... "apps/v1/deployments": { @@ -169,6 +174,7 @@ var Registry = map[string]ResourceMeta{ // CRDs... "apiextensions.k8s.io/v1/customresourcedefinitions": { + DAO: &dao.CustomResourceDefinition{}, Renderer: &render.CustomResourceDefinition{}, }, diff --git a/internal/model/rev_values.go b/internal/model/rev_values.go index 08badab6d5..a0b8cb98c5 100644 --- a/internal/model/rev_values.go +++ b/internal/model/rev_values.go @@ -10,6 +10,7 @@ import ( "time" backoff "github.com/cenkalti/backoff/v4" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" @@ -84,7 +85,7 @@ func (v *RevValues) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if f, ok := dao.HasFuzzySelector(q); ok { + if f, ok := internal.IsFuzzySelector(q); ok { return v.fuzzyFilter(strings.TrimSpace(f), lines) } return rxFilter(q, lines) diff --git a/internal/model/stack.go b/internal/model/stack.go index b660a1ab1b..55221b8560 100644 --- a/internal/model/stack.go +++ b/internal/model/stack.go @@ -112,6 +112,7 @@ func (s *Stack) Pop() (Component, bool) { s.mx.Lock() { c = s.components[len(s.components)-1] + c.Stop() s.components = s.components[:len(s.components)-1] } s.mx.Unlock() diff --git a/internal/model/table.go b/internal/model/table.go index 6259382799..ce848724ab 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -14,7 +14,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -25,7 +25,7 @@ const initRefreshRate = 300 * time.Millisecond // TableListener represents a table model listener. type TableListener interface { // TableDataChanged notifies the model data changed. - TableDataChanged(*render.TableData) + TableDataChanged(*model1.TableData) // TableLoadFailed notifies the load failed. TableLoadFailed(error) @@ -34,21 +34,20 @@ type TableListener interface { // Table represents a table model. type Table struct { gvr client.GVR - namespace string - data *render.TableData + data *model1.TableData listeners []TableListener inUpdate int32 refreshRate time.Duration instance string - mx sync.RWMutex labelFilter string + mx sync.RWMutex } // NewTable returns a new table model. func NewTable(gvr client.GVR) *Table { return &Table{ gvr: gvr, - data: render.NewTableData(), + data: model1.NewTableData(gvr), refreshRate: 2 * time.Second, } } @@ -141,18 +140,17 @@ func (t *Table) Delete(ctx context.Context, path string, propagation *metav1.Del // GetNamespace returns the model namespace. func (t *Table) GetNamespace() string { - return t.namespace + return t.data.GetNamespace() } // SetNamespace sets up model namespace. func (t *Table) SetNamespace(ns string) { - t.namespace = ns - t.data.Clear() + t.data.Reset(ns) } // InNamespace checks if current namespace matches desired namespace. func (t *Table) InNamespace(ns string) bool { - return len(t.data.RowEvents) > 0 && t.namespace == ns + return t.data.GetNamespace() == ns && !t.data.Empty() } // SetRefreshRate sets model refresh duration. @@ -162,7 +160,7 @@ func (t *Table) SetRefreshRate(d time.Duration) { // ClusterWide checks if resource is scope for all namespaces. func (t *Table) ClusterWide() bool { - return client.IsClusterWide(t.namespace) + return client.IsClusterWide(t.data.GetNamespace()) } // Empty returns true if no model data. @@ -170,13 +168,13 @@ func (t *Table) Empty() bool { return t.data.Empty() } -// Count returns the row count. -func (t *Table) Count() int { - return t.data.Count() +// RowCount returns the row count. +func (t *Table) RowCount() int { + return t.data.RowCount() } // Peek returns model data. -func (t *Table) Peek() *render.TableData { +func (t *Table) Peek() *model1.TableData { t.mx.RLock() defer t.mx.RUnlock() @@ -184,8 +182,6 @@ func (t *Table) Peek() *render.TableData { } func (t *Table) updater(ctx context.Context) { - defer log.Debug().Msgf("TABLE-UPDATER canceled -- %q", t.gvr) - bf := backoff.NewExponentialBackOff() bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval rate := initRefreshRate @@ -199,7 +195,7 @@ func (t *Table) updater(ctx context.Context) { return t.refresh(ctx) }, backoff.WithContext(bf, ctx)) if err != nil { - log.Error().Err(err).Msgf("Retry failed") + log.Warn().Err(err).Msgf("reconciler exited") t.fireTableLoadFailed(err) return } @@ -229,24 +225,25 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err } a.Init(factory, t.gvr) - ns := client.CleanseNamespace(t.namespace) - if client.IsClusterScoped(t.namespace) { + t.mx.RLock() + ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) + t.mx.RUnlock() + + ns := client.CleanseNamespace(t.data.GetNamespace()) + if client.IsClusterScoped(ns) { ns = client.BlankNamespace } - ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) return a.List(ctx, ns) } func (t *Table) reconcile(ctx context.Context) error { - t.mx.Lock() - defer t.mx.Unlock() - meta := resourceMeta(t.gvr) - ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) var ( oo []runtime.Object err error ) + meta := resourceMeta(t.gvr) + ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) if t.instance == "" { oo, err = t.list(ctx, meta.DAO) } else { @@ -257,41 +254,10 @@ func (t *Table) reconcile(ctx context.Context) error { return err } - var rows render.Rows - if len(oo) > 0 { - if meta.Renderer.IsGeneric() { - table, ok := oo[0].(*metav1.Table) - if !ok { - return fmt.Errorf("expecting a meta table but got %T", oo[0]) - } - rows = make(render.Rows, len(table.Rows)) - if err := genericHydrate(t.namespace, table, rows, meta.Renderer); err != nil { - return err - } - } else { - rows = make(render.Rows, len(oo)) - if err := hydrate(t.namespace, oo, rows, meta.Renderer); err != nil { - return err - } - } - } - - // if labelSelector in place might as well clear the model data. - sel, ok := ctx.Value(internal.KeyLabels).(string) - if ok && sel != "" { - t.data.Clear() - } - t.data.Update(rows) - t.data.SetHeader(t.namespace, meta.Renderer.Header(t.namespace)) - - if len(t.data.Header) == 0 { - return fmt.Errorf("fail to list resource %s", t.gvr) - } - - return nil + return t.data.Reconcile(ctx, meta.Renderer, oo) } -func (t *Table) fireTableChanged(data *render.TableData) { +func (t *Table) fireTableChanged(data *model1.TableData) { var ll []TableListener t.mx.RLock() ll = t.listeners @@ -312,43 +278,3 @@ func (t *Table) fireTableLoadFailed(err error) { l.TableLoadFailed(err) } } - -// ---------------------------------------------------------------------------- -// Helpers... - -func hydrate(ns string, oo []runtime.Object, rr render.Rows, re Renderer) error { - for i, o := range oo { - if err := re.Render(o, ns, &rr[i]); err != nil { - return err - } - } - - return nil -} - -// Generic represents a generic resource. -type Generic interface { - // SetTable sets up the resource tabular definition. - SetTable(ns string, table *metav1.Table) - - // Header returns a resource header. - Header(ns string) render.Header - - // Render renders the resource. - Render(o interface{}, ns string, row *render.Row) error -} - -func genericHydrate(ns string, table *metav1.Table, rr render.Rows, re Renderer) error { - gr, ok := re.(Generic) - if !ok { - return fmt.Errorf("expecting generic renderer but got %T", re) - } - gr.SetTable(ns, table) - for i, row := range table.Rows { - if err := gr.Render(row, ns, &rr[i]); err != nil { - return err - } - } - - return nil -} diff --git a/internal/model/table_int_test.go b/internal/model/table_int_test.go index 522cd18c4c..ea7c03904e 100644 --- a/internal/model/table_int_test.go +++ b/internal/model/table_int_test.go @@ -13,11 +13,11 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/watch" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/informers" @@ -35,9 +35,9 @@ func TestTableReconcile(t *testing.T) { err := ta.reconcile(ctx) assert.Nil(t, err) data := ta.Peek() - assert.Equal(t, 23, len(data.Header)) - assert.Equal(t, 1, len(data.RowEvents)) - assert.Equal(t, client.NamespaceAll, data.Namespace) + assert.Equal(t, 23, data.HeaderCount()) + assert.Equal(t, 1, data.RowCount()) + assert.Equal(t, client.NamespaceAll, data.GetNamespace()) } func TestTableList(t *testing.T) { @@ -66,12 +66,10 @@ func TestTableGet(t *testing.T) { } func TestTableMeta(t *testing.T) { - pd := dao.Pod{} - pd.Init(makeFactory(), client.NewGVR("v1/pods")) uu := map[string]struct { gvr string accessor dao.Accessor - renderer Renderer + renderer model1.Renderer }{ "generic": { gvr: "containers", @@ -84,9 +82,9 @@ func TestTableMeta(t *testing.T) { renderer: &render.Node{}, }, "table": { - gvr: "v1/configmaps", + gvr: "v1/events", accessor: &dao.Table{}, - renderer: &render.Generic{}, + renderer: &render.Event{}, }, } @@ -100,44 +98,6 @@ func TestTableMeta(t *testing.T) { } } -func TestTableHydrate(t *testing.T) { - oo := []runtime.Object{ - &render.PodWithMetrics{Raw: load(t, "p1")}, - } - rr := make([]render.Row, 1) - - assert.Nil(t, hydrate("blee", oo, rr, render.Pod{})) - assert.Equal(t, 1, len(rr)) - assert.Equal(t, 23, len(rr[0].Fields)) -} - -func TestTableGenericHydrate(t *testing.T) { - raw := raw(t, "p1") - tt := metav1beta1.Table{ - ColumnDefinitions: []metav1beta1.TableColumnDefinition{ - {Name: "c1"}, - {Name: "c2"}, - }, - Rows: []metav1beta1.TableRow{ - { - Cells: []interface{}{"fred", 10}, - Object: runtime.RawExtension{Raw: raw}, - }, - { - Cells: []interface{}{"blee", 20}, - Object: runtime.RawExtension{Raw: raw}, - }, - }, - } - rr := make([]render.Row, 2) - re := render.Generic{} - re.SetTable("blee", &tt) - - assert.Nil(t, genericHydrate("blee", &tt, rr, &re)) - assert.Equal(t, 2, len(rr)) - assert.Equal(t, 3, len(rr[0].Fields)) -} - // ---------------------------------------------------------------------------- // Helpers... @@ -162,12 +122,6 @@ func load(t *testing.T, n string) *unstructured.Unstructured { return &o } -func raw(t *testing.T, n string) []byte { - raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) - assert.Nil(t, err) - return raw -} - // ---------------------------------------------------------------------------- func makeFactory() testFactory { diff --git a/internal/model/table_test.go b/internal/model/table_test.go index ec636b9ed9..5417101778 100644 --- a/internal/model/table_test.go +++ b/internal/model/table_test.go @@ -14,7 +14,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/watch" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -36,9 +36,9 @@ func TestTableRefresh(t *testing.T) { ctx = context.WithValue(ctx, internal.KeyWithMetrics, false) assert.NoError(t, ta.Refresh(ctx)) data := ta.Peek() - assert.Equal(t, 23, len(data.Header)) - assert.Equal(t, 1, len(data.RowEvents)) - assert.Equal(t, client.NamespaceAll, data.Namespace) + assert.Equal(t, 23, data.HeaderCount()) + assert.Equal(t, 1, data.RowCount()) + assert.Equal(t, client.NamespaceAll, data.GetNamespace()) assert.Equal(t, 1, l.count) assert.Equal(t, 0, l.errs) } @@ -75,7 +75,7 @@ type tableListener struct { count, errs int } -func (l *tableListener) TableDataChanged(*render.TableData) { +func (l *tableListener) TableDataChanged(*model1.TableData) { l.count++ } diff --git a/internal/model/text.go b/internal/model/text.go index 64e4d4f90a..b50c8680af 100644 --- a/internal/model/text.go +++ b/internal/model/text.go @@ -6,7 +6,7 @@ package model import ( "strings" - "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal" "github.com/sahilm/fuzzy" ) @@ -111,7 +111,7 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if f, ok := dao.HasFuzzySelector(q); ok { + if f, ok := internal.IsFuzzySelector(q); ok { return t.fuzzyFilter(strings.TrimSpace(f), lines) } return rxFilter(q, lines) diff --git a/internal/model/types.go b/internal/model/types.go index b8ec0b6c84..e729b32204 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -9,7 +9,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tview" "github.com/sahilm/fuzzy" "k8s.io/apimachinery/pkg/runtime" @@ -100,21 +100,6 @@ type Filterer interface { SetLabelFilter(map[string]string) } -// Renderer represents a resource renderer. -type Renderer interface { - // IsGeneric identifies a generic handler. - IsGeneric() bool - - // Render converts raw resources to tabular data. - Render(o interface{}, ns string, row *render.Row) error - - // Header returns the resource header. - Header(ns string) render.Header - - // ColorerFunc returns a row colorer function. - ColorerFunc() render.ColorerFunc -} - // Cruder performs crud operations. type Cruder interface { // List returns a collection of resources. @@ -149,6 +134,6 @@ type TreeRenderer interface { // ResourceMeta represents model info about a resource. type ResourceMeta struct { DAO dao.Accessor - Renderer Renderer + Renderer model1.Renderer TreeRenderer TreeRenderer } diff --git a/internal/model/values.go b/internal/model/values.go index 25870d5a3b..eafe098d30 100644 --- a/internal/model/values.go +++ b/internal/model/values.go @@ -11,6 +11,7 @@ import ( "time" backoff "github.com/cenkalti/backoff/v4" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" @@ -113,7 +114,7 @@ func (v *Values) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if f, ok := dao.HasFuzzySelector(q); ok { + if f, ok := internal.IsFuzzySelector(q); ok { return v.fuzzyFilter(strings.TrimSpace(f), lines) } return rxFilter(q, lines) diff --git a/internal/model/yaml.go b/internal/model/yaml.go index 7e7dd1a243..e476e27288 100644 --- a/internal/model/yaml.go +++ b/internal/model/yaml.go @@ -74,7 +74,7 @@ func (y *YAML) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if f, ok := dao.HasFuzzySelector(q); ok { + if f, ok := internal.IsFuzzySelector(q); ok { return y.fuzzyFilter(strings.TrimSpace(f), lines) } return rxFilter(q, lines) diff --git a/internal/render/color.go b/internal/model1/color.go similarity index 74% rename from internal/render/color.go rename to internal/model1/color.go index 816c1e39f7..f570a0409e 100644 --- a/internal/render/color.go +++ b/internal/model1/color.go @@ -1,11 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s -package render +package model1 -import ( - "github.com/derailed/tcell/v2" -) +import "github.com/derailed/tcell/v2" var ( // ModColor row modified color. @@ -33,12 +31,9 @@ var ( CompletedColor tcell.Color ) -// ColorerFunc represents a resource row colorer. -type ColorerFunc func(ns string, h Header, re RowEvent) tcell.Color - // DefaultColorer set the default table row colors. -func DefaultColorer(ns string, h Header, re RowEvent) tcell.Color { - if !Happy(ns, h, re.Row) { +func DefaultColorer(ns string, h Header, re *RowEvent) tcell.Color { + if !IsValid(ns, h, re.Row) { return ErrColor } diff --git a/internal/model1/color_test.go b/internal/model1/color_test.go new file mode 100644 index 0000000000..985bab95a9 --- /dev/null +++ b/internal/model1/color_test.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/model1" + "github.com/derailed/tcell/v2" + "github.com/stretchr/testify/assert" +) + +func TestDefaultColorer(t *testing.T) { + uu := map[string]struct { + re model1.RowEvent + e tcell.Color + }{ + "add": { + model1.RowEvent{ + Kind: model1.EventAdd, + }, + model1.AddColor, + }, + "update": { + model1.RowEvent{ + Kind: model1.EventUpdate, + }, + model1.ModColor, + }, + "delete": { + model1.RowEvent{ + Kind: model1.EventDelete, + }, + model1.KillColor, + }, + "no-change": { + model1.RowEvent{ + Kind: model1.EventUnchanged, + }, + model1.StdColor, + }, + "invalid": { + model1.RowEvent{ + Kind: model1.EventUnchanged, + Row: model1.Row{ + Fields: model1.Fields{"", "", "blah"}, + }, + }, + model1.ErrColor, + }, + } + + h := model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "VALID"}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, model1.DefaultColorer("", h, &u.re)) + }) + } +} diff --git a/internal/render/delta.go b/internal/model1/delta.go similarity index 97% rename from internal/render/delta.go rename to internal/model1/delta.go index bdc95aa1c0..3d3e32dc84 100644 --- a/internal/render/delta.go +++ b/internal/model1/delta.go @@ -1,11 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s -package render +package model1 -import ( - "reflect" -) +import "reflect" // DeltaRow represents a collection of row deltas between old and new row. type DeltaRow []string diff --git a/internal/model1/delta_test.go b/internal/model1/delta_test.go new file mode 100644 index 0000000000..a809660c4c --- /dev/null +++ b/internal/model1/delta_test.go @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/model1" + "github.com/stretchr/testify/assert" +) + +func TestDeltaLabelize(t *testing.T) { + uu := map[string]struct { + o model1.Row + n model1.Row + e model1.DeltaRow + }{ + "same": { + o: model1.Row{ + Fields: model1.Fields{"a", "b", "blee=fred,doh=zorg"}, + }, + n: model1.Row{ + Fields: model1.Fields{"a", "b", "blee=fred1,doh=zorg"}, + }, + e: model1.DeltaRow{"", "", "fred", "zorg"}, + }, + } + + hh := model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, + } + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + d := model1.NewDeltaRow(u.o, u.n, hh) + d = d.Labelize([]int{0, 1}, 2) + assert.Equal(t, u.e, d) + }) + } +} + +func TestDeltaCustomize(t *testing.T) { + uu := map[string]struct { + r1, r2 model1.Row + cols []int + e model1.DeltaRow + }{ + "same": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + cols: []int{0, 1, 2}, + e: model1.DeltaRow{"", "", ""}, + }, + "empty": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + e: model1.DeltaRow{}, + }, + "diff-full": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a1", "b1", "c1"}, + }, + cols: []int{0, 1, 2}, + e: model1.DeltaRow{"a", "b", "c"}, + }, + "diff-reverse": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a1", "b1", "c1"}, + }, + cols: []int{2, 1, 0}, + e: model1.DeltaRow{"c", "b", "a"}, + }, + "diff-skip": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a1", "b1", "c1"}, + }, + cols: []int{2, 0}, + e: model1.DeltaRow{"c", "a"}, + }, + "diff-missing": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a1", "b1", "c1"}, + }, + cols: []int{2, 10, 0}, + e: model1.DeltaRow{"c", "", "a"}, + }, + "diff-negative": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a1", "b1", "c1"}, + }, + cols: []int{2, -1, 0}, + e: model1.DeltaRow{"c", "", "a"}, + }, + } + + hh := model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, + } + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + d := model1.NewDeltaRow(u.r1, u.r2, hh) + out := make(model1.DeltaRow, len(u.cols)) + d.Customize(u.cols, out) + assert.Equal(t, u.e, out) + }) + } +} + +func TestDeltaNew(t *testing.T) { + uu := map[string]struct { + o model1.Row + n model1.Row + blank bool + e model1.DeltaRow + }{ + "same": { + o: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + n: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + blank: true, + e: model1.DeltaRow{"", "", ""}, + }, + "diff": { + o: model1.Row{ + Fields: model1.Fields{"a1", "b", "c"}, + }, + n: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + e: model1.DeltaRow{"a1", "", ""}, + }, + "diff2": { + o: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + n: model1.Row{ + Fields: model1.Fields{"a", "b1", "c"}, + }, + e: model1.DeltaRow{"", "b", ""}, + }, + "diffLast": { + o: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + n: model1.Row{ + Fields: model1.Fields{"a", "b", "c1"}, + }, + e: model1.DeltaRow{"", "", "c"}, + }, + } + + hh := model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, + } + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + d := model1.NewDeltaRow(u.o, u.n, hh) + assert.Equal(t, u.e, d) + assert.Equal(t, u.blank, d.IsBlank()) + }) + } +} + +func TestDeltaBlank(t *testing.T) { + uu := map[string]struct { + r model1.DeltaRow + e bool + }{ + "empty": { + r: model1.DeltaRow{}, + e: true, + }, + "blank": { + r: model1.DeltaRow{"", "", ""}, + e: true, + }, + "notblank": { + r: model1.DeltaRow{"", "", "z"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.r.IsBlank()) + }) + } +} + +func TestDeltaDiff(t *testing.T) { + uu := map[string]struct { + d1, d2 model1.DeltaRow + ageCol int + e bool + }{ + "empty": { + d1: model1.DeltaRow{"f1", "f2", "f3"}, + ageCol: 2, + e: true, + }, + "same": { + d1: model1.DeltaRow{"f1", "f2", "f3"}, + d2: model1.DeltaRow{"f1", "f2", "f3"}, + ageCol: -1, + }, + "diff": { + d1: model1.DeltaRow{"f1", "f2", "f3"}, + d2: model1.DeltaRow{"f1", "f2", "f13"}, + ageCol: -1, + e: true, + }, + "diff-age-first": { + d1: model1.DeltaRow{"f1", "f2", "f3"}, + d2: model1.DeltaRow{"f1", "f2", "f13"}, + ageCol: 0, + e: true, + }, + "diff-age-last": { + d1: model1.DeltaRow{"f1", "f2", "f3"}, + d2: model1.DeltaRow{"f1", "f2", "f13"}, + ageCol: 2, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.d1.Diff(u.d2, u.ageCol)) + }) + } +} diff --git a/internal/model1/fields.go b/internal/model1/fields.go new file mode 100644 index 0000000000..9242d54f3b --- /dev/null +++ b/internal/model1/fields.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import "reflect" + +// Fields represents a collection of row fields. +type Fields []string + +// Customize returns a subset of fields. +func (f Fields) Customize(cols []int, out Fields) { + for i, c := range cols { + if c < 0 { + out[i] = NAValue + continue + } + if c < len(f) { + out[i] = f[c] + } + } +} + +// Diff returns true if fields differ or false otherwise. +func (f Fields) Diff(ff Fields, ageCol int) bool { + if ageCol < 0 { + return !reflect.DeepEqual(f[:len(f)-1], ff[:len(ff)-1]) + } + if !reflect.DeepEqual(f[:ageCol], ff[:ageCol]) { + return true + } + return !reflect.DeepEqual(f[ageCol+1:], ff[ageCol+1:]) +} + +// Clone returns a copy of the fields. +func (f Fields) Clone() Fields { + cp := make(Fields, len(f)) + copy(cp, f) + + return cp +} diff --git a/internal/render/header.go b/internal/model1/header.go similarity index 82% rename from internal/render/header.go rename to internal/model1/header.go index 5ac9bcd2fb..798f1d92ce 100644 --- a/internal/render/header.go +++ b/internal/model1/header.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s -package render +package model1 import ( "reflect" @@ -34,18 +34,24 @@ func (h HeaderColumn) Clone() HeaderColumn { // Header represents a table header. type Header []HeaderColumn +func (h Header) Clear() Header { + h = h[:0] + + return h +} + // Clone duplicates a header. func (h Header) Clone() Header { - header := make(Header, len(h)) - for i, c := range h { - header[i] = c.Clone() + he := make(Header, 0, len(h)) + for _, h := range h { + he = append(he, h.Clone()) } - return header + return he } // Labelize returns a new Header based on labels. -func (h Header) Labelize(cols []int, labelCol int, rr RowEvents) Header { +func (h Header) Labelize(cols []int, labelCol int, rr *RowEvents) Header { header := make(Header, 0, len(cols)+1) for _, c := range cols { header = append(header, h[c]) @@ -63,8 +69,8 @@ func (h Header) MapIndices(cols []string, wide bool) []int { ii := make([]int, 0, len(cols)) cc := make(map[int]struct{}, len(cols)) for _, col := range cols { - idx := h.IndexOf(col, true) - if idx < 0 { + idx, ok := h.IndexOf(col, true) + if !ok { log.Warn().Msgf("Column %q not found on resource", col) } ii, cc[idx] = append(ii, idx), struct{}{} @@ -90,13 +96,10 @@ func (h Header) Customize(cols []string, wide bool) Header { cc := make(Header, 0, len(h)) xx := make(map[int]struct{}, len(h)) for _, c := range cols { - idx := h.IndexOf(c, true) - if idx == -1 { + idx, ok := h.IndexOf(c, true) + if !ok { log.Warn().Msgf("Column %s is not available on this resource", c) - col := HeaderColumn{ - Name: c, - } - cc = append(cc, col) + cc = append(cc, HeaderColumn{Name: c}) continue } xx[idx] = struct{}{} @@ -129,8 +132,8 @@ func (h Header) Diff(header Header) bool { return !reflect.DeepEqual(h, header) } -// Columns return header as a collection of strings. -func (h Header) Columns(wide bool) []string { +// ColumnNames return header col names +func (h Header) ColumnNames(wide bool) []string { if len(h) == 0 { return nil } @@ -147,7 +150,9 @@ func (h Header) Columns(wide bool) []string { // HasAge returns true if table has an age column. func (h Header) HasAge() bool { - return h.IndexOf(ageCol, true) != -1 + _, ok := h.IndexOf(ageCol, true) + + return ok } // IsMetricsCol checks if given column index represents metrics. @@ -177,22 +182,17 @@ func (h Header) IsCapacityCol(col int) bool { return h[col].Capacity } -// ValidColIndex returns the valid col index or -1 if none. -func (h Header) ValidColIndex() int { - return h.IndexOf("VALID", true) -} - // IndexOf returns the col index or -1 if none. -func (h Header) IndexOf(colName string, includeWide bool) int { +func (h Header) IndexOf(colName string, includeWide bool) (int, bool) { for i, c := range h { if c.Wide && !includeWide { continue } if c.Name == colName { - return i + return i, true } } - return -1 + return -1, false } // Dump for debugging. diff --git a/internal/render/header_test.go b/internal/model1/header_test.go similarity index 53% rename from internal/render/header_test.go rename to internal/model1/header_test.go index 8c82a141dc..3d1d62f321 100644 --- a/internal/render/header_test.go +++ b/internal/model1/header_test.go @@ -1,18 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s -package render_test +package model1_test import ( "testing" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/stretchr/testify/assert" ) func TestHeaderMapIndices(t *testing.T) { uu := map[string]struct { - h1 render.Header + h1 model1.Header cols []string wide bool e []int @@ -50,15 +50,16 @@ func TestHeaderMapIndices(t *testing.T) { func TestHeaderIndexOf(t *testing.T) { uu := map[string]struct { - h render.Header - name string - wide bool - e int + h model1.Header + name string + wide, ok bool + e int }{ "shown": { h: makeHeader(), name: "A", e: 0, + ok: true, }, "hidden": { h: makeHeader(), @@ -70,23 +71,26 @@ func TestHeaderIndexOf(t *testing.T) { name: "B", wide: true, e: 1, + ok: true, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.h.IndexOf(u.name, u.wide)) + idx, ok := u.h.IndexOf(u.name, u.wide) + assert.Equal(t, u.ok, ok) + assert.Equal(t, u.e, idx) }) } } func TestHeaderCustomize(t *testing.T) { uu := map[string]struct { - h render.Header + h model1.Header cols []string wide bool - e render.Header + e model1.Header }{ "default": { h: makeHeader(), @@ -98,58 +102,58 @@ func TestHeaderCustomize(t *testing.T) { e: makeHeader(), }, "reverse": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, cols: []string{"C", "A"}, - e: render.Header{ - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "A"}, + e: model1.Header{ + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "A"}, }, }, "reverse-wide": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, cols: []string{"C", "A"}, wide: true, - e: render.Header{ - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, + e: model1.Header{ + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, }, }, "toggle-wide": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, cols: []string{"C", "B"}, wide: true, - e: render.Header{ - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "B", Wide: false}, - render.HeaderColumn{Name: "A", Wide: true}, + e: model1.Header{ + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "B", Wide: false}, + model1.HeaderColumn{Name: "A", Wide: true}, }, }, "missing": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, cols: []string{"BLEE", "A"}, wide: true, - e: render.Header{ - render.HeaderColumn{Name: "BLEE"}, - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C", Wide: true}, + e: model1.Header{ + model1.HeaderColumn{Name: "BLEE"}, + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C", Wide: true}, }, }, } @@ -164,7 +168,7 @@ func TestHeaderCustomize(t *testing.T) { func TestHeaderDiff(t *testing.T) { uu := map[string]struct { - h1, h2 render.Header + h1, h2 model1.Header e bool }{ "same": { @@ -177,37 +181,37 @@ func TestHeaderDiff(t *testing.T) { e: true, }, "differ-wide": { - h1: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h1: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, - h2: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + h2: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, e: true, }, "differ-order": { - h1: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h1: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, - h2: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "B", Wide: true}, + h2: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "B", Wide: true}, }, e: true, }, "differ-name": { - h1: render.Header{ - render.HeaderColumn{Name: "A"}, + h1: model1.Header{ + model1.HeaderColumn{Name: "A"}, }, - h2: render.Header{ - render.HeaderColumn{Name: "B"}, + h2: model1.Header{ + model1.HeaderColumn{Name: "B"}, }, e: true, }, @@ -223,17 +227,17 @@ func TestHeaderDiff(t *testing.T) { func TestHeaderHasAge(t *testing.T) { uu := map[string]struct { - h render.Header + h model1.Header age, e bool }{ "no-age": { - h: render.Header{}, + h: model1.Header{}, }, "age": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "AGE", Time: true}, + h: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, }, e: true, age: true, @@ -249,41 +253,14 @@ func TestHeaderHasAge(t *testing.T) { } } -func TestHeaderValidColIndex(t *testing.T) { - uu := map[string]struct { - h render.Header - e int - }{ - "none": { - h: render.Header{}, - e: -1, - }, - "valid": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "VALID", Wide: true}, - }, - e: 2, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.h.ValidColIndex()) - }) - } -} - func TestHeaderColumns(t *testing.T) { uu := map[string]struct { - h render.Header + h model1.Header wide bool e []string }{ "empty": { - h: render.Header{}, + h: model1.Header{}, }, "regular": { h: makeHeader(), @@ -299,17 +276,17 @@ func TestHeaderColumns(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.h.Columns(u.wide)) + assert.Equal(t, u.e, u.h.ColumnNames(u.wide)) }) } } func TestHeaderClone(t *testing.T) { uu := map[string]struct { - h render.Header + h model1.Header }{ "empty": { - h: render.Header{}, + h: model1.Header{}, }, "full": { h: makeHeader(), @@ -332,10 +309,10 @@ func TestHeaderClone(t *testing.T) { // ---------------------------------------------------------------------------- // Helpers... -func makeHeader() render.Header { - return render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, +func makeHeader() model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, } } diff --git a/internal/model1/helpers.go b/internal/model1/helpers.go new file mode 100644 index 0000000000..fb0ac597a7 --- /dev/null +++ b/internal/model1/helpers.go @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import ( + "fmt" + "math" + "sort" + "strings" + + "github.com/fvbommel/sortorder" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func Hydrate(ns string, oo []runtime.Object, rr Rows, re Renderer) error { + for i, o := range oo { + if err := re.Render(o, ns, &rr[i]); err != nil { + return err + } + } + + return nil +} + +func GenericHydrate(ns string, table *metav1.Table, rr Rows, re Renderer) error { + gr, ok := re.(Generic) + if !ok { + return fmt.Errorf("expecting generic renderer but got %T", re) + } + gr.SetTable(ns, table) + for i, row := range table.Rows { + if err := gr.Render(row, ns, &rr[i]); err != nil { + return err + } + } + + return nil +} + +// IsValid returns true if resource is valid, false otherwise. +func IsValid(ns string, h Header, r Row) bool { + if len(r.Fields) == 0 { + return true + } + idx, ok := h.IndexOf("VALID", true) + if !ok || idx >= len(r.Fields) { + return true + } + + return strings.TrimSpace(r.Fields[idx]) == "" +} + +func sortLabels(m map[string]string) (keys, vals []string) { + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + vals = append(vals, m[k]) + } + + return +} + +// Converts labels string to map. +func labelize(labels string) map[string]string { + ll := strings.Split(labels, ",") + data := make(map[string]string, len(ll)) + for _, l := range ll { + tokens := strings.Split(l, "=") + if len(tokens) == 2 { + data[tokens[0]] = tokens[1] + } + } + + return data +} + +func durationToSeconds(duration string) int64 { + if len(duration) == 0 { + return 0 + } + if duration == NAValue { + return math.MaxInt64 + } + + num := make([]rune, 0, 5) + var n, m int64 + for _, r := range duration { + switch r { + case 'y': + m = 365 * 24 * 60 * 60 + case 'd': + m = 24 * 60 * 60 + case 'h': + m = 60 * 60 + case 'm': + m = 60 + case 's': + m = 1 + default: + num = append(num, r) + continue + } + n, num = n+runesToNum(num)*m, num[:0] + } + + return n +} + +func runesToNum(rr []rune) int64 { + var r int64 + var m int64 = 1 + for i := len(rr) - 1; i >= 0; i-- { + v := int64(rr[i] - '0') + r += v * m + m *= 10 + } + + return r +} + +func capacityToNumber(capacity string) int64 { + quantity := resource.MustParse(capacity) + return quantity.Value() +} + +// Less return true if c1 <= c2. +func Less(isNumber, isDuration, isCapacity bool, id1, id2, v1, v2 string) bool { + var less bool + switch { + case isNumber: + less = lessNumber(v1, v2) + case isDuration: + less = lessDuration(v1, v2) + case isCapacity: + less = lessCapacity(v1, v2) + default: + less = sortorder.NaturalLess(v1, v2) + } + if v1 == v2 { + return sortorder.NaturalLess(id1, id2) + } + + return less +} + +func lessDuration(s1, s2 string) bool { + d1, d2 := durationToSeconds(s1), durationToSeconds(s2) + return d1 <= d2 +} + +func lessCapacity(s1, s2 string) bool { + c1, c2 := capacityToNumber(s1), capacityToNumber(s2) + + return c1 <= c2 +} + +func lessNumber(s1, s2 string) bool { + v1, v2 := strings.Replace(s1, ",", "", -1), strings.Replace(s2, ",", "", -1) + + return sortorder.NaturalLess(v1, v2) +} diff --git a/internal/model1/helpers_test.go b/internal/model1/helpers_test.go new file mode 100644 index 0000000000..3e6a04272f --- /dev/null +++ b/internal/model1/helpers_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSortLabels(t *testing.T) { + uu := map[string]struct { + labels string + e [][]string + }{ + "simple": { + labels: "a=b,c=d", + e: [][]string{ + {"a", "c"}, + {"b", "d"}, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + hh, vv := sortLabels(labelize(u.labels)) + assert.Equal(t, u.e[0], hh) + assert.Equal(t, u.e[1], vv) + }) + } +} + +func TestLabelize(t *testing.T) { + uu := map[string]struct { + labels string + e map[string]string + }{ + "simple": { + labels: "a=b,c=d", + e: map[string]string{"a": "b", "c": "d"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, labelize(u.labels)) + }) + } +} + +func TestDurationToSecond(t *testing.T) { + uu := map[string]struct { + s string + e int64 + }{ + "seconds": {s: "22s", e: 22}, + "minutes": {s: "22m", e: 1320}, + "hours": {s: "12h", e: 43200}, + "days": {s: "3d", e: 259200}, + "day_hour": {s: "3d9h", e: 291600}, + "day_hour_minute": {s: "2d22h3m", e: 252180}, + "day_hour_minute_seconds": {s: "2d22h3m50s", e: 252230}, + "year": {s: "3y", e: 94608000}, + "year_day": {s: "1y2d", e: 31708800}, + "n/a": {s: NAValue, e: math.MaxInt64}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, durationToSeconds(u.s)) + }) + } +} + +func BenchmarkDurationToSecond(b *testing.B) { + t := "2d22h3m50s" + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + durationToSeconds(t) + } +} diff --git a/internal/model1/row.go b/internal/model1/row.go new file mode 100644 index 0000000000..bda6a86065 --- /dev/null +++ b/internal/model1/row.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +// Row represents a collection of columns. +type Row struct { + ID string + Fields Fields +} + +// NewRow returns a new row with initialized fields. +func NewRow(size int) Row { + return Row{Fields: make([]string, size)} +} + +// Labelize returns a new row based on labels. +func (r Row) Labelize(cols []int, labelCol int, labels []string) Row { + out := NewRow(len(cols) + len(labels)) + for _, col := range cols { + out.Fields = append(out.Fields, r.Fields[col]) + } + m := labelize(r.Fields[labelCol]) + for _, label := range labels { + out.Fields = append(out.Fields, m[label]) + } + + return out +} + +// Customize returns a row subset based on given col indices. +func (r Row) Customize(cols []int) Row { + out := NewRow(len(cols)) + r.Fields.Customize(cols, out.Fields) + out.ID = r.ID + + return out +} + +// Diff returns true if row differ or false otherwise. +func (r Row) Diff(ro Row, ageCol int) bool { + if r.ID != ro.ID { + return true + } + return r.Fields.Diff(ro.Fields, ageCol) +} + +// Clone copies a row. +func (r Row) Clone() Row { + return Row{ + ID: r.ID, + Fields: r.Fields.Clone(), + } +} + +// Len returns the length of the row. +func (r Row) Len() int { + return len(r.Fields) +} + +// ---------------------------------------------------------------------------- + +// RowSorter sorts rows. +type RowSorter struct { + Rows Rows + Index int + IsNumber bool + IsDuration bool + IsCapacity bool + Asc bool +} + +func (s RowSorter) Len() int { + return len(s.Rows) +} + +func (s RowSorter) Swap(i, j int) { + s.Rows[i], s.Rows[j] = s.Rows[j], s.Rows[i] +} + +func (s RowSorter) Less(i, j int) bool { + v1, v2 := s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index] + id1, id2 := s.Rows[i].ID, s.Rows[j].ID + less := Less(s.IsNumber, s.IsDuration, s.IsCapacity, id1, id2, v1, v2) + if s.Asc { + return less + } + return !less +} + +// ---------------------------------------------------------------------------- +// Helpers... diff --git a/internal/render/row_event.go b/internal/model1/row_event.go similarity index 54% rename from internal/render/row_event.go rename to internal/model1/row_event.go index 7248371db0..628ddab057 100644 --- a/internal/render/row_event.go +++ b/internal/model1/row_event.go @@ -1,28 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s -package render +package model1 import ( + "fmt" "sort" ) -const ( - // EventUnchanged notifies listener resource has not changed. - EventUnchanged ResEvent = 1 << iota - - // EventAdd notifies listener of a resource was added. - EventAdd - - // EventUpdate notifies listener of a resource updated. - EventUpdate - - // EventDelete notifies listener of a resource was deleted. - EventDelete - - // EventClear the stack was reset. - EventClear -) +type ReRangeFn func(int, RowEvent) bool // ResEvent represents a resource event. type ResEvent int @@ -103,13 +89,58 @@ func (r RowEvent) Diff(re RowEvent, ageCol int) bool { // ---------------------------------------------------------------------------- +type reIndex map[string]int + // RowEvents a collection of row events. -type RowEvents []RowEvent +type RowEvents struct { + events []RowEvent + index reIndex +} + +func NewRowEvents(size int) *RowEvents { + return &RowEvents{ + events: make([]RowEvent, 0, size), + index: make(reIndex, size), + } +} + +func NewRowEventsWithEvts(ee ...RowEvent) *RowEvents { + re := NewRowEvents(len(ee)) + for _, e := range ee { + re.Add(e) + } + + return re +} + +func (r *RowEvents) reindex() { + for i, e := range r.events { + r.index[e.Row.ID] = i + } +} + +func (r *RowEvents) At(i int) (RowEvent, bool) { + if i < 0 || i > len(r.events) { + return RowEvent{}, false + } + + return r.events[i], true +} + +func (r *RowEvents) Set(i int, re RowEvent) { + r.events[i] = re + r.index[re.Row.ID] = i +} + +func (r *RowEvents) Add(re RowEvent) { + r.events = append(r.events, re) + r.index[re.Row.ID] = len(r.events) - 1 +} // ExtractHeaderLabels extract header labels. -func (r RowEvents) ExtractHeaderLabels(labelCol int) []string { +func (r *RowEvents) ExtractHeaderLabels(labelCol int) []string { ll := make([]string, 0, 10) - for _, re := range r { + for _, re := range r.events { ll = append(ll, re.ExtractHeaderLabels(labelCol)...) } @@ -117,32 +148,32 @@ func (r RowEvents) ExtractHeaderLabels(labelCol int) []string { } // Labelize converts labels into a row event. -func (r RowEvents) Labelize(cols []int, labelCol int, labels []string) RowEvents { - out := make(RowEvents, 0, len(r)) - for _, re := range r { +func (r *RowEvents) Labelize(cols []int, labelCol int, labels []string) *RowEvents { + out := make([]RowEvent, 0, len(r.events)) + for _, re := range r.events { out = append(out, re.Labelize(cols, labelCol, labels)) } - return out + return NewRowEventsWithEvts(out...) } // Customize returns custom row events based on columns layout. -func (r RowEvents) Customize(cols []int) RowEvents { - ee := make(RowEvents, 0, len(cols)) - for _, re := range r { +func (r *RowEvents) Customize(cols []int) *RowEvents { + ee := make([]RowEvent, 0, len(cols)) + for _, re := range r.events { ee = append(ee, re.Customize(cols)) } - return ee + + return NewRowEventsWithEvts(ee...) } // Diff returns true if the event changed. -func (r RowEvents) Diff(re RowEvents, ageCol int) bool { - if len(r) != len(re) { +func (r *RowEvents) Diff(re *RowEvents, ageCol int) bool { + if len(r.events) != len(re.events) { return true } - - for i := range r { - if r[i].Diff(re[i], ageCol) { + for i := range r.events { + if r.events[i].Diff(re.events[i], ageCol) { return true } } @@ -150,53 +181,80 @@ func (r RowEvents) Diff(re RowEvents, ageCol int) bool { return false } -// Clone returns a rowevents deep copy. -func (r RowEvents) Clone() RowEvents { - res := make(RowEvents, len(r)) - for i, re := range r { - res[i] = re.Clone() +// Clone returns a deep copy. +func (r *RowEvents) Clone() *RowEvents { + re := make([]RowEvent, 0, len(r.events)) + for _, e := range r.events { + re = append(re, e.Clone()) } - return res + return NewRowEventsWithEvts(re...) } // Upsert add or update a row if it exists. -func (r RowEvents) Upsert(re RowEvent) RowEvents { +func (r *RowEvents) Upsert(re RowEvent) { if idx, ok := r.FindIndex(re.Row.ID); ok { - r[idx] = re + r.events[idx] = re } else { - r = append(r, re) + r.Add(re) } - return r } // Delete removes an element by id. -func (r RowEvents) Delete(id string) RowEvents { - victim, ok := r.FindIndex(id) +func (r *RowEvents) Delete(fqn string) error { + victim, ok := r.FindIndex(fqn) if !ok { - return r + return fmt.Errorf("unable to delete row with fqn: %q", fqn) } - return append(r[0:victim], r[victim+1:]...) + r.events = append(r.events[0:victim], r.events[victim+1:]...) + delete(r.index, fqn) + r.reindex() + + return nil +} + +func (r *RowEvents) Len() int { + return len(r.events) +} + +func (r *RowEvents) Empty() bool { + return len(r.events) == 0 } // Clear delete all row events. -func (r RowEvents) Clear() RowEvents { - return RowEvents{} +func (r *RowEvents) Clear() { + r.events = r.events[:0] + for k := range r.index { + delete(r.index, k) + } } -// 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) 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) { + i, ok := r.index[id] - return 0, false + return i, ok } // Sort rows based on column index and order. -func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, asc bool) { +func (r *RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, asc bool) { if sortCol == -1 { return } @@ -211,13 +269,14 @@ func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, IsCapacity: isCapacity, } sort.Sort(t) + r.reindex() } // ---------------------------------------------------------------------------- // RowEventSorter sorts row events by a given colon. type RowEventSorter struct { - Events RowEvents + Events *RowEvents Index int NS string IsNumber bool @@ -227,16 +286,16 @@ type RowEventSorter struct { } func (r RowEventSorter) Len() int { - return len(r.Events) + return len(r.Events.events) } func (r RowEventSorter) Swap(i, j int) { - r.Events[i], r.Events[j] = r.Events[j], r.Events[i] + r.Events.events[i], r.Events.events[j] = r.Events.events[j], r.Events.events[i] } func (r RowEventSorter) Less(i, j int) bool { - f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields - id1, id2 := r.Events[i].Row.ID, r.Events[j].Row.ID + f1, f2 := r.Events.events[i].Row.Fields, r.Events.events[j].Row.Fields + id1, id2 := r.Events.events[i].Row.ID, r.Events.events[j].Row.ID less := Less(r.IsNumber, r.IsDuration, r.IsCapacity, id1, id2, f1[r.Index], f2[r.Index]) if r.Asc { return less diff --git a/internal/model1/row_event_test.go b/internal/model1/row_event_test.go new file mode 100644 index 0000000000..9ca9322980 --- /dev/null +++ b/internal/model1/row_event_test.go @@ -0,0 +1,543 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1_test + +import ( + "testing" + "time" + + "github.com/derailed/k9s/internal/model1" + "github.com/stretchr/testify/assert" +) + +func TestRowEventCustomize(t *testing.T) { + uu := map[string]struct { + re1, e model1.RowEvent + cols []int + }{ + "empty": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{}}, + }, + }, + "full": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + cols: []int{0, 1, 2}, + }, + "deltas": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + Deltas: model1.DeltaRow{"a", "b", "c"}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + Deltas: model1.DeltaRow{"a", "b", "c"}, + }, + cols: []int{0, 1, 2}, + }, + "deltas-skip": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + Deltas: model1.DeltaRow{"a", "b", "c"}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "1"}}, + Deltas: model1.DeltaRow{"c", "a"}, + }, + cols: []int{2, 0}, + }, + "reverse": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "2", "1"}}, + }, + cols: []int{2, 1, 0}, + }, + "skip": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "1"}}, + }, + cols: []int{2, 0}, + }, + "miss": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "", "1"}}, + }, + cols: []int{2, 10, 0}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.re1.Customize(u.cols)) + }) + } +} + +func TestRowEventDiff(t *testing.T) { + uu := map[string]struct { + re1, re2 model1.RowEvent + e bool + }{ + "same": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + re2: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + }, + "diff-kind": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + re2: model1.RowEvent{ + Kind: model1.EventDelete, + Row: model1.Row{ID: "B", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: true, + }, + "diff-delta": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + Deltas: model1.DeltaRow{"1", "2", "3"}, + }, + re2: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + Deltas: model1.DeltaRow{"10", "2", "3"}, + }, + e: true, + }, + "diff-id": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + re2: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "B", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: true, + }, + "diff-field": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + re2: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"10", "2", "3"}}, + }, + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.re1.Diff(u.re2, -1)) + }) + } +} + +func TestRowEventsDiff(t *testing.T) { + uu := map[string]struct { + re1, re2 *model1.RowEvents + ageCol int + e bool + }{ + "same": { + re1: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re2: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + ageCol: -1, + }, + "diff-len": { + re1: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re2: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + ageCol: -1, + e: true, + }, + "diff-id": { + re1: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re2: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "D", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + ageCol: -1, + e: true, + }, + "diff-order": { + re1: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re2: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + ageCol: -1, + e: true, + }, + "diff-withAge": { + re1: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re2: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "13"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + ageCol: 1, + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.re1.Diff(u.re2, u.ageCol)) + }) + } +} + +func TestRowEventsUpsert(t *testing.T) { + uu := map[string]struct { + ee, e *model1.RowEvents + re model1.RowEvent + }{ + "add": { + ee: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re: model1.RowEvent{ + Row: model1.Row{ID: "D", Fields: model1.Fields{"f1", "f2", "f3"}}, + }, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "D", Fields: model1.Fields{"f1", "f2", "f3"}}}, + ), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.ee.Upsert(u.re) + assert.Equal(t, u.e, u.ee) + }) + } +} + +func TestRowEventsCustomize(t *testing.T) { + uu := map[string]struct { + re, e *model1.RowEvents + cols []int + }{ + "same": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + cols: []int{0, 1, 2}, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + }, + "reverse": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + cols: []int{2, 1, 0}, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "2", "1"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"3", "2", "0"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"3", "2", "10"}}}, + ), + }, + "skip": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + cols: []int{1, 0}, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"2", "1"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"2", "0"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"2", "10"}}}, + ), + }, + "missing": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + cols: []int{1, 0, 4}, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"2", "1", ""}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"2", "0", ""}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"2", "10", ""}}}, + ), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.re.Customize(u.cols)) + }) + } +} + +func TestRowEventsDelete(t *testing.T) { + uu := map[string]struct { + re, e *model1.RowEvents + id string + }{ + "first": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + id: "A", + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + }, + "middle": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + id: "B", + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + }, + "last": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + id: "C", + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + ), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.NoError(t, u.re.Delete(u.id)) + assert.Equal(t, u.e, u.re) + }) + } +} + +func TestRowEventsSort(t *testing.T) { + uu := map[string]struct { + re, e *model1.RowEvents + col int + duration, num, asc bool + capacity bool + }{ + "age_time": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", testTime().String()}}}, + ), + col: 2, + asc: true, + duration: true, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", testTime().String()}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}}, + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}}, + ), + }, + "col0": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + col: 0, + asc: true, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + }, + "id_preserve": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3"}}}, + ), + col: 1, + asc: true, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3"}}}, + ), + }, + "capacity": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3", "1Gi"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3", "1.1G"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3", "0.5Ti"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3", "12e6"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3", "1234"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3", "0.1Ei"}}}, + ), + col: 3, + asc: true, + capacity: true, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3", "1234"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3", "12e6"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3", "1Gi"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3", "1.1G"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3", "0.5Ti"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3", "0.1Ei"}}}, + ), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.re.Sort("", u.col, u.duration, u.num, u.capacity, u.asc) + assert.Equal(t, u.e, u.re) + }) + } +} + +func TestRowEventsClone(t *testing.T) { + uu := map[string]struct { + r *model1.RowEvents + }{ + "empty": { + r: model1.NewRowEventsWithEvts(), + }, + "full": { + r: makeRowEvents(), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c := u.r.Clone() + assert.Equal(t, u.r.Len(), c.Len()) + if !u.r.Empty() { + r, ok := u.r.At(0) + assert.True(t, ok) + r.Row.Fields[0] = "blee" + cr, ok := c.At(0) + assert.True(t, ok) + assert.Equal(t, "A", cr.Row.Fields[0]) + } + }) + } +} + +// Helpers... + +func makeRowEvents() *model1.RowEvents { + return model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3"}}}, + ) +} diff --git a/internal/render/row_test.go b/internal/model1/row_test.go similarity index 78% rename from internal/render/row_test.go rename to internal/model1/row_test.go index f1d5a5bc0a..d206083165 100644 --- a/internal/render/row_test.go +++ b/internal/model1/row_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of K9s -package render_test +package model1_test import ( "fmt" @@ -9,12 +9,12 @@ import ( "testing" "time" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/stretchr/testify/assert" ) func BenchmarkRowCustomize(b *testing.B) { - row := render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}} + row := model1.Row{ID: "fred", Fields: model1.Fields{"f1", "f2", "f3"}} cols := []int{0, 1, 2} b.ReportAllocs() b.ResetTimer() @@ -25,36 +25,36 @@ func BenchmarkRowCustomize(b *testing.B) { func TestFieldCustomize(t *testing.T) { uu := map[string]struct { - fields render.Fields + fields model1.Fields cols []int - e render.Fields + e model1.Fields }{ "empty": { - fields: render.Fields{}, + fields: model1.Fields{}, cols: []int{0, 1, 2}, - e: render.Fields{"", "", ""}, + e: model1.Fields{"", "", ""}, }, "no-cols": { - fields: render.Fields{"f1", "f2", "f3"}, + fields: model1.Fields{"f1", "f2", "f3"}, cols: []int{}, - e: render.Fields{}, + e: model1.Fields{}, }, "reverse": { - fields: render.Fields{"f1", "f2", "f3"}, + fields: model1.Fields{"f1", "f2", "f3"}, cols: []int{1, 0}, - e: render.Fields{"f2", "f1"}, + e: model1.Fields{"f2", "f1"}, }, "missing": { - fields: render.Fields{"f1", "f2", "f3"}, + fields: model1.Fields{"f1", "f2", "f3"}, cols: []int{10, 0}, - e: render.Fields{"", "f1"}, + e: model1.Fields{"", "f1"}, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - ff := make(render.Fields, len(u.cols)) + ff := make(model1.Fields, len(u.cols)) u.fields.Customize(u.cols, ff) assert.Equal(t, u.e, ff) }) @@ -62,7 +62,7 @@ func TestFieldCustomize(t *testing.T) { } func TestFieldClone(t *testing.T) { - f := render.Fields{"a", "b", "c"} + f := model1.Fields{"a", "b", "c"} f1 := f.Clone() assert.True(t, reflect.DeepEqual(f, f1)) @@ -71,24 +71,24 @@ func TestFieldClone(t *testing.T) { func TestRowlabelize(t *testing.T) { uu := map[string]struct { - row render.Row + row model1.Row cols []int - e render.Row + e model1.Row }{ "empty": { - row: render.Row{}, + row: model1.Row{}, cols: []int{0, 1, 2}, - e: render.Row{ID: "", Fields: render.Fields{"", "", ""}}, + e: model1.Row{ID: "", Fields: model1.Fields{"", "", ""}}, }, "no-cols-no-data": { - row: render.Row{}, + row: model1.Row{}, cols: []int{}, - e: render.Row{ID: "", Fields: render.Fields{}}, + e: model1.Row{ID: "", Fields: model1.Fields{}}, }, "no-cols-data": { - row: render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}}, + row: model1.Row{ID: "fred", Fields: model1.Fields{"f1", "f2", "f3"}}, cols: []int{}, - e: render.Row{ID: "fred", Fields: render.Fields{}}, + e: model1.Row{ID: "fred", Fields: model1.Fields{}}, }, } @@ -103,24 +103,24 @@ func TestRowlabelize(t *testing.T) { func TestRowCustomize(t *testing.T) { uu := map[string]struct { - row render.Row + row model1.Row cols []int - e render.Row + e model1.Row }{ "empty": { - row: render.Row{}, + row: model1.Row{}, cols: []int{0, 1, 2}, - e: render.Row{ID: "", Fields: render.Fields{"", "", ""}}, + e: model1.Row{ID: "", Fields: model1.Fields{"", "", ""}}, }, "no-cols-no-data": { - row: render.Row{}, + row: model1.Row{}, cols: []int{}, - e: render.Row{ID: "", Fields: render.Fields{}}, + e: model1.Row{ID: "", Fields: model1.Fields{}}, }, "no-cols-data": { - row: render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}}, + row: model1.Row{ID: "fred", Fields: model1.Fields{"f1", "f2", "f3"}}, cols: []int{}, - e: render.Row{ID: "fred", Fields: render.Fields{}}, + e: model1.Row{ID: "fred", Fields: model1.Fields{}}, }, } @@ -135,49 +135,49 @@ func TestRowCustomize(t *testing.T) { func TestRowsDelete(t *testing.T) { uu := map[string]struct { - rows render.Rows + rows model1.Rows id string - e render.Rows + e model1.Rows }{ "first": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, id: "a", - e: render.Rows{ + e: model1.Rows{ {ID: "b", Fields: []string{"albert", "blee"}}, }, }, "last": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, id: "b", - e: render.Rows{ + e: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, }, }, "middle": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, {ID: "c", Fields: []string{"fred", "zorg"}}, }, id: "b", - e: render.Rows{ + e: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "c", Fields: []string{"fred", "zorg"}}, }, }, "missing": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, id: "zorg", - e: render.Rows{ + e: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, @@ -195,29 +195,29 @@ func TestRowsDelete(t *testing.T) { func TestRowsUpsert(t *testing.T) { uu := map[string]struct { - rows render.Rows - row render.Row - e render.Rows + rows model1.Rows + row model1.Row + e model1.Rows }{ "add": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, - row: render.Row{ID: "c", Fields: []string{"f1", "f2"}}, - e: render.Rows{ + row: model1.Row{ID: "c", Fields: []string{"f1", "f2"}}, + e: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, {ID: "c", Fields: []string{"f1", "f2"}}, }, }, "update": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, - row: render.Row{ID: "a", Fields: []string{"f1", "f2"}}, - e: render.Rows{ + row: model1.Row{ID: "a", Fields: []string{"f1", "f2"}}, + e: model1.Rows{ {ID: "a", Fields: []string{"f1", "f2"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, @@ -235,69 +235,69 @@ func TestRowsUpsert(t *testing.T) { func TestRowsSortText(t *testing.T) { uu := map[string]struct { - rows render.Rows + rows model1.Rows col int asc, num bool - e render.Rows + e model1.Rows }{ "plainAsc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"blee", "duh"}}, {Fields: []string{"albert", "blee"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"albert", "blee"}}, {Fields: []string{"blee", "duh"}}, }, }, "plainDesc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"blee", "duh"}}, {Fields: []string{"albert", "blee"}}, }, col: 0, asc: false, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"blee", "duh"}}, {Fields: []string{"albert", "blee"}}, }, }, "numericAsc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10", "duh"}}, {Fields: []string{"1", "blee"}}, }, col: 0, num: true, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"1", "blee"}}, {Fields: []string{"10", "duh"}}, }, }, "numericDesc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10", "duh"}}, {Fields: []string{"1", "blee"}}, }, col: 0, num: true, asc: false, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"10", "duh"}}, {Fields: []string{"1", "blee"}}, }, }, "composite": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"blee-duh", "duh"}}, {Fields: []string{"blee", "blee"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"blee", "blee"}}, {Fields: []string{"blee-duh", "duh"}}, }, @@ -315,54 +315,54 @@ func TestRowsSortText(t *testing.T) { func TestRowsSortDuration(t *testing.T) { uu := map[string]struct { - rows render.Rows + rows model1.Rows col int asc bool - e render.Rows + e model1.Rows }{ "fred": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"2m24s", "blee"}}, {Fields: []string{"2m12s", "duh"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"2m12s", "duh"}}, {Fields: []string{"2m24s", "blee"}}, }, }, "years": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{testTime().Add(-365 * 24 * time.Hour).String(), "blee"}}, {Fields: []string{testTime().String(), "duh"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{testTime().String(), "duh"}}, {Fields: []string{testTime().Add(-365 * 24 * time.Hour).String(), "blee"}}, }, }, "durationAsc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}}, {Fields: []string{testTime().String(), "blee"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{testTime().String(), "blee"}}, {Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}}, }, }, "durationDesc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}}, {Fields: []string{testTime().String(), "blee"}}, }, col: 0, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}}, {Fields: []string{testTime().String(), "blee"}}, }, @@ -380,31 +380,31 @@ func TestRowsSortDuration(t *testing.T) { func TestRowsSortMetrics(t *testing.T) { uu := map[string]struct { - rows render.Rows + rows model1.Rows col int asc bool - e render.Rows + e model1.Rows }{ "metricAsc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10m", "duh"}}, {Fields: []string{"1m", "blee"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"1m", "blee"}}, {Fields: []string{"10m", "duh"}}, }, }, "metricDesc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10000m", "1000Mi"}}, {Fields: []string{"1m", "50Mi"}}, }, col: 1, asc: false, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"10000m", "1000Mi"}}, {Fields: []string{"1m", "50Mi"}}, }, @@ -422,31 +422,31 @@ func TestRowsSortMetrics(t *testing.T) { func TestRowsSortCapacity(t *testing.T) { uu := map[string]struct { - rows render.Rows + rows model1.Rows col int asc bool - e render.Rows + e model1.Rows }{ "capacityAsc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10Gi", "duh"}}, {Fields: []string{"10G", "blee"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"10G", "blee"}}, {Fields: []string{"10Gi", "duh"}}, }, }, "capacityDesc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10000m", "1000Mi"}}, {Fields: []string{"1m", "50Mi"}}, }, col: 1, asc: false, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"10000m", "1000Mi"}}, {Fields: []string{"1m", "50Mi"}}, }, @@ -514,7 +514,7 @@ func TestLess(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, render.Less(u.isNumber, u.isDuration, u.isCapacity, u.id1, u.id2, u.v1, u.v2)) + assert.Equal(t, u.e, model1.Less(u.isNumber, u.isDuration, u.isCapacity, u.id1, u.id2, u.v1, u.v2)) }) } } diff --git a/internal/model1/rows.go b/internal/model1/rows.go new file mode 100644 index 0000000000..4085cb9b08 --- /dev/null +++ b/internal/model1/rows.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import "sort" + +// Rows represents a collection of rows. +type Rows []Row + +// Delete removes an element by id. +func (rr Rows) Delete(id string) Rows { + idx, ok := rr.Find(id) + if !ok { + return rr + } + + if idx == 0 { + return rr[1:] + } + if idx+1 == len(rr) { + return rr[:len(rr)-1] + } + + return append(rr[:idx], rr[idx+1:]...) +} + +// Upsert adds a new item. +func (rr Rows) Upsert(r Row) Rows { + idx, ok := rr.Find(r.ID) + if !ok { + return append(rr, r) + } + rr[idx] = r + + return rr +} + +// Find locates a row by id. Returns false is not found. +func (rr Rows) Find(id string) (int, bool) { + for i, r := range rr { + if r.ID == id { + return i, true + } + } + + return 0, false +} + +// Sort rows based on column index and order. +func (rr Rows) Sort(col int, asc, isNum, isDur, isCapacity bool) { + t := RowSorter{ + Rows: rr, + Index: col, + IsNumber: isNum, + IsDuration: isDur, + IsCapacity: isCapacity, + Asc: asc, + } + sort.Sort(t) +} diff --git a/internal/model1/table_data.go b/internal/model1/table_data.go new file mode 100644 index 0000000000..2b8cd096fe --- /dev/null +++ b/internal/model1/table_data.go @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + "sync" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/rs/zerolog/log" + "github.com/sahilm/fuzzy" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type ( + // SortFn represent a function that can sort columnar data. + SortFn func(rows Rows, sortCol SortColumn) + + // SortColumn represents a sortable column. + SortColumn struct { + Name string + ASC bool + } +) + +const spacer = " " + +type FilterOpts struct { + Toast bool + Filter string + Invert bool +} + +// TableData tracks a K8s resource for tabular display. +type TableData struct { + header Header + rowEvents *RowEvents + namespace string + gvr client.GVR + mx sync.RWMutex +} + +// NewTableData returns a new table. +func NewTableData(gvr client.GVR) *TableData { + return &TableData{ + gvr: gvr, + rowEvents: NewRowEvents(10), + } +} + +func NewTableDataFull(gvr client.GVR, ns string, h Header, re *RowEvents) *TableData { + t := NewTableDataWithRows(gvr, h, re) + t.namespace = ns + + return t +} + +func NewTableDataWithRows(gvr client.GVR, h Header, re *RowEvents) *TableData { + t := NewTableData(gvr) + t.header, t.rowEvents = h, re + + return t +} + +func NewTableDataFromTable(td *TableData) *TableData { + t := NewTableData(td.gvr) + t.header = td.header + t.rowEvents = td.rowEvents + t.namespace = td.namespace + + return t +} + +func (t *TableData) AddRow(re RowEvent) { + t.rowEvents.Add(re) +} + +func (t *TableData) SetRow(idx int, re RowEvent) { + t.rowEvents.Set(idx, re) +} + +func (t *TableData) FindRow(id string) (RowEvent, bool) { + return t.rowEvents.Get(id) +} + +func (t *TableData) RowAt(idx int) (RowEvent, bool) { + return t.rowEvents.At(idx) +} + +func (t *TableData) RowsRange(f ReRangeFn) { + t.rowEvents.Range(f) +} + +func (t *TableData) Sort(sc SortColumn) { + col, idx := t.HeadCol(sc.Name, false) + if idx < 0 { + return + } + t.rowEvents.Sort( + t.GetNamespace(), + idx, + col.Time, + col.MX, + col.Capacity, + sc.ASC, + ) +} + +func (t *TableData) Header() Header { + return t.header +} + +// HeaderCount returns the number of header cols. +func (t *TableData) HeaderCount() int { + t.mx.RLock() + defer t.mx.RUnlock() + + return len(t.header) +} + +func (t *TableData) HeadCol(n string, w bool) (HeaderColumn, int) { + idx, ok := t.header.IndexOf(n, w) + if !ok { + return HeaderColumn{}, -1 + } + + return t.header[idx], idx +} + +func (t *TableData) Filter(f FilterOpts) *TableData { + td := NewTableDataFromTable(t) + + if f.Toast { + td.rowEvents = t.filterToast() + } + if f.Filter == "" || internal.IsLabelSelector(f.Filter) { + return td + } + if f, ok := internal.IsFuzzySelector(f.Filter); ok { + td.rowEvents = t.fuzzyFilter(f) + return td + } + rr, err := t.rxFilter(f.Filter, internal.IsInverseSelector(f.Filter)) + if err == nil { + td.rowEvents = rr + } else { + log.Error().Err(err).Msg("rx filter failed") + } + + return td +} + +func (t *TableData) rxFilter(q string, inverse bool) (*RowEvents, error) { + if inverse { + q = q[1:] + } + rx, err := regexp.Compile(`(?i)(` + q + `)`) + if err != nil { + return nil, fmt.Errorf("invalid rx filter %q: %w", q, err) + } + + ageIndex, ok := t.header.IndexOf("AGE", true) + + rr := NewRowEvents(t.RowCount() / 2) + t.rowEvents.Range(func(_ int, re RowEvent) bool { + ff := re.Row.Fields + if ok && ageIndex+1 <= len(ff) { + ff = append(ff[0:ageIndex], ff[ageIndex+1:]...) + } + fields := strings.Join(ff, spacer) + if (inverse && !rx.MatchString(fields)) || + ((!inverse) && rx.MatchString(fields)) { + rr.Add(re) + } + return true + }) + + return rr, nil +} + +func (t *TableData) fuzzyFilter(q string) *RowEvents { + q = strings.TrimSpace(q) + ss := make([]string, 0, t.RowCount()/2) + t.rowEvents.Range(func(_ int, re RowEvent) bool { + ss = append(ss, re.Row.ID) + return true + }) + + mm := fuzzy.Find(q, ss) + rr := NewRowEvents(t.RowCount() / 2) + for _, m := range mm { + re, ok := t.rowEvents.At(m.Index) + if !ok { + log.Error().Msgf("unable to find event for index in fuzzfilter: %d", m.Index) + continue + } + rr.Add(re) + } + + return rr +} + +func (t *TableData) filterToast() *RowEvents { + idx, ok := t.header.IndexOf("VALID", true) + if !ok { + return nil + } + + rr := NewRowEvents(10) + t.rowEvents.Range(func(_ int, re RowEvent) bool { + if re.Row.Fields[idx] != "" { + rr.Add(re) + } + return true + }) + + return rr +} + +func (t *TableData) GetNamespace() string { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.namespace +} + +func (t *TableData) Reset(ns string) { + t.mx.Lock() + { + t.namespace = ns + } + t.mx.Unlock() + + t.Clear() +} + +func (t *TableData) Reconcile(ctx context.Context, r Renderer, oo []runtime.Object) error { + var rows Rows + + if len(oo) > 0 { + if r.IsGeneric() { + table, ok := oo[0].(*metav1.Table) + if !ok { + return fmt.Errorf("expecting a meta table but got %T", oo[0]) + } + rows = make(Rows, len(table.Rows)) + if err := GenericHydrate(t.namespace, table, rows, r); err != nil { + return err + } + } else { + rows = make(Rows, len(oo)) + if err := Hydrate(t.namespace, oo, rows, r); err != nil { + return err + } + } + } + + t.Update(rows) + t.SetHeader(t.namespace, r.Header(t.namespace)) + if t.HeaderCount() == 0 { + return fmt.Errorf("fail to list resource %s", t.gvr) + } + + return nil +} + +// Empty checks if there are no entries. +func (t *TableData) Empty() bool { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.rowEvents.Empty() +} + +func (t *TableData) SetRowEvents(re *RowEvents) { + t.rowEvents = re +} + +func (t *TableData) GetRowEvents() *RowEvents { + return t.rowEvents +} + +// RowCount returns the number of rows. +func (t *TableData) RowCount() int { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.rowEvents.Len() +} + +// IndexOfHeader return the index of the header. +func (t *TableData) IndexOfHeader(h string) (int, bool) { + return t.header.IndexOf(h, false) +} + +// Labelize prints out specific label columns. +func (t *TableData) Labelize(labels []string) *TableData { + idx, ok := t.header.IndexOf("LABELS", true) + if !ok { + return t + } + cols := []int{0, 1} + if client.IsNamespaced(t.namespace) { + cols = cols[1:] + } + data := TableData{ + namespace: t.namespace, + header: t.header.Labelize(cols, idx, t.rowEvents), + } + data.rowEvents = t.rowEvents.Labelize(cols, idx, labels) + + return &data +} + +// Customize returns a new model with customized column layout. +func (t *TableData) Customize(vs *config.ViewSetting, sc SortColumn, manual, wide bool) (*TableData, SortColumn) { + if vs.IsBlank() { + if sc.Name != "" { + return t, sc + } + psc, err := t.sortCol(vs) + if err == nil { + return t, psc + } + return t, sc + } + + cols := vs.Columns + cdata := TableData{ + gvr: t.gvr, + namespace: t.namespace, + header: t.header.Customize(cols, wide), + } + ids := t.header.MapIndices(cols, wide) + cdata.rowEvents = t.rowEvents.Customize(ids) + if manual || vs == nil { + return &cdata, sc + } + psc, err := cdata.sortCol(vs) + if err != nil { + return &cdata, sc + } + + return &cdata, psc +} + +func (t *TableData) sortCol(vs *config.ViewSetting) (SortColumn, error) { + var psc SortColumn + + if t.HeaderCount() == 0 { + return psc, errors.New("no header found") + } + name, order, _ := vs.SortCol() + if _, ok := t.header.IndexOf(name, false); ok { + psc.Name, psc.ASC = name, order + return psc, nil + } + if client.IsAllNamespaces(t.GetNamespace()) { + if _, ok := t.header.IndexOf("NAMESPACE", false); ok { + psc.Name = "NAMESPACE" + } else if _, ok := t.header.IndexOf("NAME", false); ok { + psc.Name = "NAME" + } + } else { + if _, ok := t.header.IndexOf("NAME", false); ok { + psc.Name = "NAME" + } else { + psc.Name = t.header[0].Name + } + } + + return psc, nil +} + +// Clear clears out the entire table. +func (t *TableData) Clear() { + t.mx.Lock() + defer t.mx.Unlock() + + t.header = t.header.Clear() + t.rowEvents.Clear() +} + +// Clone returns a copy of the table. +func (t *TableData) Clone() *TableData { + t.mx.RLock() + defer t.mx.RUnlock() + + return &TableData{ + header: t.header.Clone(), + rowEvents: t.rowEvents.Clone(), + namespace: t.namespace, + } +} + +func (t *TableData) ColumnNames(w bool) []string { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.header.ColumnNames(w) +} + +// GetHeader returns table header. +func (t *TableData) GetHeader() Header { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.header +} + +// SetHeader sets table header. +func (t *TableData) SetHeader(ns string, h Header) { + t.mx.Lock() + defer t.mx.Unlock() + + t.namespace, t.header = ns, h +} + +// Update computes row deltas and update the table data. +func (t *TableData) Update(rows Rows) { + empty := t.Empty() + kk := make(map[string]struct{}, len(rows)) + var blankDelta DeltaRow + t.mx.Lock() + { + for _, row := range rows { + kk[row.ID] = struct{}{} + if empty { + t.rowEvents.Add(NewRowEvent(EventAdd, row)) + continue + } + if index, ok := t.rowEvents.FindIndex(row.ID); ok { + ev, ok := t.rowEvents.At(index) + if !ok { + continue + } + delta := NewDeltaRow(ev.Row, row, t.header) + if delta.IsBlank() { + ev.Kind, ev.Deltas, ev.Row = EventUnchanged, blankDelta, row + t.rowEvents.Set(index, ev) + } else { + t.rowEvents.Set(index, NewRowEventWithDeltas(row, delta)) + } + continue + } + t.rowEvents.Add(NewRowEvent(EventAdd, row)) + } + } + t.mx.Unlock() + + if !empty { + t.Delete(kk) + } +} + +// Delete removes items in cache that are no longer valid. +func (t *TableData) Delete(newKeys map[string]struct{}) { + t.mx.Lock() + { + victims := make([]string, 0, 10) + t.rowEvents.Range(func(_ int, e RowEvent) bool { + if _, ok := newKeys[e.Row.ID]; !ok { + victims = append(victims, e.Row.ID) + } else { + delete(newKeys, e.Row.ID) + } + return true + }) + for _, id := range victims { + if err := t.rowEvents.Delete(id); err != nil { + log.Error().Err(err).Msgf("table delete failed: %q", id) + } + } + } + t.mx.Unlock() +} + +// Diff checks if two tables are equal. +func (t *TableData) Diff(t2 *TableData) bool { + if t2 == nil || t.namespace != t2.namespace || t.header.Diff(t2.header) { + return true + } + idx, ok := t.header.IndexOf("AGE", true) + if !ok { + idx = -1 + } + return t.rowEvents.Diff(t2.rowEvents, idx) +} diff --git a/internal/model1/table_data_test.go b/internal/model1/table_data_test.go new file mode 100644 index 0000000000..fc338a5649 --- /dev/null +++ b/internal/model1/table_data_test.go @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestTableDataCustomize(t *testing.T) { + uu := map[string]struct { + t1, e *TableData + vs config.ViewSetting + sc SortColumn + wide, manual bool + }{ + "same": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + vs: config.ViewSetting{Columns: []string{"A", "B", "C"}}, + e: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + }, + "wide-col": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B", Wide: true}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + vs: config.ViewSetting{Columns: []string{"A", "B", "C"}}, + e: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B", Wide: false}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + }, + "wide": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B", Wide: true}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + wide: true, + vs: config.ViewSetting{Columns: []string{"A", "C"}}, + e: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "C"}, + HeaderColumn{Name: "B", Wide: true}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "3", "2"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "3", "2"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "3", "2"}}}, + ), + ), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + td, _ := u.t1.Customize(&u.vs, u.sc, u.manual, u.wide) + assert.Equal(t, u.e, td) + }) + } +} + +func TestTableDataDiff(t *testing.T) { + uu := map[string]struct { + t1, t2 *TableData + e bool + }{ + "empty": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + e: true, + }, + "same": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + t2: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + }, + "ns-diff": { + t1: NewTableDataFull( + client.NewGVR("test"), + "ns1", + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + t2: NewTableDataFull( + client.NewGVR("test"), + "ns-2", + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + e: true, + }, + "header-diff": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "D"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + t2: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + e: true, + }, + "row-diff": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + t2: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"100", "2", "3"}}}, + ), + ), + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.t1.Diff(u.t2)) + }) + } +} + +func TestTableDataUpdate(t *testing.T) { + uu := map[string]struct { + re, e *RowEvents + rr Rows + }{ + "no-change": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + rr: Rows{ + Row{ID: "A", Fields: Fields{"1", "2", "3"}}, + Row{ID: "B", Fields: Fields{"0", "2", "3"}}, + Row{ID: "C", Fields: Fields{"10", "2", "3"}}, + }, + e: NewRowEventsWithEvts( + RowEvent{Kind: EventUnchanged, Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + }, + "add": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + rr: Rows{ + Row{ID: "A", Fields: Fields{"1", "2", "3"}}, + Row{ID: "B", Fields: Fields{"0", "2", "3"}}, + Row{ID: "C", Fields: Fields{"10", "2", "3"}}, + Row{ID: "D", Fields: Fields{"10", "2", "3"}}, + }, + e: NewRowEventsWithEvts( + RowEvent{Kind: EventUnchanged, Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + RowEvent{Kind: EventAdd, Row: Row{ID: "D", Fields: Fields{"10", "2", "3"}}}, + ), + }, + "delete": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + rr: Rows{ + Row{ID: "A", Fields: Fields{"1", "2", "3"}}, + Row{ID: "C", Fields: Fields{"10", "2", "3"}}, + }, + e: NewRowEventsWithEvts( + RowEvent{Kind: EventUnchanged, Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + }, + "update": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + rr: Rows{ + Row{ID: "A", Fields: Fields{"10", "2", "3"}}, + Row{ID: "B", Fields: Fields{"0", "2", "3"}}, + Row{ID: "C", Fields: Fields{"10", "2", "3"}}, + }, + e: NewRowEventsWithEvts( + RowEvent{ + Kind: EventUpdate, + Row: Row{ID: "A", Fields: Fields{"10", "2", "3"}}, + Deltas: DeltaRow{"1", "", ""}, + }, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + }, + } + + var table TableData + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + table.SetRowEvents(u.re) + table.Update(u.rr) + assert.Equal(t, u.e, table.GetRowEvents()) + }) + } +} + +func TestTableDataDelete(t *testing.T) { + uu := map[string]struct { + re, e *RowEvents + kk map[string]struct{} + }{ + "ordered": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + kk: map[string]struct{}{"A": {}, "C": {}}, + e: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + }, + "unordered": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + RowEvent{Row: Row{ID: "D", Fields: Fields{"10", "2", "3"}}}, + ), + kk: map[string]struct{}{"C": {}, "A": {}}, + e: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + }, + } + + var table TableData + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + table.SetRowEvents(u.re) + table.Delete(u.kk) + assert.Equal(t, u.e, table.GetRowEvents()) + }) + } +} diff --git a/internal/model1/test_helper_test.go b/internal/model1/test_helper_test.go new file mode 100644 index 0000000000..42350b333a --- /dev/null +++ b/internal/model1/test_helper_test.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1_test + +import ( + "fmt" + "time" +) + +func testTime() time.Time { + t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00") + if err != nil { + fmt.Println("TestTime Failed", err) + } + return t +} diff --git a/internal/model1/types.go b/internal/model1/types.go new file mode 100644 index 0000000000..2fc32ad278 --- /dev/null +++ b/internal/model1/types.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import ( + "github.com/derailed/tcell/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + NAValue = "na" + + // EventUnchanged notifies listener resource has not changed. + EventUnchanged ResEvent = 1 << iota + + // EventAdd notifies listener of a resource was added. + EventAdd + + // EventUpdate notifies listener of a resource updated. + EventUpdate + + // EventDelete notifies listener of a resource was deleted. + EventDelete + + // EventClear the stack was reset. + EventClear +) + +// DecoratorFunc decorates a string. +type DecoratorFunc func(string) string + +// ColorerFunc represents a resource row colorer. +type ColorerFunc func(ns string, h Header, re *RowEvent) tcell.Color + +// Renderer represents a resource renderer. +type Renderer interface { + // IsGeneric identifies a generic handler. + IsGeneric() bool + + // Render converts raw resources to tabular data. + Render(o interface{}, ns string, row *Row) error + + // Header returns the resource header. + Header(ns string) Header + + // ColorerFunc returns a row colorer function. + ColorerFunc() ColorerFunc +} + +// Generic represents a generic resource. +type Generic interface { + // SetTable sets up the resource tabular definition. + SetTable(ns string, table *metav1.Table) + + // Header returns a resource header. + Header(ns string) Header + + // Render renders the resource. + Render(o interface{}, ns string, row *Row) error +} diff --git a/internal/render/alias.go b/internal/render/alias.go index ce8f386d05..592296f36c 100644 --- a/internal/render/alias.go +++ b/internal/render/alias.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -18,17 +19,17 @@ type Alias struct { } // Header returns a header row. -func (Alias) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "RESOURCE"}, - HeaderColumn{Name: "COMMAND"}, - HeaderColumn{Name: "API-GROUP"}, +func (Alias) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "RESOURCE"}, + model1.HeaderColumn{Name: "COMMAND"}, + model1.HeaderColumn{Name: "API-GROUP"}, } } // Render renders a K8s resource to screen. // BOZO!! Pass in a row with pre-alloc fields?? -func (Alias) Render(o interface{}, ns string, r *Row) error { +func (Alias) Render(o interface{}, ns string, r *model1.Row) error { a, ok := o.(AliasRes) if !ok { return fmt.Errorf("expected AliasRes, but got %T", o) diff --git a/internal/render/alias_test.go b/internal/render/alias_test.go index 85e61f86ef..af85a3f69c 100644 --- a/internal/render/alias_test.go +++ b/internal/render/alias_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" @@ -14,30 +15,30 @@ import ( func TestAliasColorer(t *testing.T) { var a render.Alias - h := render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + h := model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, } - r := render.Row{ID: "g/v/r", Fields: render.Fields{"r", "blee", "g"}} + r := model1.Row{ID: "g/v/r", Fields: model1.Fields{"r", "blee", "g"}} uu := map[string]struct { ns string - re render.RowEvent + re model1.RowEvent e tcell.Color }{ "addAll": { ns: client.NamespaceAll, - re: render.RowEvent{Kind: render.EventAdd, Row: r}, + re: model1.RowEvent{Kind: model1.EventAdd, Row: r}, e: tcell.ColorBlue, }, "deleteAll": { ns: client.NamespaceAll, - re: render.RowEvent{Kind: render.EventDelete, Row: r}, + re: model1.RowEvent{Kind: model1.EventDelete, Row: r}, e: tcell.ColorGray, }, "updateAll": { ns: client.NamespaceAll, - re: render.RowEvent{Kind: render.EventUpdate, Row: r}, + re: model1.RowEvent{Kind: model1.EventUpdate, Row: r}, e: tcell.ColorDefault, }, } @@ -45,16 +46,16 @@ func TestAliasColorer(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, a.ColorerFunc()(u.ns, h, u.re)) + assert.Equal(t, u.e, a.ColorerFunc()(u.ns, h, &u.re)) }) } } func TestAliasHeader(t *testing.T) { - h := render.Header{ - render.HeaderColumn{Name: "RESOURCE"}, - render.HeaderColumn{Name: "COMMAND"}, - render.HeaderColumn{Name: "API-GROUP"}, + h := model1.Header{ + model1.HeaderColumn{Name: "RESOURCE"}, + model1.HeaderColumn{Name: "COMMAND"}, + model1.HeaderColumn{Name: "API-GROUP"}, } var a render.Alias @@ -70,9 +71,9 @@ func TestAliasRender(t *testing.T) { Aliases: []string{"a", "b", "c"}, } - var r render.Row + var r model1.Row assert.Nil(t, a.Render(o, "fred/v1/blee", &r)) - assert.Equal(t, render.Row{ID: "fred/v1/blee", Fields: render.Fields{"blee", "a,b,c", "fred"}}, r) + assert.Equal(t, model1.Row{ID: "fred/v1/blee", Fields: model1.Fields{"blee", "a,b,c", "fred"}}, r) } func BenchmarkAlias(b *testing.B) { @@ -85,7 +86,7 @@ func BenchmarkAlias(b *testing.B) { b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - var r render.Row + var r model1.Row _ = a.Render(o, "aliases", &r) } } diff --git a/internal/render/base.go b/internal/render/base.go index 65e66b44bb..003fe6a860 100644 --- a/internal/render/base.go +++ b/internal/render/base.go @@ -3,6 +3,10 @@ package render +import ( + "github.com/derailed/k9s/internal/model1" +) + // DecoratorFunc decorates a string. type DecoratorFunc func(string) string @@ -19,11 +23,11 @@ func (Base) IsGeneric() bool { } // ColorerFunc colors a resource row. -func (Base) ColorerFunc() ColorerFunc { - return DefaultColorer +func (Base) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer } // Happy returns true if resource is happy, false otherwise. -func (Base) Happy(_ string, _ Row) bool { +func (Base) Happy(string, *model1.Row) bool { return true } diff --git a/internal/render/benchmark.go b/internal/render/benchmark.go index d8a3c75490..ded85e3170 100644 --- a/internal/render/benchmark.go +++ b/internal/render/benchmark.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "github.com/derailed/tview" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,33 +34,34 @@ type Benchmark struct { } // ColorerFunc colors a resource row. -func (b Benchmark) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - if !Happy(ns, h, re.Row) { - return ErrColor +func (b Benchmark) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + if !model1.IsValid(ns, h, re.Row) { + return model1.ErrColor } + return tcell.ColorPaleGreen } } // Header returns a header row. -func (Benchmark) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "TIME"}, - HeaderColumn{Name: "REQ/S", Align: tview.AlignRight}, - HeaderColumn{Name: "2XX", Align: tview.AlignRight}, - HeaderColumn{Name: "4XX/5XX", Align: tview.AlignRight}, - HeaderColumn{Name: "REPORT"}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Benchmark) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "TIME"}, + model1.HeaderColumn{Name: "REQ/S", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "2XX", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "4XX/5XX", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "REPORT"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (b Benchmark) Render(o interface{}, ns string, r *Row) error { +func (b Benchmark) Render(o interface{}, ns string, r *model1.Row) error { bench, ok := o.(BenchInfo) if !ok { return fmt.Errorf("no benchmarks available %T", o) @@ -71,7 +73,7 @@ func (b Benchmark) Render(o interface{}, ns string, r *Row) error { } r.ID = bench.Path - r.Fields = make(Fields, len(b.Header(ns))) + r.Fields = make(model1.Fields, len(b.Header(ns))) if err := b.initRow(r.Fields, bench.File); err != nil { return err } @@ -82,7 +84,7 @@ func (b Benchmark) Render(o interface{}, ns string, r *Row) error { } // Happy returns true if resource is happy, false otherwise. -func (Benchmark) diagnose(ns string, ff Fields) error { +func (Benchmark) diagnose(ns string, ff model1.Fields) error { statusCol := 3 if !client.IsAllNamespaces(ns) { statusCol-- @@ -109,7 +111,7 @@ func (Benchmark) readFile(file string) (string, error) { return string(data), nil } -func (b Benchmark) initRow(row Fields, f os.FileInfo) error { +func (b Benchmark) initRow(row model1.Fields, f os.FileInfo) error { tokens := strings.Split(f.Name(), "_") if len(tokens) < 2 { return fmt.Errorf("invalid file name %s", f.Name()) @@ -122,7 +124,7 @@ func (b Benchmark) initRow(row Fields, f os.FileInfo) error { return nil } -func (b Benchmark) augmentRow(fields Fields, data string) { +func (b Benchmark) augmentRow(fields model1.Fields, data string) { if len(data) == 0 { return } diff --git a/internal/render/benchmark_int_test.go b/internal/render/benchmark_int_test.go index bd296e23e6..a7d4a387c6 100644 --- a/internal/render/benchmark_int_test.go +++ b/internal/render/benchmark_int_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + "github.com/derailed/k9s/internal/model1" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) @@ -18,23 +19,23 @@ func init() { func TestAugmentRow(t *testing.T) { uu := map[string]struct { file string - e Fields + e model1.Fields }{ "cool": { "testdata/b1.txt", - Fields{"pass", "3.3544", "29.8116", "100", "0"}, + model1.Fields{"pass", "3.3544", "29.8116", "100", "0"}, }, "2XX": { "testdata/b4.txt", - Fields{"pass", "3.3544", "29.8116", "160", "0"}, + model1.Fields{"pass", "3.3544", "29.8116", "160", "0"}, }, "4XX/5XX": { "testdata/b2.txt", - Fields{"pass", "3.3544", "29.8116", "100", "12"}, + model1.Fields{"pass", "3.3544", "29.8116", "100", "12"}, }, "toast": { "testdata/b3.txt", - Fields{"fail", "2.3688", "35.4606", "0", "0"}, + model1.Fields{"fail", "2.3688", "35.4606", "0", "0"}, }, } @@ -44,7 +45,7 @@ func TestAugmentRow(t *testing.T) { data, err := os.ReadFile(u.file) assert.Nil(t, err) - fields := make(Fields, 8) + fields := make(model1.Fields, 8) b := Benchmark{} b.augmentRow(fields, string(data)) assert.Equal(t, u.e, fields[2:7]) diff --git a/internal/render/cm.go b/internal/render/cm.go new file mode 100644 index 0000000000..f6158efbeb --- /dev/null +++ b/internal/render/cm.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package render + +import ( + "fmt" + "strconv" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// ConfigMap renders a K8s ConfigMap to screen. +type ConfigMap struct { + Base +} + +// Header returns a header rbw. +func (ConfigMap) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "DATA"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, + } +} + +// Render renders a K8s resource to screen. +func (n ConfigMap) Render(o interface{}, _ string, r *model1.Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("expected ConfigMap, but got %T", o) + } + var cm v1.ConfigMap + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cm) + if err != nil { + return err + } + + r.ID = client.FQN(cm.Namespace, cm.Name) + r.Fields = model1.Fields{ + cm.Namespace, + cm.Name, + strconv.Itoa(len(cm.Data)), + "", + ToAge(cm.GetCreationTimestamp()), + } + + return nil +} diff --git a/internal/render/color_test.go b/internal/render/color_test.go deleted file mode 100644 index baa8c5ff49..0000000000 --- a/internal/render/color_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package render_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/render" - "github.com/derailed/tcell/v2" - "github.com/stretchr/testify/assert" -) - -func TestDefaultColorer(t *testing.T) { - uu := map[string]struct { - re render.RowEvent - e tcell.Color - }{ - "add": { - render.RowEvent{ - Kind: render.EventAdd, - }, - render.AddColor, - }, - "update": { - render.RowEvent{ - Kind: render.EventUpdate, - }, - render.ModColor, - }, - "delete": { - render.RowEvent{ - Kind: render.EventDelete, - }, - render.KillColor, - }, - "no-change": { - render.RowEvent{ - Kind: render.EventUnchanged, - }, - render.StdColor, - }, - "invalid": { - render.RowEvent{ - Kind: render.EventUnchanged, - Row: render.Row{ - Fields: render.Fields{"", "", "blah"}, - }, - }, - render.ErrColor, - }, - } - - h := render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "VALID"}, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, render.DefaultColorer("", h, u.re)) - }) - } -} diff --git a/internal/render/container.go b/internal/render/container.go index b9bd968e1b..35f700f267 100644 --- a/internal/render/container.go +++ b/internal/render/container.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "github.com/derailed/tview" v1 "k8s.io/api/core/v1" @@ -43,60 +44,58 @@ type Container struct { } // ColorerFunc colors a resource row. -func (c Container) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - if !Happy(ns, h, re.Row) { - return ErrColor - } +func (c Container) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) - stateCol := h.IndexOf("STATE", true) - if stateCol == -1 { - return DefaultColorer(ns, h, re) + idx, ok := h.IndexOf("STATE", true) + if !ok { + return c } - switch strings.TrimSpace(re.Row.Fields[stateCol]) { + switch strings.TrimSpace(re.Row.Fields[idx]) { case Pending: - return PendingColor + return model1.PendingColor case ContainerCreating, PodInitializing: - return AddColor + return model1.AddColor case Terminating, Initialized: - return HighlightColor + return model1.HighlightColor case Completed: - return CompletedColor + return model1.CompletedColor case Running: - return DefaultColorer(ns, h, re) + return c default: - return ErrColor + return model1.ErrColor } } } // Header returns a header row. -func (Container) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "PF"}, - HeaderColumn{Name: "IMAGE"}, - HeaderColumn{Name: "READY"}, - HeaderColumn{Name: "STATE"}, - HeaderColumn{Name: "INIT"}, - HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, - HeaderColumn{Name: "PROBES(L:R)"}, - HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight}, - HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight}, - HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "PORTS"}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Container) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "PF"}, + model1.HeaderColumn{Name: "IMAGE"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "STATE"}, + model1.HeaderColumn{Name: "INIT"}, + model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "PROBES(L:R)"}, + model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "PORTS"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (c Container) Render(o interface{}, name string, r *Row) error { +func (c Container) Render(o interface{}, name string, r *model1.Row) error { co, ok := o.(ContainerRes) if !ok { return fmt.Errorf("expected ContainerRes, but got %T", o) @@ -109,7 +108,7 @@ func (c Container) Render(o interface{}, name string, r *Row) error { } r.ID = co.Container.Name - r.Fields = Fields{ + r.Fields = model1.Fields{ co.Container.Name, "●", co.Container.Image, diff --git a/internal/render/container_test.go b/internal/render/container_test.go index e574df56e6..f790b3a541 100644 --- a/internal/render/container_test.go +++ b/internal/render/container_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" @@ -26,10 +27,10 @@ func TestContainer(t *testing.T) { IsInit: false, Age: makeAge(), } - var r render.Row + var r model1.Row assert.Nil(t, c.Render(cres, "blee", &r)) assert.Equal(t, "fred", r.ID) - assert.Equal(t, render.Fields{ + assert.Equal(t, model1.Fields{ "fred", "●", "img", @@ -63,7 +64,7 @@ func BenchmarkContainerRender(b *testing.B) { IsInit: false, Age: makeAge(), } - var r render.Row + var r model1.Row b.ReportAllocs() b.ResetTimer() diff --git a/internal/render/context.go b/internal/render/context.go index 3c105e0f20..06a622ad62 100644 --- a/internal/render/context.go +++ b/internal/render/context.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/runtime" @@ -20,11 +21,11 @@ type Context struct { } // ColorerFunc colors a resource row. -func (Context) ColorerFunc() ColorerFunc { - return func(ns string, h Header, r RowEvent) tcell.Color { - c := DefaultColorer(ns, h, r) +func (Context) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, r *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, r) if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") { - return HighlightColor + return model1.HighlightColor } return c @@ -32,17 +33,17 @@ func (Context) ColorerFunc() ColorerFunc { } // Header returns a header row. -func (Context) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "CLUSTER"}, - HeaderColumn{Name: "AUTHINFO"}, - HeaderColumn{Name: "NAMESPACE"}, +func (Context) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "CLUSTER"}, + model1.HeaderColumn{Name: "AUTHINFO"}, + model1.HeaderColumn{Name: "NAMESPACE"}, } } // Render renders a K8s resource to screen. -func (c Context) Render(o interface{}, _ string, r *Row) error { +func (c Context) Render(o interface{}, _ string, r *model1.Row) error { ctx, ok := o.(*NamedContext) if !ok { return fmt.Errorf("expected *NamedContext, but got %T", o) @@ -54,7 +55,7 @@ func (c Context) Render(o interface{}, _ string, r *Row) error { } r.ID = ctx.Name - r.Fields = Fields{ + r.Fields = model1.Fields{ name, ctx.Context.Cluster, ctx.Context.AuthInfo, diff --git a/internal/render/context_test.go b/internal/render/context_test.go index 4c20249ce5..1cdc3911f5 100644 --- a/internal/render/context_test.go +++ b/internal/render/context_test.go @@ -6,6 +6,7 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" "k8s.io/client-go/tools/clientcmd/api" @@ -20,7 +21,7 @@ func TestContextHeader(t *testing.T) { func TestContextRender(t *testing.T) { uu := map[string]struct { ctx *render.NamedContext - e render.Row + e model1.Row }{ "active": { ctx: &render.NamedContext{ @@ -33,9 +34,9 @@ func TestContextRender(t *testing.T) { }, Config: &config{}, }, - e: render.Row{ + e: model1.Row{ ID: "c1", - Fields: render.Fields{"c1", "c1", "u1", "ns1"}, + Fields: model1.Fields{"c1", "c1", "u1", "ns1"}, }, }, } @@ -44,7 +45,7 @@ func TestContextRender(t *testing.T) { for k := range uu { uc := uu[k] t.Run(k, func(t *testing.T) { - row := render.NewRow(4) + row := model1.NewRow(4) err := r.Render(uc.ctx, "", &row) assert.Nil(t, err) diff --git a/internal/render/cr.go b/internal/render/cr.go index 5a0a84fdbd..a148fc70b5 100644 --- a/internal/render/cr.go +++ b/internal/render/cr.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -18,16 +19,16 @@ type ClusterRole struct { } // Header returns a header rbw. -func (ClusterRole) Header(string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (ClusterRole) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (ClusterRole) Render(o interface{}, ns string, r *Row) error { +func (ClusterRole) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expecting clusterrole, but got %T", o) @@ -39,7 +40,7 @@ func (ClusterRole) Render(o interface{}, ns string, r *Row) error { } r.ID = client.FQN("-", cr.ObjectMeta.Name) - r.Fields = Fields{ + r.Fields = model1.Fields{ cr.Name, mapToStr(cr.Labels), ToAge(cr.GetCreationTimestamp()), diff --git a/internal/render/cr_test.go b/internal/render/cr_test.go index d6908a0ecc..d6d175311a 100644 --- a/internal/render/cr_test.go +++ b/internal/render/cr_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestClusterRoleRender(t *testing.T) { c := render.ClusterRole{} - r := render.NewRow(2) + r := model1.NewRow(2) assert.NoError(t, c.Render(load(t, "cr"), "-", &r)) assert.Equal(t, "-/blee", r.ID) - assert.Equal(t, render.Fields{"blee"}, r.Fields[:1]) + assert.Equal(t, model1.Fields{"blee"}, r.Fields[:1]) } diff --git a/internal/render/crb.go b/internal/render/crb.go index e051337dcf..8290973e6f 100644 --- a/internal/render/crb.go +++ b/internal/render/crb.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -18,19 +19,19 @@ type ClusterRoleBinding struct { } // Header returns a header rbw. -func (ClusterRoleBinding) Header(string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "CLUSTERROLE"}, - HeaderColumn{Name: "SUBJECT-KIND"}, - HeaderColumn{Name: "SUBJECTS"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (ClusterRoleBinding) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "CLUSTERROLE"}, + model1.HeaderColumn{Name: "SUBJECT-KIND"}, + model1.HeaderColumn{Name: "SUBJECTS"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error { +func (ClusterRoleBinding) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected ClusterRoleBinding, but got %T", o) @@ -44,7 +45,7 @@ func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error { kind, ss := renderSubjects(crb.Subjects) r.ID = client.FQN("-", crb.ObjectMeta.Name) - r.Fields = Fields{ + r.Fields = model1.Fields{ crb.Name, crb.RoleRef.Name, kind, diff --git a/internal/render/crb_test.go b/internal/render/crb_test.go index 85aac27671..4b350a4c94 100644 --- a/internal/render/crb_test.go +++ b/internal/render/crb_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestClusterRoleBindingRender(t *testing.T) { c := render.ClusterRoleBinding{} - r := render.NewRow(5) + r := model1.NewRow(5) assert.NoError(t, c.Render(load(t, "crb"), "-", &r)) assert.Equal(t, "-/blee", r.ID) - assert.Equal(t, render.Fields{"blee", "blee", "User", "fernand"}, r.Fields[:4]) + assert.Equal(t, model1.Fields{"blee", "blee", "User", "fernand"}, r.Fields[:4]) } diff --git a/internal/render/crd.go b/internal/render/crd.go index bed197fbc3..fddeec1973 100644 --- a/internal/render/crd.go +++ b/internal/render/crd.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/rs/zerolog/log" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -21,18 +22,22 @@ type CustomResourceDefinition struct { } // Header returns a header rbw. -func (CustomResourceDefinition) Header(string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VERSIONS"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (CustomResourceDefinition) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "GROUP"}, + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "VERSIONS"}, + model1.HeaderColumn{Name: "SCOPE"}, + model1.HeaderColumn{Name: "ALIASES", Wide: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (c CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error { +func (c CustomResourceDefinition) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected CustomResourceDefinition, but got %T", o) @@ -44,7 +49,7 @@ func (c CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error return err } - versions := make([]string, 0, 3) + versions := make([]string, 0, len(crd.Spec.Versions)) for _, v := range crd.Spec.Versions { if v.Served { n := v.Name @@ -55,15 +60,19 @@ func (c CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error } } if len(versions) == 0 { - log.Warn().Msgf("unable to assert CRD versions for %s", crd.GetName()) + log.Warn().Msgf("unable to assert CRD versions for %s", crd.Name) } - r.ID = client.FQN(client.ClusterScope, crd.GetName()) - r.Fields = Fields{ - crd.GetName(), + r.ID = client.MetaFQN(crd.ObjectMeta) + r.Fields = model1.Fields{ + crd.Spec.Names.Plural, + crd.Spec.Group, + crd.Spec.Names.Kind, naStrings(versions), + string(crd.Spec.Scope), + naStrings(crd.Spec.Names.ShortNames), mapToIfc(crd.GetLabels()), - AsStatus(c.diagnose(crd.GetName(), crd.Spec.Versions)), + AsStatus(c.diagnose(crd.Name, crd.Spec.Versions)), ToAge(crd.GetCreationTimestamp()), } diff --git a/internal/render/crd_test.go b/internal/render/crd_test.go index fd12851049..a88715ee47 100644 --- a/internal/render/crd_test.go +++ b/internal/render/crd_test.go @@ -6,15 +6,17 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestCustomResourceDefinitionRender(t *testing.T) { c := render.CustomResourceDefinition{} - r := render.NewRow(2) + r := model1.NewRow(2) assert.NoError(t, c.Render(load(t, "crd"), "", &r)) assert.Equal(t, "-/adapters.config.istio.io", r.ID) - assert.Equal(t, render.Fields{"adapters.config.istio.io"}, r.Fields[:1]) + assert.Equal(t, "adapters", r.Fields[0]) + assert.Equal(t, "config.istio.io", r.Fields[1]) } diff --git a/internal/render/cronjob.go b/internal/render/cronjob.go index e75d74f8c8..bd1ce7c334 100644 --- a/internal/render/cronjob.go +++ b/internal/render/cronjob.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -21,29 +22,26 @@ type CronJob struct { } // Header returns a header row. -func (CronJob) Header(ns string) Header { - h := Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS", VS: true}, - HeaderColumn{Name: "SCHEDULE"}, - HeaderColumn{Name: "SUSPEND"}, - HeaderColumn{Name: "ACTIVE"}, - HeaderColumn{Name: "LAST_SCHEDULE", Time: true}, - HeaderColumn{Name: "SELECTOR", Wide: true}, - HeaderColumn{Name: "CONTAINERS", Wide: true}, - HeaderColumn{Name: "IMAGES", Wide: true}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (CronJob) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "SCHEDULE"}, + model1.HeaderColumn{Name: "SUSPEND"}, + model1.HeaderColumn{Name: "ACTIVE"}, + model1.HeaderColumn{Name: "LAST_SCHEDULE", Time: true}, + model1.HeaderColumn{Name: "SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "CONTAINERS", Wide: true}, + model1.HeaderColumn{Name: "IMAGES", Wide: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } - - return h - } // Render renders a K8s resource to screen. -func (c CronJob) Render(o interface{}, ns string, r *Row) error { +func (c CronJob) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected CronJob, but got %T", o) @@ -60,7 +58,7 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(cj.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ cj.Namespace, cj.Name, computeVulScore(cj.ObjectMeta, &cj.Spec.JobTemplate.Spec.Template.Spec), diff --git a/internal/render/cronjob_test.go b/internal/render/cronjob_test.go index ff11bd9bf8..34a77a96ce 100644 --- a/internal/render/cronjob_test.go +++ b/internal/render/cronjob_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestCronJobRender(t *testing.T) { c := render.CronJob{} - r := render.NewRow(6) + r := model1.NewRow(6) assert.NoError(t, c.Render(load(t, "cj"), "", &r)) assert.Equal(t, "default/hello", r.ID) - assert.Equal(t, render.Fields{"default", "hello", "0", "*/1 * * * *", "false", "0"}, r.Fields[:6]) + assert.Equal(t, model1.Fields{"default", "hello", "0", "*/1 * * * *", "false", "0"}, r.Fields[:6]) } diff --git a/internal/render/delta_test.go b/internal/render/delta_test.go deleted file mode 100644 index 08d8960c34..0000000000 --- a/internal/render/delta_test.go +++ /dev/null @@ -1,266 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package render_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/render" - "github.com/stretchr/testify/assert" -) - -func TestDeltaLabelize(t *testing.T) { - uu := map[string]struct { - o render.Row - n render.Row - e render.DeltaRow - }{ - "same": { - o: render.Row{ - Fields: render.Fields{"a", "b", "blee=fred,doh=zorg"}, - }, - n: render.Row{ - Fields: render.Fields{"a", "b", "blee=fred1,doh=zorg"}, - }, - e: render.DeltaRow{"", "", "fred", "zorg"}, - }, - } - - hh := render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - } - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - d := render.NewDeltaRow(u.o, u.n, hh) - d = d.Labelize([]int{0, 1}, 2) - assert.Equal(t, u.e, d) - }) - } -} - -func TestDeltaCustomize(t *testing.T) { - uu := map[string]struct { - r1, r2 render.Row - cols []int - e render.DeltaRow - }{ - "same": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - cols: []int{0, 1, 2}, - e: render.DeltaRow{"", "", ""}, - }, - "empty": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - e: render.DeltaRow{}, - }, - "diff-full": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a1", "b1", "c1"}, - }, - cols: []int{0, 1, 2}, - e: render.DeltaRow{"a", "b", "c"}, - }, - "diff-reverse": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a1", "b1", "c1"}, - }, - cols: []int{2, 1, 0}, - e: render.DeltaRow{"c", "b", "a"}, - }, - "diff-skip": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a1", "b1", "c1"}, - }, - cols: []int{2, 0}, - e: render.DeltaRow{"c", "a"}, - }, - "diff-missing": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a1", "b1", "c1"}, - }, - cols: []int{2, 10, 0}, - e: render.DeltaRow{"c", "", "a"}, - }, - "diff-negative": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a1", "b1", "c1"}, - }, - cols: []int{2, -1, 0}, - e: render.DeltaRow{"c", "", "a"}, - }, - } - - hh := render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - } - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - d := render.NewDeltaRow(u.r1, u.r2, hh) - out := make(render.DeltaRow, len(u.cols)) - d.Customize(u.cols, out) - assert.Equal(t, u.e, out) - }) - } -} - -func TestDeltaNew(t *testing.T) { - uu := map[string]struct { - o render.Row - n render.Row - blank bool - e render.DeltaRow - }{ - "same": { - o: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - n: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - blank: true, - e: render.DeltaRow{"", "", ""}, - }, - "diff": { - o: render.Row{ - Fields: render.Fields{"a1", "b", "c"}, - }, - n: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - e: render.DeltaRow{"a1", "", ""}, - }, - "diff2": { - o: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - n: render.Row{ - Fields: render.Fields{"a", "b1", "c"}, - }, - e: render.DeltaRow{"", "b", ""}, - }, - "diffLast": { - o: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - n: render.Row{ - Fields: render.Fields{"a", "b", "c1"}, - }, - e: render.DeltaRow{"", "", "c"}, - }, - } - - hh := render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - } - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - d := render.NewDeltaRow(u.o, u.n, hh) - assert.Equal(t, u.e, d) - assert.Equal(t, u.blank, d.IsBlank()) - }) - } -} - -func TestDeltaBlank(t *testing.T) { - uu := map[string]struct { - r render.DeltaRow - e bool - }{ - "empty": { - r: render.DeltaRow{}, - e: true, - }, - "blank": { - r: render.DeltaRow{"", "", ""}, - e: true, - }, - "notblank": { - r: render.DeltaRow{"", "", "z"}, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.r.IsBlank()) - }) - } -} - -func TestDeltaDiff(t *testing.T) { - uu := map[string]struct { - d1, d2 render.DeltaRow - ageCol int - e bool - }{ - "empty": { - d1: render.DeltaRow{"f1", "f2", "f3"}, - ageCol: 2, - e: true, - }, - "same": { - d1: render.DeltaRow{"f1", "f2", "f3"}, - d2: render.DeltaRow{"f1", "f2", "f3"}, - ageCol: -1, - }, - "diff": { - d1: render.DeltaRow{"f1", "f2", "f3"}, - d2: render.DeltaRow{"f1", "f2", "f13"}, - ageCol: -1, - e: true, - }, - "diff-age-first": { - d1: render.DeltaRow{"f1", "f2", "f3"}, - d2: render.DeltaRow{"f1", "f2", "f13"}, - ageCol: 0, - e: true, - }, - "diff-age-last": { - d1: render.DeltaRow{"f1", "f2", "f3"}, - d2: render.DeltaRow{"f1", "f2", "f13"}, - ageCol: 2, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.d1.Diff(u.d2, u.ageCol)) - }) - } -} diff --git a/internal/render/dir.go b/internal/render/dir.go index b444c8c7c4..8e076d4e2e 100644 --- a/internal/render/dir.go +++ b/internal/render/dir.go @@ -7,6 +7,7 @@ import ( "fmt" "os" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -21,22 +22,22 @@ func (Dir) IsGeneric() bool { } // ColorerFunc colors a resource row. -func (Dir) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (Dir) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorCadetBlue } } // Header returns a header row. -func (Dir) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, +func (Dir) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, } } // Render renders a K8s resource to screen. // BOZO!! Pass in a row with pre-alloc fields?? -func (Dir) Render(o interface{}, ns string, r *Row) error { +func (Dir) Render(o interface{}, ns string, r *model1.Row) error { d, ok := o.(DirRes) if !ok { return fmt.Errorf("expected DirRes, but got %T", o) diff --git a/internal/render/dp.go b/internal/render/dp.go index 01eb96e69f..1444eeb99a 100644 --- a/internal/render/dp.go +++ b/internal/render/dp.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "github.com/derailed/tview" appsv1 "k8s.io/api/apps/v1" @@ -22,20 +23,18 @@ type Deployment struct { } // ColorerFunc colors a resource row. -func (d Deployment) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - c := DefaultColorer(ns, h, re) - if !Happy(ns, h, re.Row) { - return ErrColor - } - rdCol := h.IndexOf("READY", true) - if rdCol == -1 { +func (d Deployment) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) + + idx, ok := h.IndexOf("READY", true) + if !ok { return c } - ready := strings.TrimSpace(re.Row.Fields[rdCol]) + ready := strings.TrimSpace(re.Row.Fields[idx]) tt := strings.Split(ready, "/") if len(tt) == 2 && tt[1] == "0" { - return PendingColor + return model1.PendingColor } return c @@ -43,24 +42,22 @@ func (d Deployment) ColorerFunc() ColorerFunc { } // Header returns a header row. -func (Deployment) Header(ns string) Header { - h := Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS", VS: true}, - HeaderColumn{Name: "READY", Align: tview.AlignRight}, - HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, - HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Deployment) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "READY", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } - - return h } // Render renders a K8s resource to screen. -func (d Deployment) Render(o interface{}, ns string, r *Row) error { +func (d Deployment) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Deployment, but got %T", o) @@ -73,7 +70,7 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(dp.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ dp.Namespace, dp.Name, computeVulScore(dp.ObjectMeta, &dp.Spec.Template.Spec), diff --git a/internal/render/dp_test.go b/internal/render/dp_test.go index 92653b1864..e4ecc4b145 100644 --- a/internal/render/dp_test.go +++ b/internal/render/dp_test.go @@ -6,22 +6,23 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestDpRender(t *testing.T) { c := render.Deployment{} - r := render.NewRow(7) + r := model1.NewRow(7) assert.Nil(t, c.Render(load(t, "dp"), "", &r)) assert.Equal(t, "icx/icx-db", r.ID) - assert.Equal(t, render.Fields{"icx", "icx-db", "0", "1/1", "1", "1"}, r.Fields[:6]) + assert.Equal(t, model1.Fields{"icx", "icx-db", "0", "1/1", "1", "1"}, r.Fields[:6]) } func BenchmarkDpRender(b *testing.B) { c := render.Deployment{} - r := render.NewRow(7) + r := model1.NewRow(7) o := load(b, "dp") b.ResetTimer() diff --git a/internal/render/ds.go b/internal/render/ds.go index d14a1569b5..b3f047aa7a 100644 --- a/internal/render/ds.go +++ b/internal/render/ds.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tview" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -20,26 +21,24 @@ type DaemonSet struct { } // Header returns a header row. -func (DaemonSet) Header(ns string) Header { - h := Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS", VS: true}, - HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, - HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, - HeaderColumn{Name: "READY", Align: tview.AlignRight}, - HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, - HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (DaemonSet) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "READY", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } - - return h } // Render renders a K8s resource to screen. -func (d DaemonSet) Render(o interface{}, ns string, r *Row) error { +func (d DaemonSet) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected DaemonSet, but got %T", o) @@ -51,7 +50,7 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(ds.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ ds.Namespace, ds.Name, computeVulScore(ds.ObjectMeta, &ds.Spec.Template.Spec), diff --git a/internal/render/ds_test.go b/internal/render/ds_test.go index 5753bcb6a4..16598332da 100644 --- a/internal/render/ds_test.go +++ b/internal/render/ds_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestDaemonSetRender(t *testing.T) { c := render.DaemonSet{} - r := render.NewRow(9) + r := model1.NewRow(9) assert.NoError(t, c.Render(load(t, "ds"), "", &r)) assert.Equal(t, "kube-system/fluentd-gcp-v3.2.0", r.ID) - assert.Equal(t, render.Fields{"kube-system", "fluentd-gcp-v3.2.0", "0", "2", "2", "2", "2", "2"}, r.Fields[:8]) + assert.Equal(t, model1.Fields{"kube-system", "fluentd-gcp-v3.2.0", "0", "2", "2", "2", "2", "2"}, r.Fields[:8]) } diff --git a/internal/render/ep.go b/internal/render/ep.go index 15af70d1b4..9fa4bcc80d 100644 --- a/internal/render/ep.go +++ b/internal/render/ep.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -20,17 +21,17 @@ type Endpoints struct { } // Header returns a header row. -func (Endpoints) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "ENDPOINTS"}, - HeaderColumn{Name: "AGE", Time: true}, +func (Endpoints) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "ENDPOINTS"}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (e Endpoints) Render(o interface{}, ns string, r *Row) error { +func (e Endpoints) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Endpoints, but got %T", o) @@ -42,8 +43,8 @@ func (e Endpoints) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(ep.ObjectMeta) - r.Fields = make(Fields, 0, len(e.Header(ns))) - r.Fields = Fields{ + r.Fields = make(model1.Fields, 0, len(e.Header(ns))) + r.Fields = model1.Fields{ ep.Namespace, ep.Name, missing(toEPs(ep.Subsets)), diff --git a/internal/render/ep_test.go b/internal/render/ep_test.go index 620f87e0cf..f4359f3a7f 100644 --- a/internal/render/ep_test.go +++ b/internal/render/ep_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestEndpointsRender(t *testing.T) { c := render.Endpoints{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "ep"), "", &r)) assert.Equal(t, "default/dictionary1", r.ID) - assert.Equal(t, render.Fields{"default", "dictionary1", ""}, r.Fields[:3]) + assert.Equal(t, model1.Fields{"default", "dictionary1", ""}, r.Fields[:3]) } diff --git a/internal/render/ev.go b/internal/render/ev.go index f2e8b9b1f1..28e04f7923 100644 --- a/internal/render/ev.go +++ b/internal/render/ev.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -22,14 +23,14 @@ func (*Event) IsGeneric() bool { } // ColorerFunc colors a resource row. -func (e *Event) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - reasonCol := h.IndexOf("REASON", true) - if reasonCol >= 0 && strings.TrimSpace(re.Row.Fields[reasonCol]) == "Killing" { - return KillColor +func (e *Event) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + idx, ok := h.IndexOf("REASON", true) + if ok && strings.TrimSpace(re.Row.Fields[idx]) == "Killing" { + return model1.KillColor } - return DefaultColorer(ns, h, re) + return model1.DefaultColorer(ns, h, re) } } @@ -46,14 +47,14 @@ var wideCols = map[string]struct{}{ "MESSAGE": {}, } -func (e *Event) Header(ns string) Header { +func (e *Event) Header(ns string) model1.Header { if e.table == nil { - return Header{} + return model1.Header{} } - hh := make(Header, 0, len(e.table.ColumnDefinitions)) - hh = append(hh, HeaderColumn{Name: "NAMESPACE"}) + hh := make(model1.Header, 0, len(e.table.ColumnDefinitions)) + hh = append(hh, model1.HeaderColumn{Name: "NAMESPACE"}) for _, h := range e.table.ColumnDefinitions { - header := HeaderColumn{Name: strings.ToUpper(h.Name)} + header := model1.HeaderColumn{Name: strings.ToUpper(h.Name)} if _, ok := ageCols[header.Name]; ok { header.Time = true } @@ -67,7 +68,7 @@ func (e *Event) Header(ns string) Header { } // Render renders a K8s resource to screen. -func (e *Event) Render(o interface{}, ns string, r *Row) error { +func (e *Event) Render(o interface{}, ns string, r *model1.Row) error { row, ok := o.(metav1.TableRow) if !ok { return fmt.Errorf("expecting a TableRow but got %T", o) @@ -81,7 +82,7 @@ func (e *Event) Render(o interface{}, ns string, r *Row) error { return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0]) } r.ID = client.FQN(nns, name) - r.Fields = make(Fields, 0, len(e.Header(ns))) + r.Fields = make(model1.Fields, 0, len(e.Header(ns))) r.Fields = append(r.Fields, nns) for _, o := range row.Cells { if o == nil { diff --git a/internal/render/ev_test.go b/internal/render/ev_test.go index ce7ba27c73..dbb0794b66 100644 --- a/internal/render/ev_test.go +++ b/internal/render/ev_test.go @@ -6,17 +6,17 @@ package render_test // BOZO!! // func TestEventRender(t *testing.T) { // c := render.Event{} -// r := render.NewRow(7) +// r := model1.NewRow(7) // c.Render(load(t, "ev"), "", &r) // assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID) -// assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7]) +// assert.Equal(t, model1.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7]) // } // func BenchmarkEventRender(b *testing.B) { // ev := load(b, "ev") // var re render.Event -// r := render.NewRow(7) +// r := model1.NewRow(7) // b.ResetTimer() // b.ReportAllocs() diff --git a/internal/render/generic.go b/internal/render/generic.go index fc89fdbefa..56f5523963 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -20,7 +21,7 @@ const ageTableCol = "Age" type Generic struct { Base table *metav1.Table - header Header + header model1.Header ageIndex int } @@ -35,38 +36,38 @@ func (g *Generic) SetTable(ns string, t *metav1.Table) { } // ColorerFunc colors a resource row. -func (*Generic) ColorerFunc() ColorerFunc { - return DefaultColorer +func (*Generic) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer } // Header returns a header row. -func (g *Generic) Header(ns string) Header { +func (g *Generic) Header(ns string) model1.Header { if g.header != nil { return g.header } if g.table == nil { - return Header{} + return model1.Header{} } - h := make(Header, 0, len(g.table.ColumnDefinitions)) + h := make(model1.Header, 0, len(g.table.ColumnDefinitions)) if !client.IsClusterScoped(ns) { - h = append(h, HeaderColumn{Name: "NAMESPACE"}) + h = append(h, model1.HeaderColumn{Name: "NAMESPACE"}) } for i, c := range g.table.ColumnDefinitions { if c.Name == ageTableCol { g.ageIndex = i continue } - h = append(h, HeaderColumn{Name: strings.ToUpper(c.Name)}) + h = append(h, model1.HeaderColumn{Name: strings.ToUpper(c.Name)}) } if g.ageIndex > 0 { - h = append(h, HeaderColumn{Name: "AGE", Time: true}) + h = append(h, model1.HeaderColumn{Name: "AGE", Time: true}) } return h } // Render renders a K8s resource to screen. -func (g *Generic) Render(o interface{}, ns string, r *Row) error { +func (g *Generic) Render(o interface{}, ns string, r *model1.Row) error { row, ok := o.(metav1.TableRow) if !ok { return fmt.Errorf("expecting a TableRow but got %T", o) @@ -80,7 +81,7 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0]) } r.ID = client.FQN(nns, name) - r.Fields = make(Fields, 0, len(g.Header(ns))) + r.Fields = make(model1.Fields, 0, len(g.Header(ns))) if !client.IsClusterScoped(ns) { r.Fields = append(r.Fields, nns) } diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go index 851aaa7290..a71807622d 100644 --- a/internal/render/generic_test.go +++ b/internal/render/generic_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" @@ -18,65 +19,65 @@ func TestGenericRender(t *testing.T) { ns string table *metav1beta1.Table eID string - eFields render.Fields - eHeader render.Header + eFields model1.Fields + eHeader model1.Header }{ "withNS": { ns: "ns1", table: makeNSGeneric(), eID: "ns1/fred", - eFields: render.Fields{"ns1", "c1", "c2", "c3"}, - eHeader: render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + eFields: model1.Fields{"ns1", "c1", "c2", "c3"}, + eHeader: model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, }, "all": { ns: client.NamespaceAll, table: makeNSGeneric(), eID: "ns1/fred", - eFields: render.Fields{"ns1", "c1", "c2", "c3"}, - eHeader: render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + eFields: model1.Fields{"ns1", "c1", "c2", "c3"}, + eHeader: model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, }, "allNS": { ns: client.NamespaceAll, table: makeNSGeneric(), eID: "ns1/fred", - eFields: render.Fields{"ns1", "c1", "c2", "c3"}, - eHeader: render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + eFields: model1.Fields{"ns1", "c1", "c2", "c3"}, + eHeader: model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, }, "clusterWide": { ns: client.ClusterScope, table: makeNoNSGeneric(), eID: "-/fred", - eFields: render.Fields{"c1", "c2", "c3"}, - eHeader: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + eFields: model1.Fields{"c1", "c2", "c3"}, + eHeader: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, }, "age": { ns: client.ClusterScope, table: makeAgeGeneric(), eID: "-/fred", - eFields: render.Fields{"c1", "c2", "2d"}, - eHeader: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "AGE", Time: true}, + eFields: model1.Fields{"c1", "c2", "2d"}, + eHeader: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "AGE", Time: true}, }, }, } @@ -85,7 +86,7 @@ func TestGenericRender(t *testing.T) { var re render.Generic u := uu[k] t.Run(k, func(t *testing.T) { - var r render.Row + var r model1.Row re.SetTable(u.ns, u.table) assert.Equal(t, u.eHeader, re.Header(u.ns)) diff --git a/internal/render/helm/chart.go b/internal/render/helm/chart.go index b41d51f0b5..ee17b98a2a 100644 --- a/internal/render/helm/chart.go +++ b/internal/render/helm/chart.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "helm.sh/helm/v3/pkg/release" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,33 +25,33 @@ func (Chart) IsGeneric() bool { } // ColorerFunc colors a resource row. -func (Chart) ColorerFunc() render.ColorerFunc { - return render.DefaultColorer +func (Chart) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer } // Header returns a header row. -func (Chart) Header(_ string) render.Header { - return render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "NAME"}, - render.HeaderColumn{Name: "REVISION"}, - render.HeaderColumn{Name: "STATUS"}, - render.HeaderColumn{Name: "CHART"}, - render.HeaderColumn{Name: "APP VERSION"}, - render.HeaderColumn{Name: "VALID", Wide: true}, - render.HeaderColumn{Name: "AGE", Time: true}, +func (Chart) Header(_ string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "REVISION"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "CHART"}, + model1.HeaderColumn{Name: "APP VERSION"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a chart to screen. -func (c Chart) Render(o interface{}, ns string, r *render.Row) error { +func (c Chart) Render(o interface{}, ns string, r *model1.Row) error { h, ok := o.(ReleaseRes) if !ok { return fmt.Errorf("expected ReleaseRes, but got %T", o) } r.ID = client.FQN(h.Release.Namespace, h.Release.Name) - r.Fields = render.Fields{ + r.Fields = model1.Fields{ h.Release.Namespace, h.Release.Name, strconv.Itoa(h.Release.Version), diff --git a/internal/render/helm/history.go b/internal/render/helm/history.go index 5fbbaf6d09..cf0f118d33 100644 --- a/internal/render/helm/history.go +++ b/internal/render/helm/history.go @@ -9,6 +9,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" ) @@ -26,24 +27,24 @@ func (History) IsGeneric() bool { } // ColorerFunc colors a resource row. -func (History) ColorerFunc() render.ColorerFunc { - return render.DefaultColorer +func (History) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer } // Header returns a header row. -func (History) Header(_ string) render.Header { - return render.Header{ - render.HeaderColumn{Name: "REVISION"}, - render.HeaderColumn{Name: "STATUS"}, - render.HeaderColumn{Name: "CHART"}, - render.HeaderColumn{Name: "APP VERSION"}, - render.HeaderColumn{Name: "DESCRIPTION"}, - render.HeaderColumn{Name: "VALID", Wide: true}, +func (History) Header(_ string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "REVISION"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "CHART"}, + model1.HeaderColumn{Name: "APP VERSION"}, + model1.HeaderColumn{Name: "DESCRIPTION"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, } } // Render renders a chart to screen. -func (c History) Render(o interface{}, ns string, r *render.Row) error { +func (c History) Render(o interface{}, ns string, r *model1.Row) error { h, ok := o.(ReleaseRes) if !ok { return fmt.Errorf("expected HistoryRes, but got %T", o) @@ -51,7 +52,7 @@ func (c History) Render(o interface{}, ns string, r *render.Row) error { r.ID = client.FQN(h.Release.Namespace, h.Release.Name) r.ID += ":" + strconv.Itoa(h.Release.Version) - r.Fields = render.Fields{ + r.Fields = model1.Fields{ strconv.Itoa(h.Release.Version), h.Release.Info.Status.String(), h.Release.Chart.Metadata.Name + "-" + h.Release.Chart.Metadata.Version, diff --git a/internal/render/helpers.go b/internal/render/helpers.go index bd474cdd41..522a6fdf71 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -5,7 +5,6 @@ package render import ( "context" - "math" "sort" "strconv" "strings" @@ -19,11 +18,21 @@ import ( "golang.org/x/text/language" "golang.org/x/text/message" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" ) +// ExtractImages returns a collection of container images. +// !!BOZO!! If this has any legs?? enable scans on other container types. +func ExtractImages(spec *v1.PodSpec) []string { + ii := make([]string, 0, len(spec.Containers)) + for _, c := range spec.Containers { + ii = append(ii, c.Image) + } + + return ii +} + func computeVulScore(m metav1.ObjectMeta, spec *v1.PodSpec) string { if vul.ImgScanner == nil || vul.ImgScanner.ShouldExcludes(m) { return "0" @@ -46,62 +55,12 @@ func runesToNum(rr []rune) int64 { return r } -func durationToSeconds(duration string) int64 { - if len(duration) == 0 { - return 0 - } - if duration == NAValue { - return math.MaxInt64 - } - - num := make([]rune, 0, 5) - var n, m int64 - for _, r := range duration { - switch r { - case 'y': - m = 365 * 24 * 60 * 60 - case 'd': - m = 24 * 60 * 60 - case 'h': - m = 60 * 60 - case 'm': - m = 60 - case 's': - m = 1 - default: - num = append(num, r) - continue - } - n, num = n+runesToNum(num)*m, num[:0] - } - - return n -} - -func capacityToNumber(capacity string) int64 { - quantity := resource.MustParse(capacity) - return quantity.Value() -} - // AsThousands prints a number with thousand separator. func AsThousands(n int64) string { p := message.NewPrinter(language.English) return p.Sprintf("%d", n) } -// Happy returns true if resource is happy, false otherwise. -func Happy(ns string, h Header, r Row) bool { - if len(r.Fields) == 0 { - return true - } - validCol := h.IndexOf("VALID", true) - if validCol < 0 { - return true - } - - return strings.TrimSpace(r.Fields[validCol]) == "" -} - // AsStatus returns error as string. func AsStatus(err error) string { if err == nil { @@ -333,15 +292,15 @@ func strPtrToStr(s *string) string { return *s } -// Check if string is in a string list. -func in(ll []string, s string) bool { - for _, l := range ll { - if l == s { - return true - } - } - return false -} +// // Check if string is in a string list. +// func in(ll []string, s string) bool { +// for _, l := range ll { +// if l == s { +// return true +// } +// } +// return false +// } // Pad a string up to the given length or truncates if greater than length. func Pad(s string, width int) string { @@ -356,29 +315,29 @@ func Pad(s string, width int) string { return s + strings.Repeat(" ", width-len(s)) } -// Converts labels string to map. -func labelize(labels string) map[string]string { - ll := strings.Split(labels, ",") - data := make(map[string]string, len(ll)) - - for _, l := range ll { - tokens := strings.Split(l, "=") - if len(tokens) == 2 { - data[tokens[0]] = tokens[1] - } - } - - return data -} - -func sortLabels(m map[string]string) (keys, vals []string) { - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - vals = append(vals, m[k]) - } - - 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 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 +// } diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go index 05b0f89d6b..0c6d0787d2 100644 --- a/internal/render/helpers_test.go +++ b/internal/render/helpers_test.go @@ -4,91 +4,57 @@ package render import ( - "math" + "encoding/json" + "fmt" + "os" "testing" "time" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" ) -func TestSortLabels(t *testing.T) { - uu := map[string]struct { - labels string - e [][]string - }{ - "simple": { - labels: "a=b,c=d", - e: [][]string{ - {"a", "c"}, - {"b", "d"}, - }, +func TestTableGenericHydrate(t *testing.T) { + raw := raw(t, "p1") + tt := metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "c1"}, + {Name: "c2"}, }, - } - - 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"}, + Rows: []metav1beta1.TableRow{ + { + Cells: []interface{}{"fred", 10}, + Object: runtime.RawExtension{Raw: raw}, + }, + { + Cells: []interface{}{"blee", 20}, + Object: runtime.RawExtension{Raw: raw}, + }, }, } + rr := make([]model1.Row, 2) + var re Generic + re.SetTable("blee", &tt) - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, labelize(u.labels)) - }) - } + assert.Nil(t, model1.GenericHydrate("blee", &tt, rr, &re)) + assert.Equal(t, 2, len(rr)) + assert.Equal(t, 3, len(rr[0].Fields)) } -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}, +func TestTableHydrate(t *testing.T) { + oo := []runtime.Object{ + &PodWithMetrics{Raw: load(t, "p1")}, } + rr := make([]model1.Row, 1) - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, durationToSeconds(u.s)) - }) - } -} - -func BenchmarkDurationToSecond(b *testing.B) { - t := "2d22h3m50s" - - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - durationToSeconds(t) - } + assert.Nil(t, model1.Hydrate("blee", oo, rr, Pod{})) + assert.Equal(t, 1, len(rr)) + assert.Equal(t, 23, len(rr[0].Fields)) } func TestToAge(t *testing.T) { @@ -308,34 +274,6 @@ func TestBlank(t *testing.T) { } } -func TestIn(t *testing.T) { - uu := map[string]struct { - a []string - v string - e bool - }{ - "in": { - a: []string{"fred", "blee"}, - v: "blee", - e: true, - }, - "empty": { - v: "blee", - }, - "missing": { - a: []string{"fred", "blee"}, - v: "duh", - }, - } - - for k := range uu { - uc := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, uc.e, in(uc.a, uc.v)) - }) - } -} - func TestMetaFQN(t *testing.T) { uu := map[string]struct { m metav1.ObjectMeta @@ -489,3 +427,20 @@ func BenchmarkIntToStr(b *testing.B) { IntToStr(v) } } + +// Helpers... + +func load(t *testing.T, n string) *unstructured.Unstructured { + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + assert.Nil(t, err) + var o unstructured.Unstructured + err = json.Unmarshal(raw, &o) + assert.Nil(t, err) + return &o +} + +func raw(t *testing.T, n string) []byte { + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + assert.Nil(t, err) + return raw +} diff --git a/internal/render/img_scan.go b/internal/render/img_scan.go index 03ab3c2eb9..691e2f104a 100644 --- a/internal/render/img_scan.go +++ b/internal/render/img_scan.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/vul" "github.com/derailed/tcell/v2" "k8s.io/apimachinery/pkg/runtime" @@ -24,15 +25,15 @@ type ImageScan struct { } // ColorerFunc colors a resource row. -func (c ImageScan) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - c := DefaultColorer(ns, h, re) +func (c ImageScan) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) - sevCol := h.IndexOf(sevColName, true) - if sevCol == -1 { + idx, ok := h.IndexOf(sevColName, true) + if !ok { return c } - sev := strings.TrimSpace(re.Row.Fields[sevCol]) + sev := strings.TrimSpace(re.Row.Fields[idx]) switch sev { case vul.Sev1: c = tcell.ColorRed @@ -54,27 +55,27 @@ func (c ImageScan) ColorerFunc() ColorerFunc { } // Header returns a header row. -func (ImageScan) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "SEVERITY"}, - HeaderColumn{Name: "VULNERABILITY"}, - HeaderColumn{Name: "IMAGE"}, - HeaderColumn{Name: "LIBRARY"}, - HeaderColumn{Name: "VERSION"}, - HeaderColumn{Name: "FIXED-IN"}, - HeaderColumn{Name: "TYPE"}, +func (ImageScan) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "SEVERITY"}, + model1.HeaderColumn{Name: "VULNERABILITY"}, + model1.HeaderColumn{Name: "IMAGE"}, + model1.HeaderColumn{Name: "LIBRARY"}, + model1.HeaderColumn{Name: "VERSION"}, + model1.HeaderColumn{Name: "FIXED-IN"}, + model1.HeaderColumn{Name: "TYPE"}, } } // Render renders a K8s resource to screen. -func (is ImageScan) Render(o interface{}, name string, r *Row) error { +func (is ImageScan) Render(o interface{}, name string, r *model1.Row) error { res, ok := o.(ImageScanRes) if !ok { return fmt.Errorf("expected ImageScanRes, but got %T", o) } r.ID = fmt.Sprintf("%s|%s", res.Image, strings.Join(res.Row, "|")) - r.Fields = Fields{ + r.Fields = model1.Fields{ res.Row.Severity(), res.Row.Vulnerability(), res.Image, diff --git a/internal/render/job.go b/internal/render/job.go index 89298d314d..451f811ca6 100644 --- a/internal/render/job.go +++ b/internal/render/job.go @@ -10,6 +10,7 @@ import ( "time" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,25 +25,23 @@ type Job struct { } // Header returns a header row. -func (Job) Header(ns string) Header { - h := Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS", VS: true}, - HeaderColumn{Name: "COMPLETIONS"}, - HeaderColumn{Name: "DURATION"}, - HeaderColumn{Name: "SELECTOR", Wide: true}, - HeaderColumn{Name: "CONTAINERS", Wide: true}, - HeaderColumn{Name: "IMAGES", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Job) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "COMPLETIONS"}, + model1.HeaderColumn{Name: "DURATION"}, + model1.HeaderColumn{Name: "SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "CONTAINERS", Wide: true}, + model1.HeaderColumn{Name: "IMAGES", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } - - return h } // Render renders a K8s resource to screen. -func (j Job) Render(o interface{}, ns string, r *Row) error { +func (j Job) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Job, but got %T", o) @@ -57,7 +56,7 @@ func (j Job) Render(o interface{}, ns string, r *Row) error { cc, ii := toContainers(job.Spec.Template.Spec) r.ID = client.MetaFQN(job.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ job.Namespace, job.Name, computeVulScore(job.ObjectMeta, &job.Spec.Template.Spec), diff --git a/internal/render/job_test.go b/internal/render/job_test.go index b26179966a..028a4ddfe4 100644 --- a/internal/render/job_test.go +++ b/internal/render/job_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestJobRender(t *testing.T) { c := render.Job{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "job"), "", &r)) assert.Equal(t, "default/hello-1567179180", r.ID) - assert.Equal(t, render.Fields{"default", "hello-1567179180", "0", "1/1", "8s", "controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218", "c1", "blang/busybox-bash"}, r.Fields[:8]) + assert.Equal(t, model1.Fields{"default", "hello-1567179180", "0", "1/1", "8s", "controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218", "c1", "blang/busybox-bash"}, r.Fields[:8]) } diff --git a/internal/render/node.go b/internal/render/node.go index 96b935811b..4a43c43e11 100644 --- a/internal/render/node.go +++ b/internal/render/node.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tview" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -30,32 +31,32 @@ type Node struct { } // Header returns a header row. -func (Node) Header(_ string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "ROLE"}, - HeaderColumn{Name: "ARCH", Wide: true}, - HeaderColumn{Name: "TAINTS"}, - HeaderColumn{Name: "VERSION"}, - HeaderColumn{Name: "KERNEL", Wide: true}, - HeaderColumn{Name: "INTERNAL-IP", Wide: true}, - HeaderColumn{Name: "EXTERNAL-IP", Wide: true}, - HeaderColumn{Name: "PODS", Align: tview.AlignRight}, - HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "CPU/A", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "MEM/A", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Node) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "ROLE"}, + model1.HeaderColumn{Name: "ARCH", Wide: true}, + model1.HeaderColumn{Name: "TAINTS"}, + model1.HeaderColumn{Name: "VERSION"}, + model1.HeaderColumn{Name: "KERNEL", Wide: true}, + model1.HeaderColumn{Name: "INTERNAL-IP", Wide: true}, + model1.HeaderColumn{Name: "EXTERNAL-IP", Wide: true}, + model1.HeaderColumn{Name: "PODS", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "CPU/A", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "MEM/A", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (n Node) Render(o interface{}, ns string, r *Row) error { +func (n Node) Render(o interface{}, ns string, r *model1.Row) error { oo, ok := o.(*NodeWithMetrics) if !ok { return fmt.Errorf("expected *NodeAndMetrics, but got %T", o) @@ -87,7 +88,7 @@ func (n Node) Render(o interface{}, ns string, r *Row) error { podCount = NAValue } r.ID = client.FQN("", na) - r.Fields = Fields{ + r.Fields = model1.Fields{ no.Name, join(statuses, ","), join(roles, ","), diff --git a/internal/render/node_test.go b/internal/render/node_test.go index a276f06b72..09fb4a6889 100644 --- a/internal/render/node_test.go +++ b/internal/render/node_test.go @@ -6,6 +6,7 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,12 +20,12 @@ func TestNodeRender(t *testing.T) { } var no render.Node - r := render.NewRow(14) + r := model1.NewRow(14) err := no.Render(&pom, "", &r) assert.Nil(t, err) assert.Equal(t, "minikube", r.ID) - e := render.Fields{"minikube", "Ready", "master", "amd64", "0", "v1.15.2", "4.15.0", "192.168.64.107", "", "0", "10", "20", "0", "0", "4000", "7874"} + e := model1.Fields{"minikube", "Ready", "master", "amd64", "0", "v1.15.2", "4.15.0", "192.168.64.107", "", "0", "10", "20", "0", "0", "4000", "7874"} assert.Equal(t, e, r.Fields[:16]) } @@ -34,7 +35,7 @@ func BenchmarkNodeRender(b *testing.B) { MX: makeNodeMX("n1", "10m", "10Mi"), } var no render.Node - r := render.NewRow(14) + r := model1.NewRow(14) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/internal/render/np.go b/internal/render/np.go index 26333aebfe..8f7bb24262 100644 --- a/internal/render/np.go +++ b/internal/render/np.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" netv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -20,24 +21,24 @@ type NetworkPolicy struct { } // Header returns a header row. -func (NetworkPolicy) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "ING-SELECTOR", Wide: true}, - HeaderColumn{Name: "ING-PORTS"}, - HeaderColumn{Name: "ING-BLOCK"}, - HeaderColumn{Name: "EGR-SELECTOR", Wide: true}, - HeaderColumn{Name: "EGR-PORTS"}, - HeaderColumn{Name: "EGR-BLOCK"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (NetworkPolicy) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "ING-SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "ING-PORTS"}, + model1.HeaderColumn{Name: "ING-BLOCK"}, + model1.HeaderColumn{Name: "EGR-SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "EGR-PORTS"}, + model1.HeaderColumn{Name: "EGR-BLOCK"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error { +func (n NetworkPolicy) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected NetworkPolicy, but got %T", o) @@ -52,7 +53,7 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error { ep, es, eb := egress(np.Spec.Egress) r.ID = client.MetaFQN(np.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ np.Namespace, np.Name, is, diff --git a/internal/render/np_test.go b/internal/render/np_test.go index bd3412f205..bd371df453 100644 --- a/internal/render/np_test.go +++ b/internal/render/np_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestNetworkPolicyRender(t *testing.T) { c := render.NetworkPolicy{} - r := render.NewRow(9) + r := model1.NewRow(9) assert.NoError(t, c.Render(load(t, "np"), "", &r)) assert.Equal(t, "default/fred", r.ID) - assert.Equal(t, render.Fields{"default", "fred", "ns:app=blee,po:app=fred", "TCP:6379", "172.17.0.0/16[172.17.1.0/24,172.17.3.0/24...]", "", "TCP:5978", "10.0.0.0/24"}, r.Fields[:8]) + assert.Equal(t, model1.Fields{"default", "fred", "ns:app=blee,po:app=fred", "TCP:6379", "172.17.0.0/16[172.17.1.0/24,172.17.3.0/24...]", "", "TCP:5978", "10.0.0.0/24"}, r.Fields[:8]) } diff --git a/internal/render/ns.go b/internal/render/ns.go index 77954a5eda..4f9ecf81f6 100644 --- a/internal/render/ns.go +++ b/internal/render/ns.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -21,19 +22,17 @@ type Namespace struct { } // ColorerFunc colors a resource row. -func (n Namespace) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - c := DefaultColorer(ns, h, re) - - if re.Kind == EventUpdate { - c = StdColor +func (n Namespace) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) + if c == model1.ErrColor { + return c } - if strings.Contains(strings.TrimSpace(re.Row.Fields[0]), "*") { - c = HighlightColor + if re.Kind == model1.EventUpdate { + c = model1.StdColor } - - if !Happy(ns, h, re.Row) { - c = ErrColor + if strings.Contains(strings.TrimSpace(re.Row.Fields[0]), "*") { + c = model1.HighlightColor } return c @@ -41,18 +40,18 @@ func (n Namespace) ColorerFunc() ColorerFunc { } // Header returns a header rbw. -func (Namespace) Header(string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Namespace) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (n Namespace) Render(o interface{}, _ string, r *Row) error { +func (n Namespace) Render(o interface{}, _ string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Namespace, but got %T", o) @@ -64,7 +63,7 @@ func (n Namespace) Render(o interface{}, _ string, r *Row) error { } r.ID = client.MetaFQN(ns.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ ns.Name, string(ns.Status.Phase), mapToStr(ns.Labels), diff --git a/internal/render/ns_test.go b/internal/render/ns_test.go index 81ca793e3d..ad4337bd7d 100644 --- a/internal/render/ns_test.go +++ b/internal/render/ns_test.go @@ -6,6 +6,7 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" @@ -13,66 +14,66 @@ import ( func TestNSColorer(t *testing.T) { uu := map[string]struct { - re render.RowEvent + re model1.RowEvent e tcell.Color }{ "add": { - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{ + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{ "blee", "Active", }, }, }, - e: render.AddColor, + e: model1.AddColor, }, "update": { - re: render.RowEvent{ - Kind: render.EventUpdate, - Row: render.Row{ - Fields: render.Fields{ + re: model1.RowEvent{ + Kind: model1.EventUpdate, + Row: model1.Row{ + Fields: model1.Fields{ "blee", "Active", }, }, }, - e: render.StdColor, + e: model1.StdColor, }, "decorator": { - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{ + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{ "blee*", "Active", }, }, }, - e: render.HighlightColor, + e: model1.HighlightColor, }, } - h := render.Header{ - render.HeaderColumn{Name: "NAME"}, - render.HeaderColumn{Name: "STATUS"}, + h := model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, } var r render.Namespace for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, r.ColorerFunc()("", h, u.re)) + assert.Equal(t, u.e, r.ColorerFunc()("", h, &u.re)) }) } } func TestNamespaceRender(t *testing.T) { c := render.Namespace{} - r := render.NewRow(3) + r := model1.NewRow(3) assert.NoError(t, c.Render(load(t, "ns"), "-", &r)) assert.Equal(t, "-/kube-system", r.ID) - assert.Equal(t, render.Fields{"kube-system", "Active"}, r.Fields[:2]) + assert.Equal(t, model1.Fields{"kube-system", "Active"}, r.Fields[:2]) } diff --git a/internal/render/pdb.go b/internal/render/pdb.go index 3b29962d56..656b49b8bd 100644 --- a/internal/render/pdb.go +++ b/internal/render/pdb.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tview" v1beta1 "k8s.io/api/policy/v1beta1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -21,24 +22,24 @@ type PodDisruptionBudget struct { } // Header returns a header row. -func (PodDisruptionBudget) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "MIN AVAILABLE", Align: tview.AlignRight}, - HeaderColumn{Name: "MAX UNAVAILABLE", Align: tview.AlignRight}, - HeaderColumn{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight}, - HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, - HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, - HeaderColumn{Name: "EXPECTED", Align: tview.AlignRight}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (PodDisruptionBudget) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "MIN AVAILABLE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "MAX UNAVAILABLE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "EXPECTED", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error { +func (p PodDisruptionBudget) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected PodDisruptionBudget, but got %T", o) @@ -50,7 +51,7 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(pdb.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ pdb.Namespace, pdb.Name, numbToStr(pdb.Spec.MinAvailable), diff --git a/internal/render/pdb_test.go b/internal/render/pdb_test.go index 2f40acaedb..9567c487f8 100644 --- a/internal/render/pdb_test.go +++ b/internal/render/pdb_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestPodDisruptionBudgetRender(t *testing.T) { c := render.PodDisruptionBudget{} - r := render.NewRow(9) + r := model1.NewRow(9) assert.NoError(t, c.Render(load(t, "pdb"), "", &r)) assert.Equal(t, "default/fred", r.ID) - assert.Equal(t, render.Fields{"default", "fred", "2", render.NAValue, "0", "0", "2", "0"}, r.Fields[:8]) + assert.Equal(t, model1.Fields{"default", "fred", "2", render.NAValue, "0", "0", "2", "0"}, r.Fields[:8]) } diff --git a/internal/render/pod.go b/internal/render/pod.go index 710bd6eefd..829834df5a 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -18,6 +18,7 @@ import ( mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" ) const ( @@ -51,84 +52,67 @@ type Pod struct { } // ColorerFunc colors a resource row. -func (p Pod) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - c := DefaultColorer(ns, h, re) +func (p Pod) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) - statusCol := h.IndexOf("STATUS", true) - if statusCol == -1 { + idx, ok := h.IndexOf("STATUS", true) + if !ok { return c } - status := strings.TrimSpace(re.Row.Fields[statusCol]) + status := strings.TrimSpace(re.Row.Fields[idx]) switch status { case Pending, ContainerCreating: - c = PendingColor + c = model1.PendingColor case PodInitializing: - c = AddColor + c = model1.AddColor case Initialized: - c = HighlightColor + c = model1.HighlightColor case Completed: - c = CompletedColor + c = model1.CompletedColor case Running: - c = StdColor - if !Happy(ns, h, re.Row) { - c = ErrColor + if c != model1.ErrColor { + c = model1.StdColor } case Terminating: - c = KillColor - default: - if !Happy(ns, h, re.Row) { - c = ErrColor - } + c = model1.KillColor } + return c } } // Header returns a header row. -func (Pod) Header(ns string) Header { - h := Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS", VS: true}, - HeaderColumn{Name: "PF"}, - HeaderColumn{Name: "READY"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, - HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight, Wide: true}, - HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight, Wide: true}, - HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "IP"}, - HeaderColumn{Name: "NODE"}, - HeaderColumn{Name: "NOMINATED NODE", Wide: true}, - HeaderColumn{Name: "READINESS GATES", Wide: true}, - HeaderColumn{Name: "QOS", Wide: true}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (p Pod) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "PF"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight, Wide: true}, + model1.HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight, Wide: true}, + model1.HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "IP"}, + model1.HeaderColumn{Name: "NODE"}, + model1.HeaderColumn{Name: "NOMINATED NODE", Wide: true}, + model1.HeaderColumn{Name: "READINESS GATES", Wide: true}, + model1.HeaderColumn{Name: "QOS", Wide: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } - - return h -} - -// ExtractImages returns a collection of container images. -// !!BOZO!! If this has any legs?? enable scans on other container types. -func ExtractImages(spec *v1.PodSpec) []string { - ii := make([]string, 0, len(spec.Containers)) - for _, c := range spec.Containers { - ii = append(ii, c.Image) - } - - return ii } // Render renders a K8s resource to screen. -func (p Pod) Render(o interface{}, ns string, row *Row) error { +func (p Pod) Render(o interface{}, ns string, row *model1.Row) error { pwm, ok := o.(*PodWithMetrics) if !ok { return fmt.Errorf("expected PodWithMetrics, but got %T", o) @@ -151,9 +135,10 @@ func (p Pod) Render(o interface{}, ns string, row *Row) error { c, r := gatherCoMX(po.Spec.Containers, ccmx) phase := p.Phase(&po) row.ID = client.MetaFQN(po.ObjectMeta) - row.Fields = Fields{ + + row.Fields = model1.Fields{ po.Namespace, - po.ObjectMeta.Name, + po.Name, computeVulScore(po.ObjectMeta, &po.Spec), "●", strconv.Itoa(cr) + "/" + strconv.Itoa(len(po.Spec.Containers)), diff --git a/internal/render/pod_test.go b/internal/render/pod_test.go index ec2e058a78..57fe6c4da8 100644 --- a/internal/render/pod_test.go +++ b/internal/render/pod_test.go @@ -6,6 +6,7 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" @@ -16,128 +17,128 @@ import ( ) func init() { - render.AddColor = tcell.ColorBlue - render.HighlightColor = tcell.ColorYellow - render.CompletedColor = tcell.ColorGray - render.StdColor = tcell.ColorWhite - render.ErrColor = tcell.ColorRed - render.KillColor = tcell.ColorGray + model1.AddColor = tcell.ColorBlue + model1.HighlightColor = tcell.ColorYellow + model1.CompletedColor = tcell.ColorGray + model1.StdColor = tcell.ColorWhite + model1.ErrColor = tcell.ColorRed + model1.KillColor = tcell.ColorGray } func TestPodColorer(t *testing.T) { - stdHeader := render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "NAME"}, - render.HeaderColumn{Name: "READY"}, - render.HeaderColumn{Name: "RESTARTS"}, - render.HeaderColumn{Name: "STATUS"}, - render.HeaderColumn{Name: "VALID"}, + stdHeader := model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "RESTARTS"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "VALID"}, } uu := map[string]struct { - re render.RowEvent - h render.Header + re model1.RowEvent + h model1.Header e tcell.Color }{ "valid": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.Running, ""}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Running, ""}, }, }, - e: render.StdColor, + e: model1.StdColor, }, "init": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, ""}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, ""}, }, }, - e: render.AddColor, + e: model1.AddColor, }, "init-err": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, "blah"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, "blah"}, }, }, - e: render.AddColor, + e: model1.AddColor, }, "initialized": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.Initialized, "blah"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Initialized, "blah"}, }, }, - e: render.HighlightColor, + e: model1.HighlightColor, }, "completed": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.Completed, "blah"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Completed, "blah"}, }, }, - e: render.CompletedColor, + e: model1.CompletedColor, }, "terminating": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.Terminating, "blah"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Terminating, "blah"}, }, }, - e: render.KillColor, + e: model1.KillColor, }, "invalid": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", "Running", "blah"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", "Running", "blah"}, }, }, - e: render.ErrColor, + e: model1.ErrColor, }, "unknown-cool": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", ""}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", ""}, }, }, - e: render.AddColor, + e: model1.AddColor, }, "unknown-err": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", "doh"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", "doh"}, }, }, - e: render.ErrColor, + e: model1.ErrColor, }, "status": { h: stdHeader[0:3], - re: render.RowEvent{ - Kind: render.EventDelete, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", ""}, + re: model1.RowEvent{ + Kind: model1.EventDelete, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", ""}, }, }, - e: render.KillColor, + e: model1.KillColor, }, } @@ -145,7 +146,7 @@ func TestPodColorer(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, r.ColorerFunc()("", u.h, u.re)) + assert.Equal(t, u.e, r.ColorerFunc()("", u.h, &u.re)) }) } } @@ -157,12 +158,12 @@ func TestPodRender(t *testing.T) { } var po render.Pod - r := render.NewRow(14) + r := model1.NewRow(14) err := po.Render(&pom, "", &r) assert.Nil(t, err) assert.Equal(t, "default/nginx", r.ID) - e := render.Fields{"default", "nginx", "0", "●", "1/1", "Running", "0", "100", "50", "100:0", "70:170", "100", "n/a", "71", "29", "172.17.0.6", "minikube", "", ""} + e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Running", "0", "100", "50", "100:0", "70:170", "100", "n/a", "71", "29", "172.17.0.6", "minikube", "", ""} assert.Equal(t, e, r.Fields[:19]) } @@ -172,7 +173,7 @@ func BenchmarkPodRender(b *testing.B) { MX: makePodMX("nginx", "10m", "10Mi"), } var po render.Pod - r := render.NewRow(12) + r := model1.NewRow(12) b.ReportAllocs() b.ResetTimer() @@ -188,12 +189,12 @@ func TestPodInitRender(t *testing.T) { } var po render.Pod - r := render.NewRow(14) + r := model1.NewRow(14) err := po.Render(&pom, "", &r) assert.Nil(t, err) assert.Equal(t, "default/nginx", r.ID) - e := render.Fields{"default", "nginx", "0", "●", "1/1", "Init:0/1", "0", "10", "10", "100:0", "70:170", "10", "n/a", "14", "5", "172.17.0.6", "minikube", "", ""} + e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Init:0/1", "0", "10", "10", "100:0", "70:170", "10", "n/a", "14", "5", "172.17.0.6", "minikube", "", ""} assert.Equal(t, e, r.Fields[:19]) } diff --git a/internal/render/policy.go b/internal/render/policy.go index e750bcb0fd..777ecfaafc 100644 --- a/internal/render/policy.go +++ b/internal/render/policy.go @@ -7,23 +7,24 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) -func rbacVerbHeader() Header { - return Header{ - HeaderColumn{Name: "GET "}, - HeaderColumn{Name: "LIST "}, - HeaderColumn{Name: "WATCH "}, - HeaderColumn{Name: "CREATE"}, - HeaderColumn{Name: "PATCH "}, - HeaderColumn{Name: "UPDATE"}, - HeaderColumn{Name: "DELETE"}, - HeaderColumn{Name: "DEL-LIST "}, - HeaderColumn{Name: "EXTRAS", Wide: true}, +func rbacVerbHeader() model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "GET "}, + model1.HeaderColumn{Name: "LIST "}, + model1.HeaderColumn{Name: "WATCH "}, + model1.HeaderColumn{Name: "CREATE"}, + model1.HeaderColumn{Name: "PATCH "}, + model1.HeaderColumn{Name: "UPDATE"}, + model1.HeaderColumn{Name: "DELETE"}, + model1.HeaderColumn{Name: "DEL-LIST "}, + model1.HeaderColumn{Name: "EXTRAS", Wide: true}, } } @@ -33,28 +34,28 @@ type Policy struct { } // ColorerFunc colors a resource row. -func (Policy) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (Policy) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorMediumSpringGreen } } // Header returns a header row. -func (Policy) Header(ns string) Header { - h := Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "API-GROUP"}, - HeaderColumn{Name: "BINDING"}, +func (Policy) Header(ns string) model1.Header { + h := model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "API-GROUP"}, + model1.HeaderColumn{Name: "BINDING"}, } h = append(h, rbacVerbHeader()...) - h = append(h, HeaderColumn{Name: "VALID", Wide: true}) + h = append(h, model1.HeaderColumn{Name: "VALID", Wide: true}) return h } // Render renders a K8s resource to screen. -func (Policy) Render(o interface{}, gvr string, r *Row) error { +func (Policy) Render(o interface{}, gvr string, r *model1.Row) error { p, ok := o.(PolicyRes) if !ok { return fmt.Errorf("expecting PolicyRes but got %T", o) diff --git a/internal/render/policy_test.go b/internal/render/policy_test.go index 536425fb3d..b0770d14dc 100644 --- a/internal/render/policy_test.go +++ b/internal/render/policy_test.go @@ -7,6 +7,7 @@ import ( "errors" "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) @@ -46,7 +47,7 @@ func TestPolicyResMerge(t *testing.T) { func TestPolicyRender(t *testing.T) { var p render.Policy - var r render.Row + var r model1.Row o := render.PolicyRes{ Namespace: "blee", Binding: "fred", @@ -59,7 +60,7 @@ func TestPolicyRender(t *testing.T) { assert.Nil(t, p.Render(o, "fred", &r)) assert.Equal(t, "blee/res", r.ID) - assert.Equal(t, render.Fields{ + assert.Equal(t, model1.Fields{ "blee", "res", "grp", diff --git a/internal/render/popeye.go b/internal/render/popeye.go index c8dd757d54..e43df218c6 100644 --- a/internal/render/popeye.go +++ b/internal/render/popeye.go @@ -3,92 +3,82 @@ package render -import ( - "fmt" - "math" - "strconv" - "strings" - - "github.com/derailed/k9s/internal/client" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/tcell/v2" - "github.com/derailed/tview" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -// Popeye renders a sanitizer to screen. -type Popeye struct { - Base -} - -// ColorerFunc colors a resource row. -func (Popeye) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - c := DefaultColorer(ns, h, re) - - warnCol := h.IndexOf("WARNING", true) - status, _ := strconv.Atoi(strings.TrimSpace(re.Row.Fields[warnCol])) - if status > 0 { - c = tcell.ColorOrange - } - errCol := h.IndexOf("ERROR", true) - status, _ = strconv.Atoi(strings.TrimSpace(re.Row.Fields[errCol])) - if status > 0 { - c = ErrColor - } - return c - } -} - -// Header returns a header row. -func (Popeye) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "RESOURCE"}, - HeaderColumn{Name: "SCORE%", Align: tview.AlignRight}, - HeaderColumn{Name: "SCANNED", Align: tview.AlignRight}, - HeaderColumn{Name: "ERROR", Align: tview.AlignRight}, - HeaderColumn{Name: "WARNING", Align: tview.AlignRight}, - HeaderColumn{Name: "INFO", Align: tview.AlignRight}, - HeaderColumn{Name: "OK", Align: tview.AlignRight}, - } -} - -// Render renders a K8s resource to screen. -func (Popeye) Render(o interface{}, ns string, r *Row) error { - s, ok := o.(Section) - if !ok { - return fmt.Errorf("expected Section, but got %T", o) - } - - r.ID = client.FQN(ns, s.Title) - r.Fields = append(r.Fields, - s.Title, - strconv.Itoa(s.Tally.Score()), - strconv.Itoa(s.Tally.OK+s.Tally.Info+s.Tally.Warning+s.Tally.Error), - strconv.Itoa(s.Tally.Error), - strconv.Itoa(s.Tally.Warning), - strconv.Itoa(s.Tally.Info), - strconv.Itoa(s.Tally.OK), - ) - return nil -} - -// ---------------------------------------------------------------------------- -// Helpers... +import "github.com/derailed/popeye/pkg/config" + +// !!BOZO!! Popeye + +// // Popeye renders a sanitizer to screen. +// type Popeye struct { +// Base +// } + +// // ColorerFunc colors a resource row. +// func (Popeye) ColorerFunc() ColorerFunc { +// return func(ns string, h Header, re *model1.RowEvent) tcell.Color { +// c := DefaultColorer(ns, h, re) + +// warnCol := h.IndexOf("WARNING", true) +// status, _ := strconv.Atoi(strings.TrimSpace(re.Row.Fields[warnCol])) +// if status > 0 { +// c = tcell.ColorOrange +// } +// errCol := h.IndexOf("ERROR", true) +// status, _ = strconv.Atoi(strings.TrimSpace(re.Row.Fields[errCol])) +// if status > 0 { +// c = ErrColor +// } +// return c +// } +// } + +// // Header returns a header row. +// func (Popeye) Header(ns string) model1.Header { +// return model1.Header{ +// model1.HeaderColumn{Name: "RESOURCE"}, +// model1.HeaderColumn{Name: "SCORE%", Align: tview.AlignRight}, +// model1.HeaderColumn{Name: "SCANNED", Align: tview.AlignRight}, +// model1.HeaderColumn{Name: "ERROR", Align: tview.AlignRight}, +// model1.HeaderColumn{Name: "WARNING", Align: tview.AlignRight}, +// model1.HeaderColumn{Name: "INFO", Align: tview.AlignRight}, +// model1.HeaderColumn{Name: "OK", Align: tview.AlignRight}, +// } +// } + +// // Render renders a K8s resource to screen. +// func (Popeye) Render(o interface{}, ns string, r *model1.Row) error { +// s, ok := o.(Section) +// if !ok { +// return fmt.Errorf("expected Section, but got %T", o) +// } + +// r.ID = client.FQN(ns, s.Title) +// r.Fields = append(r.Fields, +// s.Title, +// strconv.Itoa(s.Tally.Score()), +// strconv.Itoa(s.Tally.OK+s.Tally.Info+s.Tally.Warning+s.Tally.Error), +// strconv.Itoa(s.Tally.Error), +// strconv.Itoa(s.Tally.Warning), +// strconv.Itoa(s.Tally.Info), +// strconv.Itoa(s.Tally.OK), +// ) +// return nil +// } + +// // ---------------------------------------------------------------------------- +// // Helpers... type ( - // Builder represents a popeye report. - Builder struct { - Report Report `json:"popeye" yaml:"popeye"` - } - - // Report represents the output of a sanitization pass. - Report struct { - Score int `json:"score" yaml:"score"` - Grade string `json:"grade" yaml:"grade"` - Sections Sections `json:"sanitizers,omitempty" yaml:"sanitizers,omitempty"` - } + // // Builder represents a popeye report. + // Builder struct { + // Report Report `json:"popeye" yaml:"popeye"` + // } + + // // Report represents the output of a sanitization pass. + // Report struct { + // Score int `json:"score" yaml:"score"` + // Grade string `json:"grade" yaml:"grade"` + // Sections Sections `json:"sanitizers,omitempty" yaml:"sanitizers,omitempty"` + // } // Sections represents a collection of sections. Sections []Section @@ -116,89 +106,90 @@ type ( } // Tally tracks a section scores. + Tally struct { OK, Info, Warning, Error int Count int } ) -// Sum sums up tally counts. -func (t *Tally) Sum() int { - return t.OK + t.Info + t.Warning + t.Error -} - -// Score returns the overall sections score in percent. -func (t *Tally) Score() int { - oks := t.OK + t.Info - return toPerc(float64(oks), float64(oks+t.Warning+t.Error)) -} - -func toPerc(v1, v2 float64) int { - if v2 == 0 { - return 0 - } - return int(math.Floor((v1 / v2) * 100)) -} - -// Len returns a section length. -func (s Sections) Len() int { - return len(s) -} - -// Swap swaps values. -func (s Sections) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -// Less compares section scores. -func (s Sections) Less(i, j int) bool { - t1, t2 := s[i].Tally, s[j].Tally - return t1.Score() < t2.Score() -} - -// GetObjectKind returns a schema object. -func (Section) GetObjectKind() schema.ObjectKind { - return nil -} - -// DeepCopyObject returns a container copy. -func (s Section) DeepCopyObject() runtime.Object { - return s -} - -// MaxSeverity gather the max severity in a collection of issues. -func (s Section) MaxSeverity() config.Level { - max := config.OkLevel - for _, issues := range s.Outcome { - m := issues.MaxSeverity() - if m > max { - max = m - } - } - - return max -} - -// MaxSeverity gather the max severity in a collection of issues. -func (i Issues) MaxSeverity() config.Level { - max := config.OkLevel - for _, is := range i { - if is.Level > max { - max = is.Level - } - } - - return max -} - -// CountSeverity counts severity level instances. -func (i Issues) CountSeverity(l config.Level) int { - var count int - for _, is := range i { - if is.Level == l { - count++ - } - } - - return count -} +// // Sum sums up tally counts. +// func (t *Tally) Sum() int { +// return t.OK + t.Info + t.Warning + t.Error +// } + +// // Score returns the overall sections score in percent. +// func (t *Tally) Score() int { +// oks := t.OK + t.Info +// return toPerc(float64(oks), float64(oks+t.Warning+t.Error)) +// } + +// func toPerc(v1, v2 float64) int { +// if v2 == 0 { +// return 0 +// } +// return int(math.Floor((v1 / v2) * 100)) +// } + +// // Len returns a section length. +// func (s Sections) Len() int { +// return len(s) +// } + +// // Swap swaps values. +// func (s Sections) Swap(i, j int) { +// s[i], s[j] = s[j], s[i] +// } + +// // Less compares section scores. +// func (s Sections) Less(i, j int) bool { +// t1, t2 := s[i].Tally, s[j].Tally +// return t1.Score() < t2.Score() +// } + +// // GetObjectKind returns a schema object. +// func (Section) GetObjectKind() schema.ObjectKind { +// return nil +// } + +// // DeepCopyObject returns a container copy. +// func (s Section) DeepCopyObject() runtime.Object { +// return s +// } + +// // MaxSeverity gather the max severity in a collection of issues. +// func (s Section) MaxSeverity() config.Level { +// max := config.OkLevel +// for _, issues := range s.Outcome { +// m := issues.MaxSeverity() +// if m > max { +// max = m +// } +// } + +// return max +// } + +// // MaxSeverity gather the max severity in a collection of issues. +// func (i Issues) MaxSeverity() config.Level { +// max := config.OkLevel +// for _, is := range i { +// if is.Level > max { +// max = is.Level +// } +// } + +// return max +// } + +// // CountSeverity counts severity level instances. +// func (i Issues) CountSeverity(l config.Level) int { +// var count int +// for _, is := range i { +// if is.Level == l { +// count++ +// } +// } + +// return count +// } diff --git a/internal/render/port_forward_test.go b/internal/render/port_forward_test.go index c3d0d9c226..6c4cd18819 100644 --- a/internal/render/port_forward_test.go +++ b/internal/render/port_forward_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) @@ -23,10 +24,10 @@ func TestPortForwardRender(t *testing.T) { } var p render.PortForward - var r render.Row + var r model1.Row assert.Nil(t, p.Render(o, "fred", &r)) assert.Equal(t, "blee/fred", r.ID) - assert.Equal(t, render.Fields{ + assert.Equal(t, model1.Fields{ "blee", "fred", "co", diff --git a/internal/render/portforward.go b/internal/render/portforward.go index 5ae2f71eee..267be33cf6 100644 --- a/internal/render/portforward.go +++ b/internal/render/portforward.go @@ -9,6 +9,7 @@ import ( "time" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -39,29 +40,29 @@ type PortForward struct { } // ColorerFunc colors a resource row. -func (PortForward) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (PortForward) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorSkyblue } } // Header returns a header row. -func (PortForward) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "CONTAINER"}, - HeaderColumn{Name: "PORTS"}, - HeaderColumn{Name: "URL"}, - HeaderColumn{Name: "C"}, - HeaderColumn{Name: "N"}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (PortForward) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "CONTAINER"}, + model1.HeaderColumn{Name: "PORTS"}, + model1.HeaderColumn{Name: "URL"}, + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "N"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (f PortForward) Render(o interface{}, gvr string, r *Row) error { +func (f PortForward) Render(o interface{}, gvr string, r *model1.Row) error { pf, ok := o.(ForwardRes) if !ok { return fmt.Errorf("expecting a ForwardRes but got %T", o) @@ -71,7 +72,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error { r.ID = pf.ID() ns, n := client.Namespaced(r.ID) - r.Fields = Fields{ + r.Fields = model1.Fields{ ns, trimContainer(n), pf.Container(), diff --git a/internal/render/pv.go b/internal/render/pv.go index d91ea18b6e..9e42893725 100644 --- a/internal/render/pv.go +++ b/internal/render/pv.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -23,51 +24,49 @@ type PersistentVolume struct { } // ColorerFunc colors a resource row. -func (p PersistentVolume) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - if !Happy(ns, h, re.Row) { - return ErrColor - } +func (p PersistentVolume) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) - statusCol := h.IndexOf("STATUS", true) - if statusCol == -1 { - return DefaultColorer(ns, h, re) + idx, ok := h.IndexOf("STATUS", true) + if ok { + return c } - switch strings.TrimSpace(re.Row.Fields[statusCol]) { + switch strings.TrimSpace(re.Row.Fields[idx]) { case string(v1.VolumeBound): - return StdColor + return model1.StdColor case string(v1.VolumeAvailable): return tcell.ColorGreen case string(v1.VolumePending): - return PendingColor + return model1.PendingColor case terminatingPhase: - return CompletedColor + return model1.CompletedColor } - return DefaultColorer(ns, h, re) + return c } } // Header returns a header rbw. -func (PersistentVolume) Header(string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "CAPACITY", Capacity: true}, - HeaderColumn{Name: "ACCESS MODES"}, - HeaderColumn{Name: "RECLAIM POLICY"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "CLAIM"}, - HeaderColumn{Name: "STORAGECLASS"}, - HeaderColumn{Name: "REASON"}, - HeaderColumn{Name: "VOLUMEMODE", Wide: true}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (PersistentVolume) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "CAPACITY", Capacity: true}, + model1.HeaderColumn{Name: "ACCESS MODES"}, + model1.HeaderColumn{Name: "RECLAIM POLICY"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "CLAIM"}, + model1.HeaderColumn{Name: "STORAGECLASS"}, + model1.HeaderColumn{Name: "REASON"}, + model1.HeaderColumn{Name: "VOLUMEMODE", Wide: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error { +func (p PersistentVolume) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected PersistentVolume, but got %T", o) @@ -94,7 +93,7 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error { size := pv.Spec.Capacity[v1.ResourceStorage] r.ID = client.MetaFQN(pv.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ pv.Name, size.String(), accessMode(pv.Spec.AccessModes), diff --git a/internal/render/pv_test.go b/internal/render/pv_test.go index 93f77fb583..615fd8b6c5 100644 --- a/internal/render/pv_test.go +++ b/internal/render/pv_test.go @@ -6,24 +6,25 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestPersistentVolumeRender(t *testing.T) { c := render.PersistentVolume{} - r := render.NewRow(9) + r := model1.NewRow(9) assert.NoError(t, c.Render(load(t, "pv"), "-", &r)) assert.Equal(t, "-/pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", r.ID) - assert.Equal(t, render.Fields{"pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", "1Gi", "RWO", "Delete", "Bound", "default/www-nginx-sts-1", "standard"}, r.Fields[:7]) + assert.Equal(t, model1.Fields{"pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", "1Gi", "RWO", "Delete", "Bound", "default/www-nginx-sts-1", "standard"}, r.Fields[:7]) } func TestTerminatingPersistentVolumeRender(t *testing.T) { c := render.PersistentVolume{} - r := render.NewRow(9) + r := model1.NewRow(9) assert.NoError(t, c.Render(load(t, "pv_terminating"), "-", &r)) assert.Equal(t, "-/pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", r.ID) - assert.Equal(t, render.Fields{"pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", "1Gi", "RWO", "Delete", "Terminating", "default/www-nginx-sts-2", "standard"}, r.Fields[:7]) + assert.Equal(t, model1.Fields{"pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", "1Gi", "RWO", "Delete", "Terminating", "default/www-nginx-sts-2", "standard"}, r.Fields[:7]) } diff --git a/internal/render/pvc.go b/internal/render/pvc.go index bd3cc43b36..79678346bc 100644 --- a/internal/render/pvc.go +++ b/internal/render/pvc.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -18,23 +19,23 @@ type PersistentVolumeClaim struct { } // Header returns a header rbw. -func (PersistentVolumeClaim) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "VOLUME"}, - HeaderColumn{Name: "CAPACITY", Capacity: true}, - HeaderColumn{Name: "ACCESS MODES"}, - HeaderColumn{Name: "STORAGECLASS"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (PersistentVolumeClaim) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "VOLUME"}, + model1.HeaderColumn{Name: "CAPACITY", Capacity: true}, + model1.HeaderColumn{Name: "ACCESS MODES"}, + model1.HeaderColumn{Name: "STORAGECLASS"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error { +func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected PersistentVolumeClaim, but got %T", o) @@ -65,7 +66,7 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(pvc.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ pvc.Namespace, pvc.Name, string(phase), diff --git a/internal/render/pvc_test.go b/internal/render/pvc_test.go index c1005cb1cf..ec85c2e180 100644 --- a/internal/render/pvc_test.go +++ b/internal/render/pvc_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestPersistentVolumeClaimRender(t *testing.T) { c := render.PersistentVolumeClaim{} - r := render.NewRow(8) + r := model1.NewRow(8) assert.NoError(t, c.Render(load(t, "pvc"), "", &r)) assert.Equal(t, "default/www-nginx-sts-0", r.ID) - assert.Equal(t, render.Fields{"default", "www-nginx-sts-0", "Bound", "pvc-fbabd470-8725-11e9-a8e8-42010a80015b", "1Gi", "RWO", "standard"}, r.Fields[:7]) + assert.Equal(t, model1.Fields{"default", "www-nginx-sts-0", "Bound", "pvc-fbabd470-8725-11e9-a8e8-42010a80015b", "1Gi", "RWO", "standard"}, r.Fields[:7]) } diff --git a/internal/render/rbac.go b/internal/render/rbac.go index ec23c8ddb2..12ad96e7a5 100644 --- a/internal/render/rbac.go +++ b/internal/render/rbac.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/derailed/k9s/internal/model1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -37,31 +38,31 @@ type Rbac struct { } // ColorerFunc colors a resource row. -func (Rbac) ColorerFunc() ColorerFunc { - return DefaultColorer +func (Rbac) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer } // Header returns a header row. -func (Rbac) Header(ns string) Header { - h := make(Header, 0, 10) +func (Rbac) Header(ns string) model1.Header { + h := make(model1.Header, 0, 10) h = append(h, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "API-GROUP"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "API-GROUP"}, ) h = append(h, rbacVerbHeader()...) - return append(h, HeaderColumn{Name: "VALID", Wide: true}) + return append(h, model1.HeaderColumn{Name: "VALID", Wide: true}) } // Render renders a K8s resource to screen. -func (r Rbac) Render(o interface{}, ns string, ro *Row) error { +func (r Rbac) Render(o interface{}, ns string, ro *model1.Row) error { p, ok := o.(PolicyRes) if !ok { return fmt.Errorf("expecting RuleRes but got %T", o) } ro.ID = p.Resource - ro.Fields = make(Fields, 0, len(r.Header(ns))) + ro.Fields = make(model1.Fields, 0, len(r.Header(ns))) ro.Fields = append(ro.Fields, cleanseResource(p.Resource), p.Group, diff --git a/internal/render/reference.go b/internal/render/reference.go index 31695438ee..21dec9d75d 100644 --- a/internal/render/reference.go +++ b/internal/render/reference.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -18,24 +19,24 @@ type Reference struct { } // ColorerFunc colors a resource row. -func (Reference) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (Reference) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorCadetBlue } } // Header returns a header row. -func (Reference) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "GVR"}, +func (Reference) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "GVR"}, } } // Render renders a K8s resource to screen. // BOZO!! Pass in a row with pre-alloc fields?? -func (Reference) Render(o interface{}, ns string, r *Row) error { +func (Reference) Render(o interface{}, ns string, r *model1.Row) error { ref, ok := o.(ReferenceRes) if !ok { return fmt.Errorf("expected ReferenceRes, but got %T", o) diff --git a/internal/render/reference_test.go b/internal/render/reference_test.go index 50654c1ade..46aaf70094 100644 --- a/internal/render/reference_test.go +++ b/internal/render/reference_test.go @@ -6,6 +6,7 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) @@ -19,11 +20,11 @@ func TestReferenceRender(t *testing.T) { var ( ref = render.Reference{} - r render.Row + r model1.Row ) assert.Nil(t, ref.Render(o, "fred", &r)) assert.Equal(t, "ns1/blee", r.ID) - assert.Equal(t, render.Fields{ + assert.Equal(t, model1.Fields{ "ns1", "blee", "v1/secrets", diff --git a/internal/render/ro.go b/internal/render/ro.go index 3e90164627..7b3ce31549 100644 --- a/internal/render/ro.go +++ b/internal/render/ro.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -18,22 +19,22 @@ type Role struct { } // Header returns a header row. -func (Role) Header(ns string) Header { - var h Header +func (Role) Header(ns string) model1.Header { + var h model1.Header if client.IsAllNamespaces(ns) { - h = append(h, HeaderColumn{Name: "NAMESPACE"}) + h = append(h, model1.HeaderColumn{Name: "NAMESPACE"}) } return append(h, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, ) } // Render renders a K8s resource to screen. -func (r Role) Render(o interface{}, ns string, row *Row) error { +func (r Role) Render(o interface{}, ns string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Role, but got %T", o) @@ -45,7 +46,7 @@ func (r Role) Render(o interface{}, ns string, row *Row) error { } row.ID = client.MetaFQN(ro.ObjectMeta) - row.Fields = make(Fields, 0, len(r.Header(ns))) + row.Fields = make(model1.Fields, 0, len(r.Header(ns))) if client.IsAllNamespaces(ns) { row.Fields = append(row.Fields, ro.Namespace) } diff --git a/internal/render/ro_test.go b/internal/render/ro_test.go index 1e0e4cc5d4..5beb907d4d 100644 --- a/internal/render/ro_test.go +++ b/internal/render/ro_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestRoleRender(t *testing.T) { c := render.Role{} - r := render.NewRow(3) + r := model1.NewRow(3) assert.NoError(t, c.Render(load(t, "ro"), "", &r)) assert.Equal(t, "default/blee", r.ID) - assert.Equal(t, render.Fields{"default", "blee"}, r.Fields[:2]) + assert.Equal(t, model1.Fields{"default", "blee"}, r.Fields[:2]) } diff --git a/internal/render/rob.go b/internal/render/rob.go index 46783a88f8..1f58fd609c 100644 --- a/internal/render/rob.go +++ b/internal/render/rob.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -19,25 +20,25 @@ type RoleBinding struct { } // Header returns a header rbw. -func (RoleBinding) Header(ns string) Header { - var h Header +func (RoleBinding) Header(ns string) model1.Header { + var h model1.Header if client.IsAllNamespaces(ns) { - h = append(h, HeaderColumn{Name: "NAMESPACE"}) + h = append(h, model1.HeaderColumn{Name: "NAMESPACE"}) } return append(h, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "ROLE"}, - HeaderColumn{Name: "KIND"}, - HeaderColumn{Name: "SUBJECTS"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "ROLE"}, + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "SUBJECTS"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, ) } // Render renders a K8s resource to screen. -func (r RoleBinding) Render(o interface{}, ns string, row *Row) error { +func (r RoleBinding) Render(o interface{}, ns string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected RoleBinding, but got %T", o) @@ -51,7 +52,7 @@ func (r RoleBinding) Render(o interface{}, ns string, row *Row) error { kind, ss := renderSubjects(rb.Subjects) row.ID = client.MetaFQN(rb.ObjectMeta) - row.Fields = make(Fields, 0, len(r.Header(ns))) + row.Fields = make(model1.Fields, 0, len(r.Header(ns))) if client.IsAllNamespaces(ns) { row.Fields = append(row.Fields, rb.Namespace) } diff --git a/internal/render/rob_test.go b/internal/render/rob_test.go index 306cab3066..f18a08bf26 100644 --- a/internal/render/rob_test.go +++ b/internal/render/rob_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestRoleBindingRender(t *testing.T) { c := render.RoleBinding{} - r := render.NewRow(6) + r := model1.NewRow(6) assert.NoError(t, c.Render(load(t, "rb"), "", &r)) assert.Equal(t, "default/blee", r.ID) - assert.Equal(t, render.Fields{"default", "blee", "blee", "SvcAcct", "fernand"}, r.Fields[:5]) + assert.Equal(t, model1.Fields{"default", "blee", "blee", "SvcAcct", "fernand"}, r.Fields[:5]) } diff --git a/internal/render/row.go b/internal/render/row.go deleted file mode 100644 index 858b11987a..0000000000 --- a/internal/render/row.go +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package render - -import ( - "reflect" - "sort" - "strings" - - "github.com/fvbommel/sortorder" -) - -// Fields represents a collection of row fields. -type Fields []string - -// Customize returns a subset of fields. -func (f Fields) Customize(cols []int, out Fields) { - for i, c := range cols { - if c < 0 { - out[i] = NAValue - continue - } - if c < len(f) { - out[i] = f[c] - } - } -} - -// Diff returns true if fields differ or false otherwise. -func (f Fields) Diff(ff Fields, ageCol int) bool { - if ageCol < 0 { - return !reflect.DeepEqual(f[:len(f)-1], ff[:len(ff)-1]) - } - if !reflect.DeepEqual(f[:ageCol], ff[:ageCol]) { - return true - } - return !reflect.DeepEqual(f[ageCol+1:], ff[ageCol+1:]) -} - -// Clone returns a copy of the fields. -func (f Fields) Clone() Fields { - cp := make(Fields, len(f)) - copy(cp, f) - - return cp -} - -// ---------------------------------------------------------------------------- - -// Row represents a collection of columns. -type Row struct { - ID string - Fields Fields -} - -// NewRow returns a new row with initialized fields. -func NewRow(size int) Row { - return Row{Fields: make([]string, size)} -} - -// Labelize returns a new row based on labels. -func (r Row) Labelize(cols []int, labelCol int, labels []string) Row { - out := NewRow(len(cols) + len(labels)) - for _, col := range cols { - out.Fields = append(out.Fields, r.Fields[col]) - } - m := labelize(r.Fields[labelCol]) - for _, label := range labels { - out.Fields = append(out.Fields, m[label]) - } - - return out -} - -// Customize returns a row subset based on given col indices. -func (r Row) Customize(cols []int) Row { - out := NewRow(len(cols)) - r.Fields.Customize(cols, out.Fields) - out.ID = r.ID - - return out -} - -// Diff returns true if row differ or false otherwise. -func (r Row) Diff(ro Row, ageCol int) bool { - if r.ID != ro.ID { - return true - } - return r.Fields.Diff(ro.Fields, ageCol) -} - -// Clone copies a row. -func (r Row) Clone() Row { - return Row{ - ID: r.ID, - Fields: r.Fields.Clone(), - } -} - -// Len returns the length of the row. -func (r Row) Len() int { - return len(r.Fields) -} - -// ---------------------------------------------------------------------------- - -// Rows represents a collection of rows. -type Rows []Row - -// Delete removes an element by id. -func (rr Rows) Delete(id string) Rows { - idx, ok := rr.Find(id) - if !ok { - return rr - } - - if idx == 0 { - return rr[1:] - } - if idx+1 == len(rr) { - return rr[:len(rr)-1] - } - - return append(rr[:idx], rr[idx+1:]...) -} - -// Upsert adds a new item. -func (rr Rows) Upsert(r Row) Rows { - idx, ok := rr.Find(r.ID) - if !ok { - return append(rr, r) - } - rr[idx] = r - - return rr -} - -// Find locates a row by id. Returns false is not found. -func (rr Rows) Find(id string) (int, bool) { - for i, r := range rr { - if r.ID == id { - return i, true - } - } - - return 0, false -} - -// Sort rows based on column index and order. -func (rr Rows) Sort(col int, asc, isNum, isDur, isCapacity bool) { - t := RowSorter{ - Rows: rr, - Index: col, - IsNumber: isNum, - IsDuration: isDur, - IsCapacity: isCapacity, - Asc: asc, - } - sort.Sort(t) -} - -// ---------------------------------------------------------------------------- - -// RowSorter sorts rows. -type RowSorter struct { - Rows Rows - Index int - IsNumber bool - IsDuration bool - IsCapacity bool - Asc bool -} - -func (s RowSorter) Len() int { - return len(s.Rows) -} - -func (s RowSorter) Swap(i, j int) { - s.Rows[i], s.Rows[j] = s.Rows[j], s.Rows[i] -} - -func (s RowSorter) Less(i, j int) bool { - v1, v2 := s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index] - id1, id2 := s.Rows[i].ID, s.Rows[j].ID - less := Less(s.IsNumber, s.IsDuration, s.IsCapacity, id1, id2, v1, v2) - if s.Asc { - return less - } - return !less -} - -// ---------------------------------------------------------------------------- -// Helpers... - -// Less return true if c1 <= c2. -func Less(isNumber, isDuration, isCapacity bool, id1, id2, v1, v2 string) bool { - var less bool - switch { - case isNumber: - less = lessNumber(v1, v2) - case isDuration: - less = lessDuration(v1, v2) - case isCapacity: - less = lessCapacity(v1, v2) - default: - less = sortorder.NaturalLess(v1, v2) - } - if v1 == v2 { - return sortorder.NaturalLess(id1, id2) - } - - return less -} - -func lessDuration(s1, s2 string) bool { - d1, d2 := durationToSeconds(s1), durationToSeconds(s2) - return d1 <= d2 -} - -func lessCapacity(s1, s2 string) bool { - c1, c2 := capacityToNumber(s1), capacityToNumber(s2) - - return c1 <= c2 -} - -func lessNumber(s1, s2 string) bool { - v1, v2 := strings.Replace(s1, ",", "", -1), strings.Replace(s2, ",", "", -1) - - return sortorder.NaturalLess(v1, v2) -} diff --git a/internal/render/row_event_test.go b/internal/render/row_event_test.go deleted file mode 100644 index d75e1894f3..0000000000 --- a/internal/render/row_event_test.go +++ /dev/null @@ -1,539 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package render_test - -import ( - "testing" - "time" - - "github.com/derailed/k9s/internal/render" - "github.com/stretchr/testify/assert" -) - -func TestRowEventCustomize(t *testing.T) { - uu := map[string]struct { - re1, e render.RowEvent - cols []int - }{ - "empty": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{}}, - }, - }, - "full": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - cols: []int{0, 1, 2}, - }, - "deltas": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - Deltas: render.DeltaRow{"a", "b", "c"}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - Deltas: render.DeltaRow{"a", "b", "c"}, - }, - cols: []int{0, 1, 2}, - }, - "deltas-skip": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - Deltas: render.DeltaRow{"a", "b", "c"}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"3", "1"}}, - Deltas: render.DeltaRow{"c", "a"}, - }, - cols: []int{2, 0}, - }, - "reverse": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"3", "2", "1"}}, - }, - cols: []int{2, 1, 0}, - }, - "skip": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"3", "1"}}, - }, - cols: []int{2, 0}, - }, - "miss": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"3", "", "1"}}, - }, - cols: []int{2, 10, 0}, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re1.Customize(u.cols)) - }) - } -} - -func TestRowEventDiff(t *testing.T) { - uu := map[string]struct { - re1, re2 render.RowEvent - e bool - }{ - "same": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - re2: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - }, - "diff-kind": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - re2: render.RowEvent{ - Kind: render.EventDelete, - Row: render.Row{ID: "B", Fields: render.Fields{"1", "2", "3"}}, - }, - e: true, - }, - "diff-delta": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - Deltas: render.DeltaRow{"1", "2", "3"}, - }, - re2: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - Deltas: render.DeltaRow{"10", "2", "3"}, - }, - e: true, - }, - "diff-id": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - re2: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "B", Fields: render.Fields{"1", "2", "3"}}, - }, - e: true, - }, - "diff-field": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - re2: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}}, - }, - e: true, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re1.Diff(u.re2, -1)) - }) - } -} - -func TestRowEventsDiff(t *testing.T) { - uu := map[string]struct { - re1, re2 render.RowEvents - ageCol int - e bool - }{ - "same": { - re1: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re2: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - ageCol: -1, - }, - "diff-len": { - re1: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re2: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - ageCol: -1, - e: true, - }, - "diff-id": { - re1: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re2: render.RowEvents{ - {Row: render.Row{ID: "D", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - ageCol: -1, - e: true, - }, - "diff-order": { - re1: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re2: render.RowEvents{ - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - ageCol: -1, - e: true, - }, - "diff-withAge": { - re1: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re2: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "13"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - ageCol: 1, - e: true, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re1.Diff(u.re2, u.ageCol)) - }) - } -} - -func TestRowEventsUpsert(t *testing.T) { - uu := map[string]struct { - ee, e render.RowEvents - re render.RowEvent - }{ - "add": { - ee: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re: render.RowEvent{ - Row: render.Row{ID: "D", Fields: render.Fields{"f1", "f2", "f3"}}, - }, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - {Row: render.Row{ID: "D", Fields: render.Fields{"f1", "f2", "f3"}}}, - }, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.ee.Upsert(u.re)) - }) - } -} - -func TestRowEventsCustomize(t *testing.T) { - uu := map[string]struct { - re, e render.RowEvents - cols []int - }{ - "same": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - cols: []int{0, 1, 2}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "reverse": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - cols: []int{2, 1, 0}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"3", "2", "1"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"3", "2", "0"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"3", "2", "10"}}}, - }, - }, - "skip": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - cols: []int{1, 0}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"2", "1"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"2", "0"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"2", "10"}}}, - }, - }, - "missing": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - cols: []int{1, 0, 4}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"2", "1", ""}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"2", "0", ""}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"2", "10", ""}}}, - }, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re.Customize(u.cols)) - }) - } -} - -func TestRowEventsDelete(t *testing.T) { - uu := map[string]struct { - re render.RowEvents - id string - e render.RowEvents - }{ - "first": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - id: "A", - e: render.RowEvents{ - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "middle": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - id: "B", - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "last": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - id: "C", - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - }, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re.Delete(u.id)) - }) - } -} - -func TestRowEventsSort(t *testing.T) { - uu := map[string]struct { - re render.RowEvents - col int - duration, num, asc bool - capacity bool - e render.RowEvents - }{ - "age_time": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", testTime().String()}}}, - }, - col: 2, - asc: true, - duration: true, - e: render.RowEvents{ - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", testTime().String()}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}}, - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}}, - }, - }, - "col0": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - col: 0, - asc: true, - e: render.RowEvents{ - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "id_preserve": { - re: render.RowEvents{ - {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}}, - {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}}, - }, - col: 1, - asc: true, - e: render.RowEvents{ - {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}}, - {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}}, - }, - }, - "capacity": { - re: render.RowEvents{ - {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3", "1Gi"}}}, - {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3", "1.1G"}}}, - {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3", "0.5Ti"}}}, - {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3", "12e6"}}}, - {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3", "1234"}}}, - {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3", "0.1Ei"}}}, - }, - col: 3, - asc: true, - capacity: true, - e: render.RowEvents{ - {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3", "1234"}}}, - {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3", "12e6"}}}, - {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3", "1Gi"}}}, - {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3", "1.1G"}}}, - {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3", "0.5Ti"}}}, - {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3", "0.1Ei"}}}, - }, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - u.re.Sort("", u.col, u.duration, u.num, u.capacity, u.asc) - assert.Equal(t, u.e, u.re) - }) - } -} - -func TestRowEventsClone(t *testing.T) { - uu := map[string]struct { - r render.RowEvents - }{ - "empty": { - r: render.RowEvents{}, - }, - "full": { - r: makeRowEvents(), - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - c := u.r.Clone() - assert.Equal(t, len(u.r), len(c)) - if len(u.r) > 0 { - u.r[0].Row.Fields[0] = "blee" - assert.Equal(t, "A", c[0].Row.Fields[0]) - } - }) - } -} - -// Helpers... - -func makeRowEvents() render.RowEvents { - return render.RowEvents{ - {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}}, - {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}}, - } -} diff --git a/internal/render/rs.go b/internal/render/rs.go index 6dd7f38f1e..85d5dbed3a 100644 --- a/internal/render/rs.go +++ b/internal/render/rs.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tview" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -20,29 +21,27 @@ type ReplicaSet struct { } // ColorerFunc colors a resource row. -func (r ReplicaSet) ColorerFunc() ColorerFunc { - return DefaultColorer +func (r ReplicaSet) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer } // Header returns a header row. -func (ReplicaSet) Header(ns string) Header { - h := Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS", VS: true}, - HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, - HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, - HeaderColumn{Name: "READY", Align: tview.AlignRight}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (ReplicaSet) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "READY", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } - - return h } // Render renders a K8s resource to screen. -func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error { +func (r ReplicaSet) Render(o interface{}, ns string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected ReplicaSet, but got %T", o) @@ -54,7 +53,7 @@ func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error { } row.ID = client.MetaFQN(rs.ObjectMeta) - row.Fields = Fields{ + row.Fields = model1.Fields{ rs.Namespace, rs.Name, computeVulScore(rs.ObjectMeta, &rs.Spec.Template.Spec), diff --git a/internal/render/rs_test.go b/internal/render/rs_test.go index 8a85dc599a..7a84cf38e3 100644 --- a/internal/render/rs_test.go +++ b/internal/render/rs_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestReplicaSetRender(t *testing.T) { c := render.ReplicaSet{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "rs"), "", &r)) assert.Equal(t, "icx/icx-db-7d4b578979", r.ID) - assert.Equal(t, render.Fields{"icx", "icx-db-7d4b578979", "0", "1", "1", "1"}, r.Fields[:6]) + assert.Equal(t, model1.Fields{"icx", "icx-db-7d4b578979", "0", "1", "1", "1"}, r.Fields[:6]) } diff --git a/internal/render/sa.go b/internal/render/sa.go index 43f7c89861..1f463a4e34 100644 --- a/internal/render/sa.go +++ b/internal/render/sa.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -19,19 +20,19 @@ type ServiceAccount struct { } // Header returns a header row. -func (ServiceAccount) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "SECRET"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (ServiceAccount) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "SECRET"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error { +func (s ServiceAccount) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected ServiceAccount, but got %T", o) @@ -43,7 +44,7 @@ func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(sa.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ sa.Namespace, sa.Name, strconv.Itoa(len(sa.Secrets)), diff --git a/internal/render/sa_test.go b/internal/render/sa_test.go index c143bc509d..932ee79895 100644 --- a/internal/render/sa_test.go +++ b/internal/render/sa_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestServiceAccountRender(t *testing.T) { c := render.ServiceAccount{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "sa"), "", &r)) assert.Equal(t, "default/blee", r.ID) - assert.Equal(t, render.Fields{"default", "blee", "2"}, r.Fields[:3]) + assert.Equal(t, model1.Fields{"default", "blee", "2"}, r.Fields[:3]) } diff --git a/internal/render/sc.go b/internal/render/sc.go index d5f5ecaaeb..f805fb1000 100644 --- a/internal/render/sc.go +++ b/internal/render/sc.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" storagev1 "k8s.io/api/storage/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -20,21 +21,21 @@ type StorageClass struct { } // Header returns a header row. -func (StorageClass) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "PROVISIONER"}, - HeaderColumn{Name: "RECLAIMPOLICY"}, - HeaderColumn{Name: "VOLUMEBINDINGMODE"}, - HeaderColumn{Name: "ALLOWVOLUMEEXPANSION"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (StorageClass) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "PROVISIONER"}, + model1.HeaderColumn{Name: "RECLAIMPOLICY"}, + model1.HeaderColumn{Name: "VOLUMEBINDINGMODE"}, + model1.HeaderColumn{Name: "ALLOWVOLUMEEXPANSION"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (s StorageClass) Render(o interface{}, ns string, r *Row) error { +func (s StorageClass) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected StorageClass, but got %T", o) @@ -46,7 +47,7 @@ func (s StorageClass) Render(o interface{}, ns string, r *Row) error { } r.ID = client.FQN(client.ClusterScope, sc.ObjectMeta.Name) - r.Fields = Fields{ + r.Fields = model1.Fields{ s.nameWithDefault(sc.ObjectMeta), sc.Provisioner, strPtrToStr((*string)(sc.ReclaimPolicy)), diff --git a/internal/render/sc_test.go b/internal/render/sc_test.go index 004e91c494..c588313610 100644 --- a/internal/render/sc_test.go +++ b/internal/render/sc_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestStorageClassRender(t *testing.T) { c := render.StorageClass{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "sc"), "", &r)) assert.Equal(t, "-/standard", r.ID) - assert.Equal(t, render.Fields{"standard (default)", "kubernetes.io/gce-pd", "Delete", "Immediate", "true"}, r.Fields[:5]) + assert.Equal(t, model1.Fields{"standard (default)", "kubernetes.io/gce-pd", "Delete", "Immediate", "true"}, r.Fields[:5]) } diff --git a/internal/render/screen_dump.go b/internal/render/screen_dump.go index 9b237d7a07..8193c612dc 100644 --- a/internal/render/screen_dump.go +++ b/internal/render/screen_dump.go @@ -9,6 +9,7 @@ import ( "path/filepath" "time" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -21,31 +22,31 @@ type ScreenDump struct { } // ColorerFunc colors a resource row. -func (ScreenDump) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (ScreenDump) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorNavajoWhite } } // Header returns a header row. -func (ScreenDump) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "DIR"}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (ScreenDump) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "DIR"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (b ScreenDump) Render(o interface{}, ns string, r *Row) error { +func (b ScreenDump) Render(o interface{}, ns string, r *model1.Row) error { f, ok := o.(FileRes) if !ok { return fmt.Errorf("expecting screendumper, but got %T", o) } r.ID = filepath.Join(f.Dir, f.File.Name()) - r.Fields = Fields{ + r.Fields = model1.Fields{ f.File.Name(), f.Dir, "", diff --git a/internal/render/screen_dump_test.go b/internal/render/screen_dump_test.go index bde7f109fd..55c82f3887 100644 --- a/internal/render/screen_dump_test.go +++ b/internal/render/screen_dump_test.go @@ -8,13 +8,14 @@ import ( "testing" "time" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestScreenDumpRender(t *testing.T) { var s render.ScreenDump - var r render.Row + var r model1.Row o := render.FileRes{ File: fileInfo{}, Dir: "fred/blee", @@ -22,7 +23,7 @@ func TestScreenDumpRender(t *testing.T) { assert.Nil(t, s.Render(o, "fred", &r)) assert.Equal(t, "fred/blee/bob", r.ID) - assert.Equal(t, render.Fields{ + assert.Equal(t, model1.Fields{ "bob", "fred/blee", "", diff --git a/internal/render/secret.go b/internal/render/secret.go new file mode 100644 index 0000000000..d1d3aad9b0 --- /dev/null +++ b/internal/render/secret.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package render + +import ( + "fmt" + "strconv" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// Secret renders a K8s Secret to screen. +type Secret struct { + Base +} + +// Header returns a header rbw. +func (Secret) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "TYPE"}, + model1.HeaderColumn{Name: "DATA"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, + } +} + +// Render renders a K8s resource to screen. +func (n Secret) Render(o interface{}, _ string, r *model1.Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("expected Secret, but got %T", o) + } + var sec v1.Secret + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sec) + if err != nil { + return err + } + + r.ID = client.FQN(sec.Namespace, sec.Name) + r.Fields = model1.Fields{ + sec.Namespace, + sec.Name, + string(sec.Type), + strconv.Itoa(len(sec.Data)), + "", + ToAge(raw.GetCreationTimestamp()), + } + + return nil +} diff --git a/internal/render/sts.go b/internal/render/sts.go index cc6b05232a..e35560ce04 100644 --- a/internal/render/sts.go +++ b/internal/render/sts.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -19,26 +20,24 @@ type StatefulSet struct { } // Header returns a header row. -func (StatefulSet) Header(ns string) Header { - h := Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VS", VS: true}, - HeaderColumn{Name: "READY"}, - HeaderColumn{Name: "SELECTOR", Wide: true}, - HeaderColumn{Name: "SERVICE"}, - HeaderColumn{Name: "CONTAINERS", Wide: true}, - HeaderColumn{Name: "IMAGES", Wide: true}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (StatefulSet) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "SERVICE"}, + model1.HeaderColumn{Name: "CONTAINERS", Wide: true}, + model1.HeaderColumn{Name: "IMAGES", Wide: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } - - return h } // Render renders a K8s resource to screen. -func (s StatefulSet) Render(o interface{}, ns string, r *Row) error { +func (s StatefulSet) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected StatefulSet, but got %T", o) @@ -50,7 +49,7 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(sts.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ sts.Namespace, sts.Name, computeVulScore(sts.ObjectMeta, &sts.Spec.Template.Spec), diff --git a/internal/render/sts_test.go b/internal/render/sts_test.go index 0070d1a818..d8a4edc8a4 100644 --- a/internal/render/sts_test.go +++ b/internal/render/sts_test.go @@ -6,15 +6,16 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestStatefulSetRender(t *testing.T) { c := render.StatefulSet{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.Nil(t, c.Render(load(t, "sts"), "", &r)) assert.Equal(t, "default/nginx-sts", r.ID) - assert.Equal(t, render.Fields{"default", "nginx-sts", "0", "4/4", "app=nginx-sts", "nginx-sts", "nginx", "k8s.gcr.io/nginx-slim:0.8", "app=nginx-sts", ""}, r.Fields[:len(r.Fields)-1]) + assert.Equal(t, model1.Fields{"default", "nginx-sts", "0", "4/4", "app=nginx-sts", "nginx-sts", "nginx", "k8s.gcr.io/nginx-slim:0.8", "app=nginx-sts", ""}, r.Fields[:len(r.Fields)-1]) } diff --git a/internal/render/subject.go b/internal/render/subject.go index b58e0ba7b4..af3c0a6173 100644 --- a/internal/render/subject.go +++ b/internal/render/subject.go @@ -6,6 +6,7 @@ package render import ( "fmt" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -17,31 +18,31 @@ type Subject struct { } // ColorerFunc colors a resource row. -func (Subject) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (Subject) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorMediumSpringGreen } } // Header returns a header row. -func (Subject) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "KIND"}, - HeaderColumn{Name: "FIRST LOCATION"}, - HeaderColumn{Name: "VALID", Wide: true}, +func (Subject) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "FIRST LOCATION"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, } } // Render renders a K8s resource to screen. -func (s Subject) Render(o interface{}, ns string, r *Row) error { +func (s Subject) Render(o interface{}, ns string, r *model1.Row) error { res, ok := o.(SubjectRes) if !ok { return fmt.Errorf("expected SubjectRes, but got %T", s) } r.ID = res.Name - r.Fields = Fields{ + r.Fields = model1.Fields{ res.Name, res.Kind, res.FirstLocation, diff --git a/internal/render/svc.go b/internal/render/svc.go index 2f3cb30cf8..73081cadf0 100644 --- a/internal/render/svc.go +++ b/internal/render/svc.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -21,23 +22,23 @@ type Service struct { } // Header returns a header row. -func (Service) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "TYPE"}, - HeaderColumn{Name: "CLUSTER-IP"}, - HeaderColumn{Name: "EXTERNAL-IP"}, - HeaderColumn{Name: "SELECTOR", Wide: true}, - HeaderColumn{Name: "PORTS", Wide: false}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Service) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "TYPE"}, + model1.HeaderColumn{Name: "CLUSTER-IP"}, + model1.HeaderColumn{Name: "EXTERNAL-IP"}, + model1.HeaderColumn{Name: "SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "PORTS", Wide: false}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (s Service) Render(o interface{}, ns string, r *Row) error { +func (s Service) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expected Service, but got %T", o) @@ -49,7 +50,7 @@ func (s Service) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(svc.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ svc.Namespace, svc.ObjectMeta.Name, string(svc.Spec.Type), diff --git a/internal/render/svc_test.go b/internal/render/svc_test.go index e0b70471ac..6a1ea62fe4 100644 --- a/internal/render/svc_test.go +++ b/internal/render/svc_test.go @@ -6,22 +6,23 @@ package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestServiceRender(t *testing.T) { c := render.Service{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "svc"), "", &r)) assert.Equal(t, "default/dictionary1", r.ID) - assert.Equal(t, render.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "", "app=dictionary1", "http:4001►0"}, r.Fields[:7]) + assert.Equal(t, model1.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "", "app=dictionary1", "http:4001►0"}, r.Fields[:7]) } func BenchmarkSvcRender(b *testing.B) { var svc render.Service - r := render.NewRow(4) + r := model1.NewRow(4) s := load(b, "svc") b.ResetTimer() b.ReportAllocs() diff --git a/internal/render/table_data.go b/internal/render/table_data.go deleted file mode 100644 index aaf96cf227..0000000000 --- a/internal/render/table_data.go +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package render - -import ( - "sync" - - "github.com/derailed/k9s/internal/client" -) - -// TableData tracks a K8s resource for tabular display. -type TableData struct { - Header Header - RowEvents RowEvents - Namespace string - mx sync.RWMutex -} - -// NewTableData returns a new table. -func NewTableData() *TableData { - return &TableData{} -} - -// Empty checks if there are no entries. -func (t *TableData) Empty() bool { - t.mx.RLock() - defer t.mx.RUnlock() - - return len(t.RowEvents) == 0 -} - -// Count returns the number of entries. -func (t *TableData) Count() int { - t.mx.RLock() - defer t.mx.RUnlock() - - return len(t.RowEvents) -} - -// IndexOfHeader return the index of the header. -func (t *TableData) IndexOfHeader(h string) int { - return t.Header.IndexOf(h, false) -} - -// Labelize prints out specific label columns. -func (t *TableData) Labelize(labels []string) *TableData { - labelCol := t.Header.IndexOf("LABELS", true) - cols := []int{0, 1} - if client.IsNamespaced(t.Namespace) { - cols = cols[1:] - } - data := TableData{ - Namespace: t.Namespace, - Header: t.Header.Labelize(cols, labelCol, t.RowEvents), - } - data.RowEvents = t.RowEvents.Labelize(cols, labelCol, labels) - - return &data -} - -// Customize returns a new model with customized column layout. -func (t *TableData) Customize(cols []string, wide bool) *TableData { - res := TableData{ - Namespace: t.Namespace, - Header: t.Header.Customize(cols, wide), - } - ids := t.Header.MapIndices(cols, wide) - res.RowEvents = t.RowEvents.Customize(ids) - - return &res -} - -// Clear clears out the entire table. -func (t *TableData) Clear() { - t.Header, t.RowEvents = Header{}, RowEvents{} -} - -// Clone returns a copy of the table. -func (t *TableData) Clone() *TableData { - return &TableData{ - Header: t.Header.Clone(), - RowEvents: t.RowEvents.Clone(), - Namespace: t.Namespace, - } -} - -// SetHeader sets table header. -func (t *TableData) SetHeader(ns string, h Header) { - t.Namespace, t.Header = ns, h -} - -// Update computes row deltas and update the table data. -func (t *TableData) Update(rows Rows) { - empty := t.Empty() - kk := make(map[string]struct{}, len(rows)) - t.mx.Lock() - { - var blankDelta DeltaRow - for _, row := range rows { - kk[row.ID] = struct{}{} - if empty { - t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row)) - continue - } - if index, ok := t.RowEvents.FindIndex(row.ID); ok { - delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header) - if delta.IsBlank() { - t.RowEvents[index].Kind, t.RowEvents[index].Deltas = EventUnchanged, blankDelta - t.RowEvents[index].Row = row - } else { - t.RowEvents[index] = NewRowEventWithDeltas(row, delta) - } - continue - } - t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row)) - } - } - t.mx.Unlock() - - if !empty { - t.Delete(kk) - } -} - -// Delete removes items in cache that are no longer valid. -func (t *TableData) Delete(newKeys map[string]struct{}) { - t.mx.Lock() - { - var victims []string - for _, re := range t.RowEvents { - if _, ok := newKeys[re.Row.ID]; !ok { - victims = append(victims, re.Row.ID) - } - } - for _, id := range victims { - t.RowEvents = t.RowEvents.Delete(id) - } - } - t.mx.Unlock() -} - -// Diff checks if two tables are equal. -func (t *TableData) Diff(t2 *TableData) bool { - if t2 == nil || t.Namespace != t2.Namespace || t.Header.Diff(t2.Header) { - return true - } - - return t.RowEvents.Diff(t2.RowEvents, t.Header.IndexOf("AGE", true)) -} diff --git a/internal/render/table_data_test.go b/internal/render/table_data_test.go deleted file mode 100644 index 0ddb7c1516..0000000000 --- a/internal/render/table_data_test.go +++ /dev/null @@ -1,396 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of K9s - -package render_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/render" - "github.com/stretchr/testify/assert" -) - -func TestTableDataCustomize(t *testing.T) { - uu := map[string]struct { - t1, e *render.TableData - cols []string - wide bool - }{ - "same": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - cols: []string{"A", "B", "C"}, - e: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - }, - "wide-col": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - cols: []string{"A", "B", "C"}, - e: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: false}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - }, - "wide": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - wide: true, - cols: []string{"A", "C"}, - e: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "B", Wide: true}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "3", "2"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "3", "2"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "3", "2"}}}, - }, - }, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.t1.Customize(u.cols, u.wide)) - }) - } -} - -func TestTableDataDiff(t *testing.T) { - uu := map[string]struct { - t1, t2 *render.TableData - e bool - }{ - "empty": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - e: true, - }, - "same": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - t2: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - }, - "ns-diff": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - t2: &render.TableData{ - Namespace: "blee", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - e: true, - }, - "header-diff": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "D"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - t2: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - e: true, - }, - "row-diff": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - t2: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"100", "2", "3"}}}, - }, - }, - e: true, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.t1.Diff(u.t2)) - }) - } -} - -func TestTableDataUpdate(t *testing.T) { - uu := map[string]struct { - re render.RowEvents - rr render.Rows - e render.RowEvents - }{ - "no-change": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - rr: render.Rows{ - render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}, - render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}, - }, - e: render.RowEvents{ - {Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "add": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - rr: render.Rows{ - render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}, - render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}, - render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}, - }, - e: render.RowEvents{ - {Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - {Kind: render.EventAdd, Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "delete": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - rr: render.Rows{ - render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}, - }, - e: render.RowEvents{ - {Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "update": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - rr: render.Rows{ - render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}}, - render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}, - render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}, - }, - e: render.RowEvents{ - { - Kind: render.EventUpdate, - Row: render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}}, - Deltas: render.DeltaRow{"1", "", ""}, - }, - {Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - } - - var table render.TableData - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - table.RowEvents = u.re - table.Update(u.rr) - assert.Equal(t, u.e, table.RowEvents) - }) - } -} - -func TestTableDataDelete(t *testing.T) { - uu := map[string]struct { - re render.RowEvents - kk map[string]struct{} - e render.RowEvents - }{ - "ordered": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - kk: map[string]struct{}{"A": {}, "C": {}}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "unordered": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - {Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}}, - }, - kk: map[string]struct{}{"C": {}, "A": {}}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - } - - var table render.TableData - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - table.RowEvents = u.re - table.Delete(u.kk) - assert.Equal(t, u.e, table.RowEvents) - }) - } -} diff --git a/internal/render/testdata/p1.json b/internal/render/testdata/p1.json new file mode 100644 index 0000000000..ea8d8dad73 --- /dev/null +++ b/internal/render/testdata/p1.json @@ -0,0 +1,146 @@ +{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/restartedAt": "2019-12-31T12:26:47-07:00" + }, + "creationTimestamp": "2019-12-31T19:27:22Z", + "generateName": "nginx-7fb78fb6d8-", + "labels": { + "app": "nginx", + "pod-template-hash": "7fb78fb6d8" + }, + "name": "nginx-7fb78fb6d8-2w75j", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "nginx-7fb78fb6d8", + "uid": "7ccd0600-2c03-11ea-883f-42010a800044" + } + ], + "resourceVersion": "87290191", + "selfLink": "/api/v1/namespaces/default/pods/nginx-7fb78fb6d8-2w75j", + "uid": "91bb1cf2-2c03-11ea-883f-42010a800044" + }, + "spec": { + "containers": [ + { + "image": "k8s.gcr.io/nginx-slim:0.8", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "200m", + "memory": "20Mi" + }, + "requests": { + "cpu": "200m", + "memory": "20Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-dsl46", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "gke-k9s-default-pool-0fa2fb89-lbtf", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-dsl46", + "secret": { + "defaultMode": 420, + "secretName": "default-token-dsl46" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2019-12-31T19:27:23Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-12-31T19:27:25Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-12-31T19:27:25Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-12-31T19:27:22Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://90e0abf7a779dd76d36038883312baed57a8351428a1d6340df3cff698f51809", + "image": "k8s.gcr.io/nginx-slim:0.8", + "imageID": "docker-pullable://k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "state": { + "running": { + "startedAt": "2019-12-31T19:27:24Z" + } + } + } + ], + "hostIP": "10.128.0.15", + "phase": "Running", + "podIP": "10.44.0.229", + "qosClass": "Guaranteed", + "startTime": "2019-12-31T19:27:23Z" + } +} \ No newline at end of file diff --git a/internal/render/workload.go b/internal/render/workload.go index 250c52ecff..7a3a2645ec 100644 --- a/internal/render/workload.go +++ b/internal/render/workload.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -19,17 +20,17 @@ type Workload struct { } // ColorerFunc colors a resource row. -func (n Workload) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - c := DefaultColorer(ns, h, re) +func (n Workload) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) - statusCol := h.IndexOf("STATUS", true) - if statusCol == -1 { + idx, ok := h.IndexOf("STATUS", true) + if !ok { return c } - status := strings.TrimSpace(re.Row.Fields[statusCol]) + status := strings.TrimSpace(re.Row.Fields[idx]) if status == "DEGRADED" { - c = PendingColor + c = model1.PendingColor } return c @@ -37,27 +38,27 @@ func (n Workload) ColorerFunc() ColorerFunc { } // Header returns a header rbw. -func (Workload) Header(string) Header { - return Header{ - HeaderColumn{Name: "KIND"}, - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "READY"}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Workload) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (n Workload) Render(o interface{}, _ string, r *Row) error { +func (n Workload) Render(o interface{}, _ string, r *model1.Row) error { res, ok := o.(*WorkloadRes) if !ok { return fmt.Errorf("expected allRes but got %T", o) } r.ID = fmt.Sprintf("%s|%s|%s", res.Row.Cells[0].(string), res.Row.Cells[1].(string), res.Row.Cells[2].(string)) - r.Fields = Fields{ + r.Fields = model1.Fields{ res.Row.Cells[0].(string), res.Row.Cells[1].(string), res.Row.Cells[2].(string), diff --git a/internal/ui/action.go b/internal/ui/action.go index 3e4b8efd2a..f270e67ba7 100644 --- a/internal/ui/action.go +++ b/internal/ui/action.go @@ -5,6 +5,7 @@ package ui import ( "sort" + "sync" "github.com/derailed/k9s/internal/model" "github.com/derailed/tcell/v2" @@ -12,9 +13,13 @@ import ( ) type ( + // RangeFn represents a range iteration callback. + RangeFn func(tcell.Key, KeyAction) + // ActionHandler handles a keyboard command. ActionHandler func(*tcell.EventKey) *tcell.EventKey + // ActionOpts tracks various action options. ActionOpts struct { Visible bool Shared bool @@ -30,8 +35,14 @@ type ( Opts ActionOpts } + // KeyMap tracks key to action mappings. + KeyMap map[tcell.Key]KeyAction + // KeyActions tracks mappings between keystrokes and actions. - KeyActions map[tcell.Key]KeyAction + KeyActions struct { + actions KeyMap + mx sync.RWMutex + } ) // NewKeyAction returns a new keyboard action. @@ -58,53 +69,134 @@ func NewKeyActionWithOpts(d string, a ActionHandler, opts ActionOpts) KeyAction } } -func (a KeyActions) Reset(aa KeyActions) { +// NewKeyActions returns a new instance. +func NewKeyActions() *KeyActions { + return &KeyActions{ + actions: make(map[tcell.Key]KeyAction), + } +} + +// NewKeyActionsFromMap construct actions from key map. +func NewKeyActionsFromMap(mm KeyMap) *KeyActions { + return &KeyActions{actions: mm} +} + +// Get fetches an action given a key. +func (a *KeyActions) Get(key tcell.Key) (KeyAction, bool) { + a.mx.RLock() + defer a.mx.RUnlock() + + v, ok := a.actions[key] + + return v, ok +} + +// Len returns action mapping count. +func (a *KeyActions) Len() int { + a.mx.RLock() + defer a.mx.RUnlock() + + return len(a.actions) +} + +// Reset clears out actions. +func (a *KeyActions) Reset(aa *KeyActions) { a.Clear() - a.Add(aa) + a.Merge(aa) } -// Add sets up keyboard action listener. -func (a KeyActions) Add(aa KeyActions) { +// Range ranges over all actions and triggers a given function. +func (a *KeyActions) Range(f RangeFn) { + var km KeyMap + a.mx.RLock() + { + km = a.actions + } + a.mx.RUnlock() + + for k, v := range km { + f(k, v) + } +} + +// Add adds a new key action. +func (a *KeyActions) Add(k tcell.Key, ka KeyAction) { + a.mx.Lock() + defer a.mx.Unlock() + + a.actions[k] = ka +} + +// Bulk bulk insert key mappings. +func (a *KeyActions) Bulk(aa KeyMap) { + a.mx.Lock() + defer a.mx.Unlock() + for k, v := range aa { - a[k] = v + a.actions[k] = v + } +} + +// Merge merges given actions into existing set. +func (a *KeyActions) Merge(aa *KeyActions) { + a.mx.Lock() + defer a.mx.Unlock() + + for k, v := range aa.actions { + a.actions[k] = v } } // Clear remove all actions. -func (a KeyActions) Clear() { - for k := range a { - delete(a, k) +func (a *KeyActions) Clear() { + a.mx.Lock() + defer a.mx.Unlock() + + for k := range a.actions { + delete(a.actions, k) } } // ClearDanger remove all dangerous actions. -func (a KeyActions) ClearDanger() { - for k, v := range a { +func (a *KeyActions) ClearDanger() { + a.mx.Lock() + defer a.mx.Unlock() + + for k, v := range a.actions { if v.Opts.Dangerous { - delete(a, k) + delete(a.actions, k) } } } // Set replace actions with new ones. -func (a KeyActions) Set(aa KeyActions) { - for k, v := range aa { - a[k] = v +func (a *KeyActions) Set(aa *KeyActions) { + a.mx.Lock() + defer a.mx.Unlock() + + for k, v := range aa.actions { + a.actions[k] = v } } // Delete deletes actions by the given keys. -func (a KeyActions) Delete(kk ...tcell.Key) { +func (a *KeyActions) Delete(kk ...tcell.Key) { + a.mx.Lock() + defer a.mx.Unlock() + for _, k := range kk { - delete(a, k) + delete(a.actions, k) } } // Hints returns a collection of hints. -func (a KeyActions) Hints() model.MenuHints { - kk := make([]int, 0, len(a)) - for k := range a { - if !a[k].Opts.Shared { +func (a *KeyActions) Hints() model.MenuHints { + a.mx.RLock() + defer a.mx.RUnlock() + + kk := make([]int, 0, len(a.actions)) + for k := range a.actions { + if !a.actions[k].Opts.Shared { kk = append(kk, int(k)) } } @@ -116,13 +208,14 @@ func (a KeyActions) Hints() model.MenuHints { hh = append(hh, model.MenuHint{ Mnemonic: name, - Description: a[tcell.Key(k)].Description, - Visible: a[tcell.Key(k)].Opts.Visible, + Description: a.actions[tcell.Key(k)].Description, + Visible: a.actions[tcell.Key(k)].Opts.Visible, }, ) } else { log.Error().Msgf("Unable to locate KeyName for %#v", k) } } + return hh } diff --git a/internal/ui/action_test.go b/internal/ui/action_test.go index de031ebd88..60733aa8e0 100644 --- a/internal/ui/action_test.go +++ b/internal/ui/action_test.go @@ -12,11 +12,11 @@ import ( ) func TestKeyActionsHints(t *testing.T) { - kk := ui.KeyActions{ + kk := ui.NewKeyActionsFromMap(ui.KeyMap{ ui.KeyF: ui.NewKeyAction("fred", nil, true), ui.KeyB: ui.NewKeyAction("blee", nil, true), ui.KeyZ: ui.NewKeyAction("zorg", nil, false), - } + }) hh := kk.Hints() diff --git a/internal/ui/app.go b/internal/ui/app.go index 4a3b3e274e..e0c261ee64 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -22,7 +22,7 @@ type App struct { Main *Pages flash *model.Flash - actions KeyActions + actions *KeyActions views map[string]tview.Primitive cmdBuff *model.FishBuff running bool @@ -33,7 +33,7 @@ type App struct { func NewApp(cfg *config.Config, context string) *App { a := App{ Application: tview.NewApplication(), - actions: make(KeyActions), + actions: NewKeyActions(), Configurator: Configurator{Config: cfg, Styles: config.NewStyles()}, Main: NewPages(), flash: model.NewFlash(model.DefaultFlashDelay), @@ -139,13 +139,14 @@ func (a *App) Conn() client.Connection { } func (a *App) bindKeys() { - a.actions = KeyActions{ + a.actions = NewKeyActionsFromMap(KeyMap{ KeyColon: NewKeyAction("Cmd", a.activateCmd, false), tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false), + tcell.KeyCtrlP: NewKeyAction("Persist", a.saveCmd, false), tcell.KeyCtrlC: NewKeyAction("Quit", a.quitCmd, false), tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false), tcell.KeyCtrlQ: NewSharedKeyAction("Clear Filter", a.clearCmd, false), - } + }) } // BailOut exits the application. @@ -156,6 +157,7 @@ func (a *App) BailOut() { // ResetPrompt reset the prompt model and marks buffer as active. func (a *App) ResetPrompt(m PromptModel) { + m.ClearText(false) a.Prompt().SetModel(m) a.SetFocus(a.Prompt()) m.SetActive(true) @@ -166,6 +168,15 @@ func (a *App) ResetCmd() { a.cmdBuff.Reset() } +func (a *App) saveCmd(evt *tcell.EventKey) *tcell.EventKey { + if err := a.Config.Save(true); err != nil { + a.Flash().Err(err) + } + a.Flash().Info("current context config saved") + + return nil +} + // ActivateCmd toggle command mode. func (a *App) ActivateCmd(b bool) { a.cmdBuff.SetActive(b) @@ -206,20 +217,17 @@ func (a *App) InCmdMode() bool { // HasAction checks if key matches a registered binding. func (a *App) HasAction(key tcell.Key) (KeyAction, bool) { - act, ok := a.actions[key] - return act, ok + return a.actions.Get(key) } // GetActions returns a collection of actions. -func (a *App) GetActions() KeyActions { +func (a *App) GetActions() *KeyActions { return a.actions } // AddActions returns the application actions. -func (a *App) AddActions(aa KeyActions) { - for k, v := range aa { - a.actions[k] = v - } +func (a *App) AddActions(aa *KeyActions) { + a.actions.Merge(aa) } // Views return the application root views. diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index 36d5c6ff02..47f96fb401 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -54,9 +54,9 @@ func TestAppGetActions(t *testing.T) { a := ui.NewApp(mock.NewMockConfig(), "") a.Init() - a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}}) + a.GetActions().Add(ui.KeyZ, ui.KeyAction{Description: "zorg"}) - assert.Equal(t, 6, len(a.GetActions())) + assert.Equal(t, 7, a.GetActions().Len()) } func TestAppViews(t *testing.T) { diff --git a/internal/ui/config.go b/internal/ui/config.go index 70435617d4..487a1cc927 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -6,13 +6,14 @@ package ui import ( "context" "errors" + "io/fs" "os" "path/filepath" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/render" "github.com/fsnotify/fsnotify" "github.com/rs/zerolog/log" ) @@ -91,7 +92,7 @@ func (c *Configurator) RefreshCustomViews() error { // SkinsDirWatcher watches for skin directory file changes. func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) error { - if _, err := os.Stat(config.AppSkinsDir); os.IsNotExist(err) { + if _, err := os.Stat(config.AppSkinsDir); errors.Is(err, fs.ErrNotExist) { return err } w, err := fsnotify.NewWatcher() @@ -139,7 +140,7 @@ func (c *Configurator) ConfigWatcher(ctx context.Context, s synchronizer) error if evt.Has(fsnotify.Create) || evt.Has(fsnotify.Write) { log.Debug().Msgf("ConfigWatcher file changed: %s", evt.Name) if evt.Name == config.AppConfigFile { - if err := c.Config.Load(evt.Name); err != nil { + if err := c.Config.Load(evt.Name, false); err != nil { log.Error().Err(err).Msgf("k9s config reload failed") s.Flash().Warn("k9s config reload failed. Check k9s logs!") s.Logo().Warn("K9s config reload failed!") @@ -190,14 +191,14 @@ func (c *Configurator) activeSkin() (string, bool) { } if ct, err := c.Config.K9s.ActiveContext(); err == nil && ct.Skin != "" { - if _, err := os.Stat(config.SkinFileFromName(ct.Skin)); !os.IsNotExist(err) { + if _, err := os.Stat(config.SkinFileFromName(ct.Skin)); err == nil { skin = ct.Skin log.Debug().Msgf("[Skin] Loading context skin (%q) from %q", skin, c.Config.K9s.ActiveContextName()) } } if sk := c.Config.K9s.UI.Skin; skin == "" && sk != "" { - if _, err := os.Stat(config.SkinFileFromName(sk)); !os.IsNotExist(err) { + if _, err := os.Stat(config.SkinFileFromName(sk)); err == nil { skin = sk log.Debug().Msgf("[Skin] Loading global skin (%q)", skin) } @@ -272,12 +273,12 @@ func (c *Configurator) updateStyles(f string) { } c.Styles.Update() - render.ModColor = c.Styles.Frame().Status.ModifyColor.Color() - render.AddColor = c.Styles.Frame().Status.AddColor.Color() - render.ErrColor = c.Styles.Frame().Status.ErrorColor.Color() - render.StdColor = c.Styles.Frame().Status.NewColor.Color() - render.PendingColor = c.Styles.Frame().Status.PendingColor.Color() - render.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color() - render.KillColor = c.Styles.Frame().Status.KillColor.Color() - render.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color() + model1.ModColor = c.Styles.Frame().Status.ModifyColor.Color() + model1.AddColor = c.Styles.Frame().Status.AddColor.Color() + model1.ErrColor = c.Styles.Frame().Status.ErrorColor.Color() + model1.StdColor = c.Styles.Frame().Status.NewColor.Color() + model1.PendingColor = c.Styles.Frame().Status.PendingColor.Color() + model1.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color() + model1.KillColor = c.Styles.Frame().Status.KillColor.Color() + model1.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color() } diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go index a38d26a1bd..3e95694989 100644 --- a/internal/ui/config_test.go +++ b/internal/ui/config_test.go @@ -13,7 +13,7 @@ import ( "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" @@ -47,8 +47,8 @@ func TestSkinnedContext(t *testing.T) { cfg.Config.K9s.UI = config.UI{Skin: "black-and-wtf"} cfg.RefreshStyles(newMockSynchronizer()) assert.True(t, cfg.HasSkin()) - assert.Equal(t, tcell.ColorGhostWhite.TrueColor(), render.StdColor) - assert.Equal(t, tcell.ColorWhiteSmoke.TrueColor(), render.ErrColor) + assert.Equal(t, tcell.ColorGhostWhite.TrueColor(), model1.StdColor) + assert.Equal(t, tcell.ColorWhiteSmoke.TrueColor(), model1.ErrColor) } func TestBenchConfig(t *testing.T) { diff --git a/internal/ui/menu_test.go b/internal/ui/menu_test.go index 599e2d2c62..9a4788035c 100644 --- a/internal/ui/menu_test.go +++ b/internal/ui/menu_test.go @@ -27,16 +27,16 @@ func TestNewMenu(t *testing.T) { func TestActionHints(t *testing.T) { uu := map[string]struct { - aa ui.KeyActions + aa *ui.KeyActions e model.MenuHints }{ "a": { - aa: ui.KeyActions{ + aa: ui.NewKeyActionsFromMap(ui.KeyMap{ ui.KeyB: ui.NewKeyAction("bleeB", nil, true), ui.KeyA: ui.NewKeyAction("bleeA", nil, true), ui.Key0: ui.NewKeyAction("zero", nil, true), ui.Key1: ui.NewKeyAction("one", nil, false), - }, + }), e: model.MenuHints{ {Mnemonic: "0", Description: "zero", Visible: true}, {Mnemonic: "1", Description: "one", Visible: false}, diff --git a/internal/ui/padding.go b/internal/ui/padding.go index b57cdb1f72..25cacdc541 100644 --- a/internal/ui/padding.go +++ b/internal/ui/padding.go @@ -7,6 +7,7 @@ import ( "strings" "unicode" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" ) @@ -14,26 +15,27 @@ import ( type MaxyPad []int // ComputeMaxColumns figures out column max size and necessary padding. -func ComputeMaxColumns(pads MaxyPad, sortColName string, header render.Header, ee render.RowEvents) { +func ComputeMaxColumns(pads MaxyPad, sortColName string, t *model1.TableData) { const colPadding = 1 - for index, h := range header { - pads[index] = len(h.Name) - if h.Name == sortColName { - pads[index] = len(h.Name) + 2 + for i, n := range t.ColumnNames(true) { + pads[i] = len(n) + if n == sortColName { + pads[i] += 2 } } var row int - for _, e := range ee { - for index, field := range e.Row.Fields { + t.RowsRange(func(_ int, re model1.RowEvent) bool { + for index, field := range re.Row.Fields { width := len(field) + colPadding if index < len(pads) && width > pads[index] { pads[index] = width } } row++ - } + return true + }) } // IsASCII checks if table cell has all ascii characters. diff --git a/internal/ui/padding_test.go b/internal/ui/padding_test.go index 51a0bcde43..0dbcb87a38 100644 --- a/internal/ui/padding_test.go +++ b/internal/ui/padding_test.go @@ -6,70 +6,74 @@ package ui import ( "testing" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/stretchr/testify/assert" ) func TestMaxColumn(t *testing.T) { uu := map[string]struct { - t *render.TableData + t *model1.TableData s string e MaxyPad }{ "ascii col 0": { - &render.TableData{ - Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}}, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"hello", "world"}, + model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"hello", "world"}, }, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"yo", "mama"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"yo", "mama"}, }, }, - }, - }, + ), + ), "A", MaxyPad{6, 6}, }, "ascii col 1": { - &render.TableData{ - Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}}, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"hello", "world"}, + model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"hello", "world"}, }, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"yo", "mama"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"yo", "mama"}, }, }, - }, - }, + ), + ), "B", MaxyPad{6, 6}, }, "non_ascii": { - &render.TableData{ - Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}}, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"Hello World lord of ipsums 😅", "world"}, + model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"Hello World lord of ipsums 😅", "world"}, }, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"o", "mama"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"o", "mama"}, }, }, - }, - }, + ), + ), "A", MaxyPad{32, 6}, }, @@ -78,8 +82,8 @@ func TestMaxColumn(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - pads := make(MaxyPad, len(u.t.Header)) - ComputeMaxColumns(pads, u.s, u.t.Header, u.t.RowEvents) + pads := make(MaxyPad, u.t.HeaderCount()) + ComputeMaxColumns(pads, u.s, u.t) assert.Equal(t, u.e, pads) }) } @@ -119,27 +123,28 @@ func TestPad(t *testing.T) { } func BenchmarkMaxColumn(b *testing.B) { - table := render.TableData{ - Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}}, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"hello", "world"}, + table := model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"hello", "world"}, }, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"yo", "mama"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"yo", "mama"}, }, }, - }, - } + ), + ) - pads := make(MaxyPad, len(table.Header)) + pads := make(MaxyPad, table.HeaderCount()) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - ComputeMaxColumns(pads, "A", table.Header, table.RowEvents) + ComputeMaxColumns(pads, "A", table) } } diff --git a/internal/ui/select_table.go b/internal/ui/select_table.go index 25f33c0f19..d1df36c480 100644 --- a/internal/ui/select_table.go +++ b/internal/ui/select_table.go @@ -107,7 +107,7 @@ func (s *SelectTable) SelectRow(r, c int, broadcast bool) { if !broadcast { s.SetSelectionChangedFunc(nil) } - if c := s.model.Count(); c > 0 && r-1 > c { + if c := s.model.RowCount(); c > 0 && r-1 > c { r = c + 1 } defer s.SetSelectionChangedFunc(s.selectionChanged) diff --git a/internal/ui/table.go b/internal/ui/table.go index 66b0450308..0af73164ec 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -5,15 +5,14 @@ package ui import ( "context" - "errors" "fmt" - "strings" + "sync" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/vul" "github.com/derailed/tcell/v2" @@ -27,10 +26,10 @@ const maxTruncate = 50 type ( // ColorerFunc represents a row colorer. - ColorerFunc func(ns string, evt render.RowEvent) tcell.Color + ColorerFunc func(ns string, evt model1.RowEvent) tcell.Color // DecorateFunc represents a row decorator. - DecorateFunc func(*render.TableData) + DecorateFunc func(*model1.TableData) // SelectedRowFunc a table selection callback. SelectedRowFunc func(r int) @@ -39,21 +38,22 @@ type ( // Table represents tabular data. type Table struct { gvr client.GVR - sortCol SortColumn + sortCol model1.SortColumn manualSort bool - header render.Header Path string Extras string *SelectTable - actions KeyActions + actions *KeyActions cmdBuff *model.FishBuff styles *config.Styles viewSetting *config.ViewSetting - colorerFn render.ColorerFunc + colorerFn model1.ColorerFunc decorateFn DecorateFunc wide bool toast bool hasMetrics bool + ctx context.Context + mx sync.RWMutex } // NewTable returns a new table view. @@ -65,12 +65,69 @@ func NewTable(gvr client.GVR) *Table { marks: make(map[string]struct{}), }, gvr: gvr, - actions: make(KeyActions), + actions: NewKeyActions(), cmdBuff: model.NewFishBuff('/', model.FilterBuffer), - sortCol: SortColumn{asc: true}, + sortCol: model1.SortColumn{ASC: true}, } } +func (t *Table) setSortCol(sc model1.SortColumn) { + t.mx.Lock() + defer t.mx.Unlock() + + t.sortCol = sc +} + +func (t *Table) toggleSortCol() { + t.mx.Lock() + defer t.mx.Unlock() + + t.sortCol.ASC = !t.sortCol.ASC +} + +func (t *Table) getSortCol() model1.SortColumn { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.sortCol +} + +func (t *Table) setMSort(b bool) { + t.mx.Lock() + defer t.mx.Unlock() + + t.manualSort = b +} + +func (t *Table) getMSort() bool { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.manualSort +} + +func (t *Table) setVs(vs *config.ViewSetting) { + t.mx.Lock() + defer t.mx.Unlock() + + t.viewSetting = vs +} + +func (t *Table) getVs() *config.ViewSetting { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.viewSetting +} + +func (t *Table) GetContext() context.Context { + return t.ctx +} + +func (t *Table) SetContext(ctx context.Context) { + t.ctx = ctx +} + // Init initializes the component. func (t *Table) Init(ctx context.Context) { t.SetFixed(1, 0) @@ -92,8 +149,9 @@ func (t *Table) Init(ctx context.Context) { func (t *Table) GVR() client.GVR { return t.gvr } // ViewSettingsChanged notifies listener the view configuration changed. -func (t *Table) ViewSettingsChanged(settings config.ViewSetting) { - t.viewSetting, t.manualSort = &settings, false +func (t *Table) ViewSettingsChanged(vs config.ViewSetting) { + t.setVs(&vs) + t.setMSort(false) t.Refresh() } @@ -129,7 +187,7 @@ func (t *Table) ToggleWide() { } // Actions returns active menu bindings. -func (t *Table) Actions() KeyActions { +func (t *Table) Actions() *KeyActions { return t.actions } @@ -171,7 +229,7 @@ func (t *Table) ExtraHints() map[string]string { } // GetFilteredData fetch filtered tabular data. -func (t *Table) GetFilteredData() *render.TableData { +func (t *Table) GetFilteredData() *model1.TableData { return t.filtered(t.GetModel().Peek()) } @@ -181,67 +239,51 @@ func (t *Table) SetDecorateFn(f DecorateFunc) { } // SetColorerFn specifies the default colorer. -func (t *Table) SetColorerFn(f render.ColorerFunc) { +func (t *Table) SetColorerFn(f model1.ColorerFunc) { t.colorerFn = f } // SetSortCol sets in sort column index and order. func (t *Table) SetSortCol(name string, asc bool) { - t.sortCol.name, t.sortCol.asc = name, asc + t.setSortCol(model1.SortColumn{Name: name, ASC: asc}) } // Update table content. -func (t *Table) Update(data *render.TableData, hasMetrics bool) { - t.header = data.Header +func (t *Table) Update(data *model1.TableData, hasMetrics bool) *model1.TableData { if t.decorateFn != nil { t.decorateFn(data) } t.hasMetrics = hasMetrics - t.doUpdate(t.filtered(data)) - t.UpdateTitle() + + return t.doUpdate(t.filtered(data)) } -func (t *Table) doUpdate(data *render.TableData) { - if client.IsAllNamespaces(data.Namespace) { - t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false) +func (t *Table) doUpdate(data *model1.TableData) *model1.TableData { + if client.IsAllNamespaces(data.GetNamespace()) { + t.actions.Add( + KeyShiftP, + NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false), + ) } else { t.actions.Delete(KeyShiftP) } - cols := t.header.Columns(t.wide) - if t.viewSetting != nil && len(t.viewSetting.Columns) > 0 { - cols = t.viewSetting.Columns - } - custData := data.Customize(cols, t.wide) - // The sortColumn settings in the configuration file are only used - // if the sortCol has not been modified manually - if t.viewSetting != nil && t.viewSetting.SortColumn != "" && !t.manualSort { - tokens := strings.Split(t.viewSetting.SortColumn, ":") - if custData.Header.IndexOf(tokens[0], false) >= 0 && !t.manualSort { - t.sortCol.name, t.sortCol.asc = tokens[0], true - if len(tokens) == 2 && tokens[1] == "desc" { - t.sortCol.asc = false - } - } - } + cdata, sortCol := data.Customize(t.getVs(), t.getSortCol(), t.getMSort(), true) + t.setSortCol(sortCol) - if t.sortCol.name == "" && client.IsAllNamespaces(data.Namespace) { - t.sortCol.name = "NAMESPACE" - } - if t.sortCol.name == "" || (t.sortCol.name == "NAMESPACE" && !client.IsAllNamespaces(data.Namespace)) && len(custData.Header) > 0 { - if idx := custData.Header.IndexOf("NAME", false); idx >= 0 { - t.sortCol.name = custData.Header[idx].Name - } else { - t.sortCol.name = custData.Header[0].Name - } - } + return cdata +} +func (t *Table) UpdateUI(cdata, data *model1.TableData) { t.Clear() fg := t.styles.Table().Header.FgColor.Color() bg := t.styles.Table().Header.BgColor.Color() var col int - for _, h := range custData.Header { + for _, h := range cdata.Header() { + if !t.wide && h.Wide { + continue + } if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() { continue } @@ -258,38 +300,42 @@ func (t *Table) doUpdate(data *render.TableData) { c.SetTextColor(fg) col++ } - colIndex := custData.Header.IndexOf(t.sortCol.name, false) - custData.RowEvents.Sort( - custData.Namespace, - colIndex, - custData.Header.IsTimeCol(colIndex), - custData.Header.IsMetricsCol(colIndex), - custData.Header.IsCapacityCol(colIndex), - t.sortCol.asc, - ) - - pads := make(MaxyPad, len(custData.Header)) - ComputeMaxColumns(pads, t.sortCol.name, custData.Header, custData.RowEvents) - for row, re := range custData.RowEvents { - idx, _ := data.RowEvents.FindIndex(re.Row.ID) - t.buildRow(row+1, re, data.RowEvents[idx], custData.Header, pads) - } + cdata.Sort(t.getSortCol()) + + pads := make(MaxyPad, cdata.HeaderCount()) + ComputeMaxColumns(pads, t.getSortCol().Name, cdata) + cdata.RowsRange(func(row int, re model1.RowEvent) bool { + ore, ok := data.FindRow(re.Row.ID) + if !ok { + log.Error().Msgf("unable to find original re: %q", re.Row.ID) + return true + } + t.buildRow(row+1, re, ore, cdata.Header(), pads) + + return true + }) + t.updateSelection(true) + t.UpdateTitle() } -func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads MaxyPad) { - color := render.DefaultColorer +func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads MaxyPad) { + color := model1.DefaultColorer if t.colorerFn != nil { color = t.colorerFn } marked := t.IsMarked(re.Row.ID) var col int + ns := t.GetModel().GetNamespace() for c, field := range re.Row.Fields { if c >= len(h) { log.Error().Msgf("field/header overflow detected for %q -- %d::%d. Check your mappings!", t.GVR(), c, len(h)) continue } + if !t.wide && h[c].Wide { + continue + } if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() { continue @@ -315,7 +361,7 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M cell := tview.NewTableCell(field) cell.SetExpansion(1) cell.SetAlign(h[c].Align) - fgColor := color(t.GetModel().GetNamespace(), t.header, ore) + fgColor := color(ns, h, &re) cell.SetTextColor(fgColor) if marked { cell.SetTextColor(t.styles.Table().MarkColor.Color()) @@ -331,13 +377,14 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M // SortColCmd designates a sorted column. func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { - t.manualSort = true - t.sortCol.asc = !t.sortCol.asc - if t.sortCol.name != name { - t.sortCol.asc = asc + sc := t.getSortCol() + sc.ASC = !sc.ASC + if sc.Name != name { + sc.ASC = asc } - t.sortCol.name = name - t.manualSort = true + sc.Name = name + t.setSortCol(sc) + t.setMSort(true) t.Refresh() return nil } @@ -345,7 +392,7 @@ func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tce // SortInvertCmd reverses sorting order. func (t *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey { - t.sortCol.asc = !t.sortCol.asc + t.toggleSortCol() t.Refresh() return nil @@ -360,21 +407,23 @@ func (t *Table) ClearMarks() { // Refresh update the table data. func (t *Table) Refresh() { data := t.model.Peek() - if len(data.Header) == 0 { + if data.HeaderCount() == 0 { return } // BOZO!! Really want to tell model reload now. Refactor! - t.Update(data, t.hasMetrics) + cdata := t.Update(data, t.hasMetrics) + t.UpdateUI(cdata, data) } // GetSelectedRow returns the entire selected row or nil if nothing selected. -func (t *Table) GetSelectedRow(path string) *render.Row { +func (t *Table) GetSelectedRow(path string) *model1.Row { data := t.model.Peek() - i, ok := data.RowEvents.FindIndex(path) + re, ok := data.FindRow(path) if !ok { return nil } - return &data.RowEvents[i].Row + + return &re.Row } // NameColIndex returns the index of the resource name column. @@ -386,38 +435,25 @@ func (t *Table) NameColIndex() int { if t.GetModel().ClusterWide() { col++ } + return col } // AddHeaderCell configures a table cell header. -func (t *Table) AddHeaderCell(col int, h render.HeaderColumn) { - sortCol := h.Name == t.sortCol.name - c := tview.NewTableCell(sortIndicator(sortCol, t.sortCol.asc, t.styles.Table(), h.Name)) +func (t *Table) AddHeaderCell(col int, h model1.HeaderColumn) { + sc := t.getSortCol() + sortCol := h.Name == sc.Name + c := tview.NewTableCell(sortIndicator(sortCol, sc.ASC, t.styles.Table(), h.Name)) c.SetExpansion(1) c.SetAlign(h.Align) t.SetCell(0, col, c) } -func (t *Table) filtered(data *render.TableData) *render.TableData { - filtered := data - if t.toast { - filtered = filterToast(data) - } - if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.GetText()) { - return filtered - } - - q := t.cmdBuff.GetText() - if f, ok := dao.HasFuzzySelector(q); ok { - return fuzzyFilter(f, filtered) - } - - filtered, err := rxFilter(q, dao.IsInverseSelector(q), filtered) - if err != nil { - log.Error().Err(errors.New("invalid filter expression")).Msg("Regexp") - } - - return filtered +func (t *Table) filtered(data *model1.TableData) *model1.TableData { + return data.Filter(model1.FilterOpts{ + Toast: t.toast, + Filter: t.cmdBuff.GetText(), + }) } // CmdBuff returns the associated command buffer. @@ -470,7 +506,7 @@ func (t *Table) styleTitle() string { } buff := t.cmdBuff.GetText() - if IsLabelSelector(buff) { + if internal.IsLabelSelector(buff) { buff = render.Truncate(TrimLabelSelector(buff), maxTruncate) } else if l := t.GetModel().GetLabelFilter(); l != "" { buff = render.Truncate(l, maxTruncate) diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index bd6ea48118..479ed2d0bd 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -6,15 +6,11 @@ package ui import ( "context" "fmt" - "regexp" "strings" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/view/cmd" "github.com/rs/zerolog/log" - "github.com/sahilm/fuzzy" ) const ( @@ -40,11 +36,6 @@ const ( NoNSFmat = "%s-%d.csv" ) -var ( - // LabelRx identifies a label query. - LabelRx = regexp.MustCompile(`\A\-l`) -) - func mustExtractStyles(ctx context.Context) *config.Styles { styles, ok := ctx.Value(internal.KeyStyles).(*config.Styles) if !ok { @@ -63,15 +54,6 @@ func TrimCell(tv *SelectTable, row, col int) string { return strings.TrimSpace(c.Text) } -// IsLabelSelector checks if query is a label query. -func IsLabelSelector(s string) bool { - if LabelRx.MatchString(s) { - return true - } - - return !strings.Contains(s, " ") && cmd.ToLabels(s) != nil -} - // TrimLabelSelector extracts label query. func TrimLabelSelector(s string) string { if strings.Index(s, "-l") == 0 { @@ -116,75 +98,3 @@ func formatCell(field string, padding int) string { return field } - -func filterToast(data *render.TableData) *render.TableData { - validX := data.Header.IndexOf("VALID", true) - if validX == -1 { - return data - } - - toast := render.TableData{ - Header: data.Header, - RowEvents: make(render.RowEvents, 0, len(data.RowEvents)), - Namespace: data.Namespace, - } - for _, re := range data.RowEvents { - if re.Row.Fields[validX] != "" { - toast.RowEvents = append(toast.RowEvents, re) - } - } - - return &toast -} - -func rxFilter(q string, inverse bool, data *render.TableData) (*render.TableData, error) { - if inverse { - q = q[1:] - } - rx, err := regexp.Compile(`(?i)(` + q + `)`) - if err != nil { - return data, fmt.Errorf("%w -- %s", err, q) - } - - filtered := render.TableData{ - Header: data.Header, - RowEvents: make(render.RowEvents, 0, len(data.RowEvents)), - Namespace: data.Namespace, - } - ageIndex := data.Header.IndexOf("AGE", true) - - const spacer = " " - for _, re := range data.RowEvents { - ff := re.Row.Fields - if ageIndex >= 0 && ageIndex+1 <= len(ff) { - ff = append(ff[0:ageIndex], ff[ageIndex+1:]...) - } - fields := strings.Join(ff, spacer) - if (inverse && !rx.MatchString(fields)) || - ((!inverse) && rx.MatchString(fields)) { - filtered.RowEvents = append(filtered.RowEvents, re) - } - } - - return &filtered, nil -} - -func fuzzyFilter(q string, data *render.TableData) *render.TableData { - q = strings.TrimSpace(q) - ss := make([]string, 0, len(data.RowEvents)) - for _, re := range data.RowEvents { - ss = append(ss, re.Row.ID) - } - - filtered := render.TableData{ - Header: data.Header, - RowEvents: make(render.RowEvents, 0, len(data.RowEvents)), - Namespace: data.Namespace, - } - mm := fuzzy.Find(q, ss) - for _, m := range mm { - filtered.RowEvents = append(filtered.RowEvents, data.RowEvents[m.Index]) - } - - return &filtered -} diff --git a/internal/ui/table_helper_test.go b/internal/ui/table_helper_test.go index 219ffaa3d3..7bec2d4082 100644 --- a/internal/ui/table_helper_test.go +++ b/internal/ui/table_helper_test.go @@ -33,28 +33,6 @@ func TestTruncate(t *testing.T) { } } -func TestIsLabelSelector(t *testing.T) { - uu := map[string]struct { - s string - ok bool - }{ - "empty": {s: ""}, - "cool": {s: "-l app=fred,env=blee", ok: true}, - "no-flag": {s: "app=fred,env=blee", ok: true}, - "no-space": {s: "-lapp=fred,env=blee", ok: true}, - "wrong-flag": {s: "-f app=fred,env=blee"}, - "missing-key": {s: "=fred"}, - "missing-val": {s: "fred="}, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.ok, IsLabelSelector(u.s)) - }) - } -} - func TestTrimLabelSelector(t *testing.T) { uu := map[string]struct { sel, e string diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index f6169abdef..9b604d84a9 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -13,7 +13,7 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,10 +32,11 @@ func TestTableUpdate(t *testing.T) { v.Init(makeContext()) data := makeTableData() - v.Update(data, false) + cdata := v.Update(data, false) + v.UpdateUI(cdata, data) - assert.Equal(t, len(data.RowEvents)+1, v.GetRowCount()) - assert.Equal(t, len(data.Header), v.GetColumnCount()) + assert.Equal(t, data.RowCount()+1, v.GetRowCount()) + assert.Equal(t, data.HeaderCount(), v.GetColumnCount()) } func TestTableSelection(t *testing.T) { @@ -43,12 +44,14 @@ func TestTableSelection(t *testing.T) { v.Init(makeContext()) m := &mockModel{} v.SetModel(m) - v.Update(m.Peek(), false) + data := m.Peek() + cdata := v.Update(data, false) + v.UpdateUI(cdata, data) v.SelectRow(1, 0, true) r := v.GetSelectedRow("r1") if r != nil { - assert.Equal(t, render.Row{ID: "r1", Fields: render.Fields{"blee", "duh", "fred"}}, *r) + assert.Equal(t, model1.Row{ID: "r1", Fields: model1.Fields{"blee", "duh", "fred"}}, *r) } assert.Equal(t, "r1", v.GetSelectedItem()) assert.Equal(t, "blee", v.GetSelectedCell(0)) @@ -71,9 +74,9 @@ func (t *mockModel) SetInstance(string) {} func (t *mockModel) SetLabelFilter(string) {} func (t *mockModel) GetLabelFilter() string { return "" } func (t *mockModel) Empty() bool { return false } -func (t *mockModel) Count() int { return 1 } +func (t *mockModel) RowCount() int { return 1 } func (t *mockModel) HasMetrics() bool { return true } -func (t *mockModel) Peek() *render.TableData { return makeTableData() } +func (t *mockModel) Peek() *model1.TableData { return makeTableData() } func (t *mockModel) Refresh(context.Context) error { return nil } func (t *mockModel) ClusterWide() bool { return false } func (t *mockModel) GetNamespace() string { return "blee" } @@ -97,30 +100,29 @@ func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) { func (t *mockModel) InNamespace(string) bool { return true } func (t *mockModel) SetRefreshRate(time.Duration) {} -func makeTableData() *render.TableData { - t := render.NewTableData() - t.Namespace = "" - t.Header = render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - } - t.RowEvents = render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - ID: "r1", - Fields: render.Fields{"blee", "duh", "fred"}, - }, +func makeTableData() *model1.TableData { + return model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, - render.RowEvent{ - Row: render.Row{ - ID: "r2", - Fields: render.Fields{"blee", "duh", "zorg"}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + ID: "r1", + Fields: model1.Fields{"blee", "duh", "fred"}, + }, }, - }, - } - - return t + model1.RowEvent{ + Row: model1.Row{ + ID: "r2", + Fields: model1.Fields{"blee", "duh", "zorg"}, + }, + }, + ), + ) } func makeContext() context.Context { diff --git a/internal/ui/tree.go b/internal/ui/tree.go index 10eb31e764..5af3046b04 100644 --- a/internal/ui/tree.go +++ b/internal/ui/tree.go @@ -18,7 +18,7 @@ type KeyListenerFunc func() type Tree struct { *tview.TreeView - actions KeyActions + actions *KeyActions selectedItem string cmdBuff *model.FishBuff expandNodes bool @@ -31,7 +31,7 @@ func NewTree() *Tree { return &Tree{ TreeView: tview.NewTreeView(), expandNodes: true, - actions: make(KeyActions), + actions: NewKeyActions(), cmdBuff: model.NewFishBuff('/', model.FilterBuffer), } } @@ -75,7 +75,7 @@ func (t *Tree) SetKeyListenerFn(f KeyListenerFunc) { } // Actions returns active menu bindings. -func (t *Tree) Actions() KeyActions { +func (t *Tree) Actions() *KeyActions { return t.actions } @@ -91,14 +91,14 @@ func (t *Tree) ExtraHints() map[string]string { // BindKeys binds default mnemonics. func (t *Tree) BindKeys() { - t.Actions().Add(KeyActions{ + t.Actions().Merge(NewKeyActionsFromMap(KeyMap{ KeySpace: NewKeyAction("Expand/Collapse", t.noopCmd, true), KeyX: NewKeyAction("Expand/Collapse All", t.toggleCollapseCmd, true), - }) + })) } func (t *Tree) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := t.actions[AsKey(evt)]; ok { + if a, ok := t.actions.Get(AsKey(evt)); ok { return a.Action(evt) } diff --git a/internal/ui/types.go b/internal/ui/types.go index 8013c5e7e8..534d084e29 100644 --- a/internal/ui/types.go +++ b/internal/ui/types.go @@ -9,22 +9,11 @@ import ( "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) -type ( - // SortFn represent a function that can sort columnar data. - SortFn func(rows render.Rows, sortCol SortColumn) - - // SortColumn represents a sortable column. - SortColumn struct { - name string - asc bool - } -) - // Namespaceable represents a namespaceable model. type Namespaceable interface { // ClusterWide returns true if the model represents resource in all namespaces. @@ -63,11 +52,11 @@ type Tabular interface { // Empty returns true if model has no data. Empty() bool - // Count returns the model data count. - Count() int + // RowCount returns the model data count. + RowCount() int // Peek returns current model data. - Peek() *render.TableData + Peek() *model1.TableData // Watch watches a given resource for changes. Watch(context.Context) error diff --git a/internal/view/actions.go b/internal/view/actions.go index 6709b4451b..7d76619889 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -57,13 +57,13 @@ func inScope(scopes []string, aliases map[string]struct{}) bool { return false } -func hotKeyActions(r Runner, aa ui.KeyActions) error { +func hotKeyActions(r Runner, aa *ui.KeyActions) error { hh := config.NewHotKeys() - for k, a := range aa { + aa.Range(func(k tcell.Key, a ui.KeyAction) { if a.Opts.HotKey { - delete(aa, k) + aa.Delete(k) } - } + }) var errs error if err := hh.Load(r.App().Config.ContextHotkeysPath()); err != nil { @@ -75,7 +75,7 @@ func hotKeyActions(r Runner, aa ui.KeyActions) error { errs = errors.Join(errs, err) continue } - if _, ok := aa[key]; ok { + if _, ok := aa.Get(key); ok { if !hk.Override { errs = errors.Join(errs, fmt.Errorf("duplicate hotkey found for %q in %q", hk.ShortCut, k)) continue @@ -89,14 +89,14 @@ func hotKeyActions(r Runner, aa ui.KeyActions) error { continue } - aa[key] = ui.NewKeyActionWithOpts( + aa.Add(key, ui.NewKeyActionWithOpts( hk.Description, gotoCmd(r, command, "", !hk.KeepHistory), ui.ActionOpts{ Shared: true, HotKey: true, }, - ) + )) } return errs @@ -109,18 +109,23 @@ func gotoCmd(r Runner, cmd, path string, clearStack bool) ui.ActionHandler { } } -func pluginActions(r Runner, aa ui.KeyActions) error { +func pluginActions(r Runner, aa *ui.KeyActions) error { pp := config.NewPlugins() - for k, a := range aa { + aa.Range(func(k tcell.Key, a ui.KeyAction) { if a.Opts.Plugin { - delete(aa, k) + aa.Delete(k) } + }) + + path, err := r.App().Config.ContextPluginsPath() + if err != nil { + return err + } + if err := pp.Load(path); err != nil { + return err } var errs error - if err := pp.Load(r.App().Config.ContextPluginsPath()); err != nil { - errs = errors.Join(errs, err) - } aliases := r.Aliases() for k, plugin := range pp.Plugins { if !inScope(plugin.Scopes, aliases) { @@ -131,7 +136,7 @@ func pluginActions(r Runner, aa ui.KeyActions) error { errs = errors.Join(errs, err) continue } - if _, ok := aa[key]; ok { + if _, ok := aa.Get(key); ok { if !plugin.Override { errs = errors.Join(errs, fmt.Errorf("duplicate plugin key found for %q in %q", plugin.ShortCut, k)) continue @@ -139,13 +144,14 @@ func pluginActions(r Runner, aa ui.KeyActions) error { log.Info().Msgf("Action %q has been overridden by plugin in %q", plugin.ShortCut, k) } - aa[key] = ui.NewKeyActionWithOpts( + aa.Add(key, ui.NewKeyActionWithOpts( plugin.Description, pluginAction(r, plugin), ui.ActionOpts{ Visible: true, Plugin: true, - }) + }, + )) } return errs diff --git a/internal/view/alias.go b/internal/view/alias.go index 7e654cba4d..496f2e5c73 100644 --- a/internal/view/alias.go +++ b/internal/view/alias.go @@ -47,10 +47,10 @@ func (a *Alias) aliasContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyAliases, a.App().command.alias) } -func (a *Alias) bindKeys(aa ui.KeyActions) { +func (a *Alias) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true), ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.GetTable().SortColCmd("RESOURCE", true), false), ui.KeyShiftC: ui.NewKeyAction("Sort Command", a.GetTable().SortColCmd("COMMAND", true), false), diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index 15cd393694..6deba28983 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -13,7 +13,7 @@ import ( "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/view" "github.com/derailed/tcell/v2" @@ -93,9 +93,9 @@ func (t *mockModel) SetInstance(string) {} func (t *mockModel) SetLabelFilter(string) {} func (t *mockModel) GetLabelFilter() string { return "" } func (t *mockModel) Empty() bool { return false } -func (t *mockModel) Count() int { return 1 } +func (t *mockModel) RowCount() int { return 1 } func (t *mockModel) HasMetrics() bool { return true } -func (t *mockModel) Peek() *render.TableData { return makeTableData() } +func (t *mockModel) Peek() *model1.TableData { return makeTableData() } func (t *mockModel) ClusterWide() bool { return false } func (t *mockModel) GetNamespace() string { return "blee" } func (t *mockModel) SetNamespace(string) {} @@ -123,27 +123,27 @@ func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) { func (t *mockModel) InNamespace(string) bool { return true } func (t *mockModel) SetRefreshRate(time.Duration) {} -func makeTableData() *render.TableData { - return &render.TableData{ - Namespace: client.ClusterScope, - Header: render.Header{ - render.HeaderColumn{Name: "RESOURCE"}, - render.HeaderColumn{Name: "COMMAND"}, - render.HeaderColumn{Name: "APIGROUP"}, +func makeTableData() *model1.TableData { + return model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{ + model1.HeaderColumn{Name: "RESOURCE"}, + model1.HeaderColumn{Name: "COMMAND"}, + model1.HeaderColumn{Name: "APIGROUP"}, }, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ ID: "r1", - Fields: render.Fields{"blee", "duh", "fred"}, + Fields: model1.Fields{"blee", "duh", "fred"}, }, }, - render.RowEvent{ - Row: render.Row{ + model1.RowEvent{ + Row: model1.Row{ ID: "r2", - Fields: render.Fields{"fred", "duh", "zorg"}, + Fields: model1.Fields{"fred", "duh", "zorg"}, }, }, - }, - } + ), + ) } diff --git a/internal/view/app.go b/internal/view/app.go index 1cfb2dcdd7..fef223111a 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -243,14 +243,14 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { } func (a *App) bindKeys() { - a.AddActions(ui.KeyActions{ + a.AddActions(ui.NewKeyActionsFromMap(ui.KeyMap{ ui.KeyShift9: ui.NewSharedKeyAction("DumpGOR", a.dumpGOR, false), tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false), tcell.KeyCtrlG: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, false), ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false), tcell.KeyCtrlA: ui.NewSharedKeyAction("Aliases", a.aliasCmd, false), tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false), - }) + })) } func (a *App) dumpGOR(evt *tcell.EventKey) *tcell.EventKey { @@ -483,7 +483,7 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error { return err } } - if err := a.Config.Save(); err != nil { + if err := a.Config.Save(true); err != nil { log.Error().Err(err).Msg("config save failed!") } else { log.Debug().Msgf("Saved context config for: %q", name) @@ -516,7 +516,7 @@ func (a *App) BailOut() { } }() - if err := a.Config.Save(); err != nil { + if err := a.Config.Save(true); err != nil { log.Error().Err(err).Msg("config save failed!") } @@ -721,7 +721,6 @@ func (a *App) inject(c model.Component, clearStack bool) error { if clearStack { a.Content.Stack.Clear() } - a.Content.Push(c) return nil diff --git a/internal/view/app_test.go b/internal/view/app_test.go index 924fb8f2cc..e1e932f0e6 100644 --- a/internal/view/app_test.go +++ b/internal/view/app_test.go @@ -15,5 +15,5 @@ func TestAppNew(t *testing.T) { a := view.NewApp(mock.NewMockConfig()) _ = a.Init("blee", 10) - assert.Equal(t, 11, len(a.GetActions())) + assert.Equal(t, 12, a.GetActions().Len()) } diff --git a/internal/view/browser.go b/internal/view/browser.go index 932fbbb6cd..b6045a6de5 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -17,7 +17,7 @@ import ( "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/tcell/v2" @@ -35,6 +35,7 @@ type Browser struct { contextFn ContextFunc cancelFn context.CancelFunc mx sync.RWMutex + updating bool } // NewBrowser returns a new browser. @@ -44,6 +45,18 @@ func NewBrowser(gvr client.GVR) ResourceViewer { } } +func (b *Browser) setUpdating(f bool) { + b.mx.Lock() + defer b.mx.Unlock() + b.updating = f +} + +func (b *Browser) getUpdating() bool { + b.mx.RLock() + defer b.mx.RUnlock() + return b.updating +} + // Init watches all running pods in given namespace. func (b *Browser) Init(ctx context.Context) error { var err error @@ -51,8 +64,8 @@ func (b *Browser) Init(ctx context.Context) error { if err != nil { return err } - colorerFn := render.DefaultColorer - if r, ok := model.Registry[b.GVR().String()]; ok { + colorerFn := model1.DefaultColorer + if r, ok := model.Registry[b.GVR().String()]; ok && r.Renderer != nil { colorerFn = r.Renderer.ColorerFunc() } b.GetTable().SetColorerFn(colorerFn) @@ -118,8 +131,8 @@ func (b *Browser) suggestFilter() model.SuggestionFunc { } } -func (b *Browser) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (b *Browser) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", b.resetCmd, false), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", b.filterCmd, false), tcell.KeyHelp: ui.NewSharedKeyAction("Help", b.helpCmd, false), @@ -179,7 +192,7 @@ func (b *Browser) BufferChanged(_, _ string) {} // BufferCompleted indicates input was accepted. func (b *Browser) BufferCompleted(text, _ string) { - if ui.IsLabelSelector(text) { + if internal.IsLabelSelector(text) { b.GetModel().SetLabelFilter(ui.TrimLabelSelector(text)) } else { b.GetModel().SetLabelFilter("") @@ -191,26 +204,48 @@ func (b *Browser) BufferActive(state bool, k model.BufferKind) { if state { return } - if err := b.GetModel().Refresh(b.prepareContext()); err != nil { + if err := b.GetModel().Refresh(b.GetContext()); err != nil { log.Error().Err(err).Msgf("Refresh failed for %s", b.GVR()) } + data := b.GetModel().Peek() + cdata := b.Update(data, b.App().Conn().HasMetrics()) b.app.QueueUpdateDraw(func() { - b.Update(b.GetModel().Peek(), b.App().Conn().HasMetrics()) + if b.getUpdating() { + return + } + b.setUpdating(true) + defer b.setUpdating(false) + b.UpdateUI(cdata, data) if b.GetRowCount() > 1 { b.App().filterHistory.Push(b.CmdBuff().GetText()) } + }) } func (b *Browser) prepareContext() context.Context { ctx := b.defaultContext() - ctx, b.cancelFn = context.WithCancel(ctx) + + b.mx.Lock() + { + if b.cancelFn != nil { + b.cancelFn() + } + ctx, b.cancelFn = context.WithCancel(ctx) + } + b.mx.Unlock() + if b.contextFn != nil { ctx = b.contextFn(ctx) } if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" { b.Path = path } + b.mx.Lock() + { + b.SetContext(ctx) + } + b.mx.Unlock() return ctx } @@ -237,7 +272,7 @@ func (b *Browser) Aliases() map[string]struct{} { // Model Protocol... // TableDataChanged notifies view new data is available. -func (b *Browser) TableDataChanged(data *render.TableData) { +func (b *Browser) TableDataChanged(data *model1.TableData) { var cancel context.CancelFunc b.mx.RLock() cancel = b.cancelFn @@ -247,9 +282,15 @@ func (b *Browser) TableDataChanged(data *render.TableData) { return } + cdata := b.Update(data, b.app.Conn().HasMetrics()) b.app.QueueUpdateDraw(func() { + if b.getUpdating() { + return + } + b.setUpdating(true) + defer b.setUpdating(false) b.refreshActions() - b.Update(data, b.app.Conn().HasMetrics()) + b.UpdateUI(cdata, data) }) } @@ -287,14 +328,17 @@ func (b *Browser) helpCmd(evt *tcell.EventKey) *tcell.EventKey { func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey { if !b.CmdBuff().InCmdMode() { + hasFilter := !b.CmdBuff().Empty() b.CmdBuff().ClearText(false) - b.GetModel().SetLabelFilter("") + if hasFilter { + b.GetModel().SetLabelFilter("") + b.Refresh() + } return b.App().PrevCmd(evt) - } b.CmdBuff().Reset() - if ui.IsLabelSelector(b.CmdBuff().GetText()) { + if internal.IsLabelSelector(b.CmdBuff().GetText()) { b.Start() } b.Refresh() @@ -308,7 +352,7 @@ func (b *Browser) filterCmd(evt *tcell.EventKey) *tcell.EventKey { } b.CmdBuff().SetActive(false) - if ui.IsLabelSelector(b.CmdBuff().GetText()) { + if internal.IsLabelSelector(b.CmdBuff().GetText()) { b.Start() return nil } @@ -471,7 +515,7 @@ func (b *Browser) defaultContext() context.Context { ctx := context.WithValue(context.Background(), internal.KeyFactory, b.app.factory) ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR()) ctx = context.WithValue(ctx, internal.KeyPath, b.Path) - if ui.IsLabelSelector(b.CmdBuff().GetText()) { + if internal.IsLabelSelector(b.CmdBuff().GetText()) { ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.CmdBuff().GetText())) } ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace())) @@ -484,41 +528,41 @@ func (b *Browser) refreshActions() { if b.App().Content.Top() != nil && b.App().Content.Top().Name() != b.Name() { return } - aa := ui.KeyActions{ + aa := ui.NewKeyActionsFromMap(ui.KeyMap{ ui.KeyC: ui.NewKeyAction("Copy", b.cpCmd, false), tcell.KeyEnter: ui.NewKeyAction("View", b.enterCmd, false), tcell.KeyCtrlR: ui.NewKeyAction("Refresh", b.refreshCmd, false), - } + }) if b.app.ConOK() { b.namespaceActions(aa) if !b.app.Config.K9s.IsReadOnly() { if client.Can(b.meta.Verbs, "edit") { - aa[ui.KeyE] = ui.NewKeyActionWithOpts("Edit", b.editCmd, + aa.Add(ui.KeyE, ui.NewKeyActionWithOpts("Edit", b.editCmd, ui.ActionOpts{ Visible: true, Dangerous: true, - }) + })) } if client.Can(b.meta.Verbs, "delete") { - aa[tcell.KeyCtrlD] = ui.NewKeyActionWithOpts("Delete", b.deleteCmd, + aa.Add(tcell.KeyCtrlD, ui.NewKeyActionWithOpts("Delete", b.deleteCmd, ui.ActionOpts{ Visible: true, Dangerous: true, - }) + })) } } else { b.Actions().ClearDanger() } } if !dao.IsK9sMeta(b.meta) { - aa[ui.KeyY] = ui.NewKeyAction(yamlAction, b.viewCmd, true) - aa[ui.KeyD] = ui.NewKeyAction("Describe", b.describeCmd, true) + aa.Add(ui.KeyY, ui.NewKeyAction(yamlAction, b.viewCmd, true)) + aa.Add(ui.KeyD, ui.NewKeyAction("Describe", b.describeCmd, true)) } for _, f := range b.bindKeysFn { f(aa) } - b.Actions().Add(aa) + b.Actions().Merge(aa) if err := pluginActions(b, b.Actions()); err != nil { log.Warn().Msgf("Plugins load failed: %s", err) @@ -528,25 +572,24 @@ func (b *Browser) refreshActions() { log.Warn().Msgf("Hotkeys load failed: %s", err) b.app.Logo().Warn("HotKeys load failed!") } - b.app.Menu().HydrateMenu(b.Hints()) } -func (b *Browser) namespaceActions(aa ui.KeyActions) { +func (b *Browser) namespaceActions(aa *ui.KeyActions) { if !b.meta.Namespaced || b.GetTable().Path != "" { return } - aa[ui.KeyN] = ui.NewKeyAction("Copy Namespace", b.cpNsCmd, false) + aa.Add(ui.KeyN, ui.NewKeyAction("Copy Namespace", b.cpNsCmd, false)) b.namespaces = make(map[int]string, data.MaxFavoritesNS) - aa[ui.Key0] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true) + aa.Add(ui.Key0, ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true)) b.namespaces[0] = client.NamespaceAll index := 1 for _, ns := range b.app.Config.FavNamespaces() { if ns == client.NamespaceAll { continue } - aa[ui.NumKeys[index]] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true) + aa.Add(ui.NumKeys[index], ui.NewKeyAction(ns, b.switchNamespaceCmd, true)) b.namespaces[index] = ns index++ } diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index 98512a9ac0..3ff7cd839f 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -111,14 +111,9 @@ func (c *ClusterInfo) warnCell(s string, w bool) string { // ClusterInfoChanged notifies the cluster meta was changed. func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) { c.app.QueueUpdateDraw(func() { - var ic = " ✏️" - if c.app.Config.K9s.IsReadOnly() { - ic = " 🔒" - } - c.Clear() c.layout() - row := c.setCell(0, curr.Context+ic) + row := c.setCell(0, curr.Context) row = c.setCell(row, curr.Cluster) row = c.setCell(row, curr.User) if curr.K9sLatest != "" { diff --git a/internal/view/cm.go b/internal/view/cm.go index 05c36da2a1..32d3e47c92 100644 --- a/internal/view/cm.go +++ b/internal/view/cm.go @@ -28,10 +28,8 @@ func NewConfigMap(gvr client.GVR) ResourceViewer { return &s } -func (s *ConfigMap) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true), - }) +func (s *ConfigMap) bindKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyU, ui.NewKeyAction("UsedBy", s.refCmd, true)) } func (s *ConfigMap) refCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/command.go b/internal/view/command.go index 9a8d335551..bb437355c8 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -157,7 +157,7 @@ func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack bool) error { if context, ok := p.HasContext(); ok { if context != c.app.Config.ActiveContextName() { - if err := c.app.Config.Save(); err != nil { + if err := c.app.Config.Save(true); err != nil { log.Error().Err(err).Msg("config save failed!") } else { log.Debug().Msgf("Saved context config for: %q", context) diff --git a/internal/view/container.go b/internal/view/container.go index 3d25a78eac..01f4f1e5e5 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/port" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" @@ -38,25 +39,29 @@ func NewContainer(gvr client.GVR) ResourceViewer { return &c } -func (c *Container) portForwardIndicator(data *render.TableData) { +func (c *Container) portForwardIndicator(data *model1.TableData) { ff := c.App().factory.Forwarders() - col := data.IndexOfHeader("PF") - for _, re := range data.RowEvents { + col, ok := data.IndexOfHeader("PF") + if !ok { + return + } + data.RowsRange(func(_ int, re model1.RowEvent) bool { if ff.IsContainerForwarded(c.GetTable().Path, re.Row.ID) { re.Row.Fields[col] = "[orange::b]Ⓕ" } - } + return true + }) } -func (c *Container) decorateRows(data *render.TableData) { +func (c *Container) decorateRows(data *model1.TableData) { decorateCpuMemHeaderRows(c.App(), data) } // Name returns the component name. func (c *Container) Name() string { return containerTitle } -func (c *Container) bindDangerousKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (c *Container) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyS: ui.NewKeyActionWithOpts( "Shell", c.shellCmd, @@ -74,25 +79,25 @@ func (c *Container) bindDangerousKeys(aa ui.KeyActions) { }) } -func (c *Container) bindKeys(aa ui.KeyActions) { +func (c *Container) bindKeys(aa *ui.KeyActions) { aa.Delete(tcell.KeyCtrlSpace, ui.KeySpace) if !c.App().Config.K9s.IsReadOnly() { c.bindDangerousKeys(aa) } - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyF: ui.NewKeyAction("Show PortForward", c.showPFCmd, true), ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", c.GetTable().SortColCmd("RESTARTS", false), false), }) - aa.Add(resourceSorters(c.GetTable())) + aa.Merge(resourceSorters(c.GetTable())) } func (c *Container) k9sEnv() Env { path := c.GetTable().GetSelectedItem() row := c.GetTable().GetSelectedRow(path) - env := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header, row) + env := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header(), row) env["NAMESPACE"], env["POD"] = client.Namespaced(c.GetTable().Path) return env diff --git a/internal/view/context.go b/internal/view/context.go index 22266194f8..4ba51bf988 100644 --- a/internal/view/context.go +++ b/internal/view/context.go @@ -37,11 +37,9 @@ func NewContext(gvr client.GVR) ResourceViewer { return &c } -func (c *Context) bindKeys(aa ui.KeyActions) { +func (c *Context) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ - ui.KeyR: ui.NewKeyAction("Rename", c.renameCmd, true), - }) + aa.Add(ui.KeyR, ui.NewKeyAction("Rename", c.renameCmd, true)) } func (c *Context) renameCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/cow.go b/internal/view/cow.go index 6920f4dabc..75b71b118e 100644 --- a/internal/view/cow.go +++ b/internal/view/cow.go @@ -19,7 +19,7 @@ import ( type Cow struct { *tview.TextView - actions ui.KeyActions + actions *ui.KeyActions app *App says string } @@ -29,7 +29,7 @@ func NewCow(app *App, says string) *Cow { return &Cow{ TextView: tview.NewTextView(), app: app, - actions: make(ui.KeyActions), + actions: ui.NewKeyActions(), says: says, } } @@ -88,13 +88,11 @@ func cowTalk(says string, w int) string { } func (c *Cow) bindKeys() { - c.actions.Set(ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Back", c.resetCmd, false), - }) + c.actions.Add(tcell.KeyEscape, ui.NewKeyAction("Back", c.resetCmd, false)) } func (c *Cow) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := c.actions[ui.AsKey(evt)]; ok { + if a, ok := c.actions.Get(ui.AsKey(evt)); ok { return a.Action(evt) } @@ -113,7 +111,7 @@ func (c *Cow) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } // Actions returns menu actions. -func (c *Cow) Actions() ui.KeyActions { +func (c *Cow) Actions() *ui.KeyActions { return c.actions } diff --git a/internal/view/crd.go b/internal/view/crd.go new file mode 100644 index 0000000000..7ff1a1f969 --- /dev/null +++ b/internal/view/crd.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/ui" +) + +// CRD represents a crd viewer. +type CRD struct { + ResourceViewer +} + +// NewCRD returns a new viewer. +func NewCRD(gvr client.GVR) ResourceViewer { + s := CRD{ + ResourceViewer: NewBrowser(gvr), + } + s.AddBindKeysFn(s.bindKeys) + s.GetTable().SetEnterFn(s.showCRD) + + return &s +} + +func (s *CRD) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyShiftV: ui.NewKeyAction("Sort Versions", s.GetTable().SortColCmd("VERSIONS", false), true), + ui.KeyShiftR: ui.NewKeyAction("Sort Group", s.GetTable().SortColCmd("GROUP", true), true), + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.GetTable().SortColCmd("KIND", true), true), + }) +} + +func (s *CRD) showCRD(app *App, _ ui.Tabular, _ client.GVR, path string) { + _, crd := client.Namespaced(path) + app.gotoResource(crd, "", false) +} diff --git a/internal/view/cronjob.go b/internal/view/cronjob.go index 90943a812d..864a3381a8 100644 --- a/internal/view/cronjob.go +++ b/internal/view/cronjob.go @@ -71,8 +71,8 @@ func jobCtx(path, uid string) ContextFunc { } } -func (c *CronJob) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (c *CronJob) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyT: ui.NewKeyAction("Trigger", c.triggerCmd, true), ui.KeyS: ui.NewKeyAction("Suspend/Resume", c.toggleSuspendCmd, true), ui.KeyShiftL: ui.NewKeyAction("Sort LastScheduled", c.GetTable().SortColCmd(lastScheduledCol, true), false), diff --git a/internal/view/details.go b/internal/view/details.go index 5ad7b4fb2a..c07c6171b3 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -28,7 +28,7 @@ type Details struct { *tview.Flex text *tview.TextView - actions ui.KeyActions + actions *ui.KeyActions app *App title, subject string cmdBuff *model.FishBuff @@ -47,7 +47,7 @@ func NewDetails(app *App, title, subject, contentType string, searchable bool) * app: app, title: title, subject: subject, - actions: make(ui.KeyActions), + actions: ui.NewKeyActions(), cmdBuff: model.NewFishBuff('/', model.FilterBuffer), model: model.NewText(), searchable: searchable, @@ -132,7 +132,7 @@ func (d *Details) BufferActive(state bool, k model.BufferKind) { } func (d *Details) bindKeys() { - d.actions.Set(ui.KeyActions{ + d.actions.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewSharedKeyAction("Filter", d.filterCmd, false), tcell.KeyEscape: ui.NewKeyAction("Back", d.resetCmd, false), tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), @@ -150,7 +150,7 @@ func (d *Details) bindKeys() { } func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := d.actions[ui.AsKey(evt)]; ok { + if a, ok := d.actions.Get(ui.AsKey(evt)); ok { return a.Action(evt) } @@ -181,7 +181,7 @@ func (d *Details) SetSubject(s string) { } // Actions returns menu actions. -func (d *Details) Actions() ui.KeyActions { +func (d *Details) Actions() *ui.KeyActions { return d.actions } diff --git a/internal/view/dir.go b/internal/view/dir.go index dae5ca724e..bb34f682d9 100644 --- a/internal/view/dir.go +++ b/internal/view/dir.go @@ -60,8 +60,8 @@ func (d *Dir) dirContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyPath, d.path) } -func (d *Dir) bindDangerousKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (d *Dir) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyA: ui.NewKeyActionWithOpts("Apply", d.applyCmd, ui.ActionOpts{ Visible: true, Dangerous: true, @@ -77,14 +77,14 @@ func (d *Dir) bindDangerousKeys(aa ui.KeyActions) { }) } -func (d *Dir) bindKeys(aa ui.KeyActions) { +func (d *Dir) bindKeys(aa *ui.KeyActions) { // !!BOZO!! Lame! aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ) if !d.App().Config.K9s.IsReadOnly() { d.bindDangerousKeys(aa) } - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyY: ui.NewKeyAction(yamlAction, d.viewCmd, true), tcell.KeyEnter: ui.NewKeyAction("Goto", d.gotoCmd, true), }) diff --git a/internal/view/dp.go b/internal/view/dp.go index 11decf20cb..f9cd93d64d 100644 --- a/internal/view/dp.go +++ b/internal/view/dp.go @@ -40,8 +40,8 @@ func NewDeploy(gvr client.GVR) ResourceViewer { return &d } -func (d *Deploy) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (d *Deploy) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(readyCol, true), false), ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(uptodateCol, true), false), ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(availCol, true), false), diff --git a/internal/view/ds.go b/internal/view/ds.go index 4bb9dd6c1b..a9e24abd73 100644 --- a/internal/view/ds.go +++ b/internal/view/ds.go @@ -33,8 +33,8 @@ func NewDaemonSet(gvr client.GVR) ResourceViewer { return &d } -func (d *DaemonSet) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (d *DaemonSet) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyShiftD: ui.NewKeyAction("Sort Desired", d.GetTable().SortColCmd("DESIRED", true), false), ui.KeyShiftC: ui.NewKeyAction("Sort Current", d.GetTable().SortColCmd("CURRENT", true), false), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(readyCol, true), false), diff --git a/internal/view/event.go b/internal/view/event.go index 6de78f30bf..b75c975a97 100644 --- a/internal/view/event.go +++ b/internal/view/event.go @@ -25,9 +25,9 @@ func NewEvent(gvr client.GVR) ResourceViewer { return &e } -func (e *Event) bindKeys(aa ui.KeyActions) { +func (e *Event) bindKeys(aa *ui.KeyActions) { aa.Delete(tcell.KeyCtrlD, ui.KeyE, ui.KeyA) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyShiftL: ui.NewKeyAction("Sort LastSeen", e.GetTable().SortColCmd("LAST SEEN", false), false), ui.KeyShiftF: ui.NewKeyAction("Sort FirstSeen", e.GetTable().SortColCmd("FIRST SEEN", false), false), ui.KeyShiftT: ui.NewKeyAction("Sort Type", e.GetTable().SortColCmd("TYPE", true), false), diff --git a/internal/view/group.go b/internal/view/group.go index 503b190c65..0cfe42ddb5 100644 --- a/internal/view/group.go +++ b/internal/view/group.go @@ -26,9 +26,9 @@ func NewGroup(gvr client.GVR) ResourceViewer { return &g } -func (g *Group) bindKeys(aa ui.KeyActions) { +func (g *Group) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("Rules", g.policyCmd, true), ui.KeyShiftK: ui.NewKeyAction("Sort Kind", g.GetTable().SortColCmd("KIND", true), false), }) diff --git a/internal/view/helm_chart.go b/internal/view/helm_chart.go index afa58e5056..c3d595baf5 100644 --- a/internal/view/helm_chart.go +++ b/internal/view/helm_chart.go @@ -37,9 +37,9 @@ func (c *HelmChart) chartContext(ctx context.Context) context.Context { return ctx } -func (c *HelmChart) bindKeys(aa ui.KeyActions) { +func (c *HelmChart) bindKeys(aa *ui.KeyActions) { aa.Delete(tcell.KeyCtrlS) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyR: ui.NewKeyAction("Releases", c.historyCmd, true), ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false), }) diff --git a/internal/view/helm_history.go b/internal/view/helm_history.go index 0949d5dd5e..a2b5e5a922 100644 --- a/internal/view/helm_history.go +++ b/internal/view/helm_history.go @@ -53,13 +53,13 @@ func (h *History) HistoryContext(ctx context.Context) context.Context { return ctx } -func (h *History) bindKeys(aa ui.KeyActions) { +func (h *History) bindKeys(aa *ui.KeyActions) { if !h.App().Config.K9s.IsReadOnly() { h.bindDangerousKeys(aa) } aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace, tcell.KeyCtrlD) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyShiftN: ui.NewKeyAction("Sort Revision", h.GetTable().SortColCmd("REVISION", true), false), ui.KeyShiftS: ui.NewKeyAction("Sort Status", h.GetTable().SortColCmd("STATUS", true), false), ui.KeyShiftA: ui.NewKeyAction("Sort Age", h.GetTable().SortColCmd("AGE", true), false), @@ -81,17 +81,13 @@ func (h *History) getValsCmd(app *App, _ ui.Tabular, _ client.GVR, path string) } } -func (h *History) bindDangerousKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyR: ui.NewKeyActionWithOpts( - "RollBackTo...", - h.rollbackCmd, - ui.ActionOpts{ - Visible: true, - Dangerous: true, - }, - ), - }) +func (h *History) bindDangerousKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyR, ui.NewKeyActionWithOpts("RollBackTo...", h.rollbackCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }, + )) } func (h *History) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/help.go b/internal/view/help.go index 06665e7be9..4347a43cfa 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -77,7 +77,7 @@ func (h *Help) StylesChanged(s *config.Styles) { func (h *Help) bindKeys() { h.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS, ui.KeySlash) - h.Actions().Set(ui.KeyActions{ + h.Actions().Bulk(ui.KeyMap{ tcell.KeyEscape: ui.NewKeyAction("Back", h.app.PrevCmd, true), ui.KeyHelp: ui.NewKeyAction("Back", h.app.PrevCmd, false), tcell.KeyEnter: ui.NewKeyAction("Back", h.app.PrevCmd, false), diff --git a/internal/view/helpers.go b/internal/view/helpers.go index 70605969a6..8ec027a9b0 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -16,6 +16,7 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/view/cmd" @@ -106,16 +107,16 @@ func k8sEnv(c *client.Config) Env { } } -func defaultEnv(c *client.Config, path string, header render.Header, row *render.Row) Env { +func defaultEnv(c *client.Config, path string, header model1.Header, row *model1.Row) Env { env := k8sEnv(c) env["NAMESPACE"], env["NAME"] = client.Namespaced(path) if row == nil { return env } - for _, col := range header.Columns(true) { - i := header.IndexOf(col, true) - if i >= 0 && i < len(row.Fields) { - env["COL-"+col] = row.Fields[i] + for _, col := range header.ColumnNames(true) { + idx, ok := header.IndexOf(col, true) + if ok && idx < len(row.Fields) { + env["COL-"+col] = row.Fields[idx] } } @@ -218,8 +219,8 @@ func fqn(ns, n string) string { return ns + "/" + n } -func decorateCpuMemHeaderRows(app *App, data *render.TableData) { - for colIndex, header := range data.Header { +func decorateCpuMemHeaderRows(app *App, data *model1.TableData) { + for colIndex, header := range data.Header() { var check string if header.Name == "%CPU/L" { check = "cpu" @@ -230,26 +231,28 @@ func decorateCpuMemHeaderRows(app *App, data *render.TableData) { if len(check) == 0 { continue } - for _, re := range data.RowEvents { + data.RowsRange(func(_ int, re model1.RowEvent) bool { if re.Row.Fields[colIndex] == render.NAValue { - continue + return true } n, err := strconv.Atoi(re.Row.Fields[colIndex]) if err != nil { - continue + return true } if n > 100 { n = 100 } severity := app.Config.K9s.Thresholds.LevelFor(check, n) if severity == config.SeverityLow { - continue + return true } color := app.Config.K9s.Thresholds.SeverityColor(check, n) if len(color) > 0 { re.Row.Fields[colIndex] = "[" + color + "::b]" + re.Row.Fields[colIndex] } - } + + return true + }) } } diff --git a/internal/view/helpers_test.go b/internal/view/helpers_test.go index 9ec347c8aa..5c2ddbe217 100644 --- a/internal/view/helpers_test.go +++ b/internal/view/helpers_test.go @@ -12,6 +12,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config/mock" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/rs/zerolog" @@ -149,12 +150,12 @@ func TestK9sEnv(t *testing.T) { KubeConfig: &cfg, } c := client.NewConfig(&flags) - h := render.Header{ + h := model1.Header{ {Name: "A"}, {Name: "B"}, {Name: "C"}, } - r := render.Row{ + r := model1.Row{ Fields: []string{"a1", "b1", "c1"}, } env := defaultEnv(c, "fred/blee", h, &r) diff --git a/internal/view/image_extender.go b/internal/view/image_extender.go index b67764c6a5..bc8f2a7b7c 100644 --- a/internal/view/image_extender.go +++ b/internal/view/image_extender.go @@ -56,13 +56,11 @@ func NewImageExtender(r ResourceViewer) ResourceViewer { return &s } -func (s *ImageExtender) bindKeys(aa ui.KeyActions) { +func (s *ImageExtender) bindKeys(aa *ui.KeyActions) { if s.App().Config.K9s.IsReadOnly() { return } - aa.Add(ui.KeyActions{ - ui.KeyI: ui.NewKeyAction("Set Image", s.setImageCmd, false), - }) + aa.Add(ui.KeyI, ui.NewKeyAction("Set Image", s.setImageCmd, false)) } func (s *ImageExtender) setImageCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/img_scan.go b/internal/view/img_scan.go index f4f5290b8f..58b1002d80 100644 --- a/internal/view/img_scan.go +++ b/internal/view/img_scan.go @@ -41,10 +41,10 @@ func NewImageScan(gvr client.GVR) ResourceViewer { // Name returns the component name. func (s *ImageScan) Name() string { return imgScanTitle } -func (c *ImageScan) bindKeys(aa ui.KeyActions) { +func (c *ImageScan) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlZ, tcell.KeyCtrlW) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyShiftL: ui.NewKeyAction("Sort Lib", c.GetTable().SortColCmd("LIBRARY", false), true), ui.KeyShiftS: ui.NewKeyAction("Sort Severity", c.GetTable().SortColCmd("SEVERITY", false), true), ui.KeyShiftF: ui.NewKeyAction("Sort Fixed-in", c.GetTable().SortColCmd("FIXED-IN", false), true), diff --git a/internal/view/live_view.go b/internal/view/live_view.go index e97a0bc0f2..a928f2e29c 100644 --- a/internal/view/live_view.go +++ b/internal/view/live_view.go @@ -31,7 +31,7 @@ type LiveView struct { title string model model.ResourceViewer text *tview.TextView - actions ui.KeyActions + actions *ui.KeyActions app *App cmdBuff *model.FishBuff currentRegion, maxRegions int @@ -48,7 +48,7 @@ func NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView { text: tview.NewTextView(), app: app, title: title, - actions: make(ui.KeyActions), + actions: ui.NewKeyActions(), currentRegion: 0, maxRegions: 0, cmdBuff: model.NewFishBuff('/', model.FilterBuffer), @@ -139,7 +139,7 @@ func (v *LiveView) BufferActive(state bool, k model.BufferKind) { } func (v *LiveView) bindKeys() { - v.actions.Set(ui.KeyActions{ + v.actions.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewSharedKeyAction("Filter", v.filterCmd, false), tcell.KeyEscape: ui.NewKeyAction("Back", v.resetCmd, false), tcell.KeyCtrlS: ui.NewKeyAction("Save", v.saveCmd, false), @@ -153,19 +153,13 @@ func (v *LiveView) bindKeys() { }) if !v.app.Config.K9s.IsReadOnly() { - v.actions.Add(ui.KeyActions{ - ui.KeyE: ui.NewKeyAction("Edit", v.editCmd, true), - }) + v.actions.Add(ui.KeyE, ui.NewKeyAction("Edit", v.editCmd, true)) } if v.title == yamlAction { - v.actions.Add(ui.KeyActions{ - ui.KeyM: ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true), - }) + v.actions.Add(ui.KeyM, ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true)) } if v.model != nil && v.model.GVR().IsDecodable() { - v.actions.Add(ui.KeyActions{ - ui.KeyX: ui.NewKeyAction("Toggle Decode", v.toggleEncodedDecodedCmd, true), - }) + v.actions.Add(ui.KeyX, ui.NewKeyAction("Toggle Decode", v.toggleEncodedDecodedCmd, true)) } } @@ -210,7 +204,7 @@ func (v *LiveView) toggleRefreshCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *LiveView) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := v.actions[ui.AsKey(evt)]; ok { + if a, ok := v.actions.Get(ui.AsKey(evt)); ok { return a.Action(evt) } @@ -225,7 +219,7 @@ func (v *LiveView) StylesChanged(s *config.Styles) { } // Actions returns menu actions. -func (v *LiveView) Actions() ui.KeyActions { +func (v *LiveView) Actions() *ui.KeyActions { return v.actions } diff --git a/internal/view/log.go b/internal/view/log.go index a5df9bceff..be01ed5a5d 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -242,7 +242,7 @@ func (l *Log) Stop() { func (l *Log) Name() string { return logTitle } func (l *Log) bindKeys() { - l.logs.Actions().Set(ui.KeyActions{ + l.logs.Actions().Bulk(ui.KeyMap{ ui.Key0: ui.NewKeyAction("tail", l.sinceCmd(-1), true), ui.Key1: ui.NewKeyAction("head", l.sinceCmd(0), true), ui.Key2: ui.NewKeyAction("1m", l.sinceCmd(60), true), @@ -262,9 +262,7 @@ func (l *Log) bindKeys() { ui.KeyC: ui.NewKeyAction("Copy", cpCmd(l.app.Flash(), l.logs.TextView), true), }) if l.model.HasDefaultContainer() { - l.logs.Actions().Set(ui.KeyActions{ - ui.KeyA: ui.NewKeyAction("Toggle AllContainers", l.toggleAllContainers, true), - }) + l.logs.Actions().Add(ui.KeyA, ui.NewKeyAction("Toggle AllContainers", l.toggleAllContainers, true)) } } diff --git a/internal/view/log_test.go b/internal/view/log_test.go index 6b466cf8ec..5db3fcaf8f 100644 --- a/internal/view/log_test.go +++ b/internal/view/log_test.go @@ -5,7 +5,9 @@ package view_test import ( "bytes" + "errors" "fmt" + "io/fs" "os" "testing" @@ -139,7 +141,7 @@ func TestAllContainerKeyBinding(t *testing.T) { t.Run(k, func(t *testing.T) { v := view.NewLog(client.NewGVR("v1/pods"), u.opts) assert.NoError(t, v.Init(makeContext())) - _, got := v.Logs().Actions()[ui.KeyA] + _, got := v.Logs().Actions().Get(ui.KeyA) assert.Equal(t, u.e, got) }) } @@ -154,7 +156,7 @@ func makeApp() *view.App { func ensureDumpDir(n string) error { config.AppDumpsDir = n - if _, err := os.Stat(n); os.IsNotExist(err) { + if _, err := os.Stat(n); errors.Is(err, fs.ErrNotExist) { return os.MkdirAll(n, 0700) } if err := os.RemoveAll(n); err != nil { diff --git a/internal/view/logger.go b/internal/view/logger.go index 7e0526cf36..7fc649f0ec 100644 --- a/internal/view/logger.go +++ b/internal/view/logger.go @@ -17,7 +17,7 @@ import ( type Logger struct { *tview.TextView - actions ui.KeyActions + actions *ui.KeyActions app *App title, subject string cmdBuff *model.FishBuff @@ -28,7 +28,7 @@ func NewLogger(app *App) *Logger { return &Logger{ TextView: tview.NewTextView(), app: app, - actions: make(ui.KeyActions), + actions: ui.NewKeyActions(), cmdBuff: model.NewFishBuff('/', model.FilterBuffer), } } @@ -69,7 +69,7 @@ func (l *Logger) BufferActive(state bool, k model.BufferKind) { } func (l *Logger) bindKeys() { - l.actions.Set(ui.KeyActions{ + l.actions.Bulk(ui.KeyMap{ tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false), tcell.KeyCtrlS: ui.NewKeyAction("Save", l.saveCmd, false), ui.KeyC: ui.NewKeyAction("Copy", cpCmd(l.app.Flash(), l.TextView), true), @@ -79,7 +79,7 @@ func (l *Logger) bindKeys() { } func (l *Logger) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := l.actions[ui.AsKey(evt)]; ok { + if a, ok := l.actions.Get(ui.AsKey(evt)); ok { return a.Action(evt) } @@ -99,7 +99,7 @@ func (l *Logger) SetSubject(s string) { } // Actions returns menu actions. -func (l *Logger) Actions() ui.KeyActions { +func (l *Logger) Actions() *ui.KeyActions { return l.actions } diff --git a/internal/view/logs_extender.go b/internal/view/logs_extender.go index 6c20a81848..95e452117d 100644 --- a/internal/view/logs_extender.go +++ b/internal/view/logs_extender.go @@ -29,8 +29,8 @@ func NewLogsExtender(v ResourceViewer, f LogOptionsFunc) ResourceViewer { } // BindKeys injects new menu actions. -func (l *LogsExtender) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (l *LogsExtender) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyL: ui.NewKeyAction("Logs", l.logsCmd(false), true), ui.KeyP: ui.NewKeyAction("Logs Previous", l.logsCmd(true), true), }) diff --git a/internal/view/node.go b/internal/view/node.go index daf9ac4de7..31a2001540 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -39,8 +39,8 @@ func (n *Node) nodeContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyPodCounting, !n.App().Config.K9s.DisablePodCounting) } -func (n *Node) bindDangerousKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (n *Node) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyC: ui.NewKeyActionWithOpts( "Cordon", n.toggleCordonCmd(true), @@ -72,18 +72,16 @@ func (n *Node) bindDangerousKeys(aa ui.KeyActions) { return } if ct.FeatureGates.NodeShell { - aa.Add(ui.KeyActions{ - ui.KeyS: ui.NewKeyAction("Shell", n.sshCmd, true), - }) + aa.Add(ui.KeyS, ui.NewKeyAction("Shell", n.sshCmd, true)) } } -func (n *Node) bindKeys(aa ui.KeyActions) { +func (n *Node) bindKeys(aa *ui.KeyActions) { if !n.App().Config.K9s.IsReadOnly() { n.bindDangerousKeys(aa) } - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyY: ui.NewKeyAction(yamlAction, n.yamlCmd, true), ui.KeyShiftR: ui.NewKeyAction("Sort ROLE", n.GetTable().SortColCmd("ROLE", true), false), ui.KeyShiftC: ui.NewKeyAction("Sort CPU", n.GetTable().SortColCmd(cpuCol, false), false), diff --git a/internal/view/ns.go b/internal/view/ns.go index e86432fd30..1eac09dbef 100644 --- a/internal/view/ns.go +++ b/internal/view/ns.go @@ -5,8 +5,7 @@ package view import ( "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config/data" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" ) @@ -33,8 +32,8 @@ func NewNamespace(gvr client.GVR) ResourceViewer { return &n } -func (n *Namespace) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (n *Namespace) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyU: ui.NewKeyAction("Use", n.useNsCmd, true), ui.KeyShiftS: ui.NewKeyAction("Sort Status", n.GetTable().SortColCmd(statusCol, true), false), }) @@ -70,32 +69,37 @@ func (n *Namespace) useNamespace(fqn string) { } } -func (n *Namespace) decorate(td *render.TableData) { - if n.App().Conn() == nil || len(td.RowEvents) == 0 { +func (n *Namespace) decorate(td *model1.TableData) { + if n.App().Conn() == nil || td.RowCount() == 0 { return } - // checks if all ns is in the list if not add it. - if _, ok := td.RowEvents.FindIndex(client.NamespaceAll); !ok { - td.RowEvents = append(td.RowEvents, - render.RowEvent{ - Kind: render.EventUnchanged, - Row: render.Row{ - ID: client.NamespaceAll, - Fields: render.Fields{client.NamespaceAll, "Active", "", "", ""}, - }, + if _, ok := td.FindRow(client.NamespaceAll); !ok { + td.AddRow(model1.RowEvent{ + Kind: model1.EventUnchanged, + Row: model1.Row{ + ID: client.NamespaceAll, + Fields: model1.Fields{client.NamespaceAll, "Active", "", "", ""}, }, + }, ) } - for _, re := range td.RowEvents { - if data.InList(n.App().Config.FavNamespaces(), re.Row.ID) { + favs := make(map[string]struct{}) + for _, ns := range n.App().Config.FavNamespaces() { + favs[ns] = struct{}{} + } + ans := n.App().Config.ActiveNamespace() + td.RowsRange(func(i int, re model1.RowEvent) bool { + _, n := client.Namespaced(re.Row.ID) + if _, ok := favs[n]; ok { re.Row.Fields[0] += favNSIndicator - re.Kind = render.EventUnchanged } - if n.App().Config.ActiveNamespace() == re.Row.ID { + if ans == re.Row.ID { re.Row.Fields[0] += defaultNSIndicator - re.Kind = render.EventUnchanged } - } + re.Kind = model1.EventUnchanged + td.SetRow(i, re) + return true + }) } diff --git a/internal/view/pf.go b/internal/view/pf.go index 40ded9d65a..a92a1824ed 100644 --- a/internal/view/pf.go +++ b/internal/view/pf.go @@ -51,8 +51,8 @@ func (p *PortForward) portForwardContext(ctx context.Context) context.Context { return ctx } -func (p *PortForward) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (p *PortForward) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("View Benchmarks", p.showBenchCmd, true), ui.KeyB: ui.NewKeyAction("Benchmark Run/Stop", p.toggleBenchCmd, true), tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, true), diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go index 627395f76e..2e19cc2694 100644 --- a/internal/view/pf_extender.go +++ b/internal/view/pf_extender.go @@ -36,8 +36,8 @@ func NewPortForwardExtender(r ResourceViewer) ResourceViewer { return &p } -func (p *PortForwardExtender) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (p *PortForwardExtender) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyF: ui.NewKeyAction("Show PortForward", p.showPFCmd, true), ui.KeyShiftF: ui.NewKeyAction("Port-Forward", p.portFwdCmd, true), }) diff --git a/internal/view/picker.go b/internal/view/picker.go index b16042be95..56e0275181 100644 --- a/internal/view/picker.go +++ b/internal/view/picker.go @@ -38,7 +38,7 @@ func (p *Picker) Init(ctx context.Context) error { } pickerView := app.Styles.Views().Picker - p.actions[tcell.KeyEscape] = ui.NewKeyAction("Back", app.PrevCmd, true) + p.actions.Add(tcell.KeyEscape, ui.NewKeyAction("Back", app.PrevCmd, true)) p.SetBorder(true) p.SetMainTextColor(pickerView.MainColor.Color()) @@ -48,7 +48,7 @@ func (p *Picker) Init(ctx context.Context) error { p.SetTitle(" [aqua::b]Containers Picker ") p.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := p.actions[evt.Key()]; ok { + if a, ok := p.actions.Get(evt.Key()); ok { a.Action(evt) evt = nil } diff --git a/internal/view/pod.go b/internal/view/pod.go index de1fc1fe35..cdd2a92428 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "io/fs" "os" "strings" @@ -14,6 +15,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" @@ -34,6 +36,7 @@ const ( osBetaSelector = "beta." + osSelector trUpload = "Upload" trDownload = "Download" + pfIndicator = "[orange::b]Ⓕ" ) // Pod represents a pod viewer. @@ -58,20 +61,25 @@ func NewPod(gvr client.GVR) ResourceViewer { return &p } -func (p *Pod) portForwardIndicator(data *render.TableData) { +func (p *Pod) portForwardIndicator(data *model1.TableData) { ff := p.App().factory.Forwarders() - col := data.IndexOfHeader("PF") - for _, re := range data.RowEvents { + defer decorateCpuMemHeaderRows(p.App(), data) + idx, ok := data.IndexOfHeader("PF") + if !ok { + return + } + + data.RowsRange(func(_ int, re model1.RowEvent) bool { if ff.IsPodForwarded(re.Row.ID) { - re.Row.Fields[col] = "[orange::b]Ⓕ" + re.Row.Fields[idx] = pfIndicator } - } - decorateCpuMemHeaderRows(p.App(), data) + return true + }) } -func (p *Pod) bindDangerousKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (p *Pod) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ tcell.KeyCtrlK: ui.NewKeyActionWithOpts( "Kill", p.killCmd, @@ -110,12 +118,12 @@ func (p *Pod) bindDangerousKeys(aa ui.KeyActions) { }) } -func (p *Pod) bindKeys(aa ui.KeyActions) { +func (p *Pod) bindKeys(aa *ui.KeyActions) { if !p.App().Config.K9s.IsReadOnly() { p.bindDangerousKeys(aa) } - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyO: ui.NewKeyAction("Show Node", p.showNode, true), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(readyCol, true), false), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd("RESTARTS", false), false), @@ -123,7 +131,7 @@ func (p *Pod) bindKeys(aa ui.KeyActions) { ui.KeyShiftI: ui.NewKeyAction("Sort IP", p.GetTable().SortColCmd("IP", true), false), ui.KeyShiftO: ui.NewKeyAction("Sort Node", p.GetTable().SortColCmd("NODE", true), false), }) - aa.Add(resourceSorters(p.GetTable())) + aa.Merge(resourceSorters(p.GetTable())) } func (p *Pod) logOptions(prev bool) (*dao.LogOptions, error) { @@ -307,7 +315,7 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey { if !download { local = from } - if _, err := os.Stat(local); !download && os.IsNotExist(err) { + if _, err := os.Stat(local); !download && errors.Is(err, fs.ErrNotExist) { p.App().Flash().Err(err) return false } @@ -555,13 +563,13 @@ func osFromSelector(s map[string]string) (string, bool) { return os, ok } -func resourceSorters(t *Table) ui.KeyActions { - return ui.KeyActions{ +func resourceSorters(t *Table) *ui.KeyActions { + return ui.NewKeyActionsFromMap(ui.KeyMap{ ui.KeyShiftC: ui.NewKeyAction("Sort CPU", t.SortColCmd(cpuCol, false), false), ui.KeyShiftM: ui.NewKeyAction("Sort MEM", t.SortColCmd(memCol, false), false), ui.KeyShiftX: ui.NewKeyAction("Sort CPU/R", t.SortColCmd("%CPU/R", false), false), ui.KeyShiftZ: ui.NewKeyAction("Sort MEM/R", t.SortColCmd("%MEM/R", false), false), tcell.KeyCtrlX: ui.NewKeyAction("Sort CPU/L", t.SortColCmd("%CPU/L", false), false), tcell.KeyCtrlQ: ui.NewKeyAction("Sort MEM/L", t.SortColCmd("%MEM/L", false), false), - } + }) } diff --git a/internal/view/policy.go b/internal/view/policy.go index 28fee83e64..b412b388f5 100644 --- a/internal/view/policy.go +++ b/internal/view/policy.go @@ -46,9 +46,9 @@ func (p *Policy) subjectCtx(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeySubjectName, p.subjectName) } -func (p *Policy) bindKeys(aa ui.KeyActions) { +func (p *Policy) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.GetTable().SortColCmd(nameCol, true), false), ui.KeyShiftA: ui.NewKeyAction("Sort Api-Group", p.GetTable().SortColCmd("API-GROUP", true), false), ui.KeyShiftB: ui.NewKeyAction("Sort Binding", p.GetTable().SortColCmd("BINDING", true), false), diff --git a/internal/view/popeye.go b/internal/view/popeye.go index ca5c273766..e3c6355b38 100644 --- a/internal/view/popeye.go +++ b/internal/view/popeye.go @@ -3,114 +3,114 @@ package view -import ( - "context" - "fmt" - "strconv" - "time" +// import ( +// "context" +// "fmt" +// "strconv" +// "time" - "github.com/derailed/k9s/internal" - "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tcell/v2" -) +// "github.com/derailed/k9s/internal" +// "github.com/derailed/k9s/internal/client" +// "github.com/derailed/k9s/internal/render" +// "github.com/derailed/k9s/internal/ui" +// "github.com/derailed/tcell/v2" +// ) -// Popeye represents a sanitizer view. -type Popeye struct { - ResourceViewer -} +// // Popeye represents a sanitizer view. +// type Popeye struct { +// ResourceViewer +// } -// NewPopeye returns a new view. -func NewPopeye(gvr client.GVR) ResourceViewer { - p := Popeye{ - ResourceViewer: NewBrowser(gvr), - } - p.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) - p.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone)) - p.GetTable().SetSortCol("SCORE%", true) - p.GetTable().SetDecorateFn(p.decorateRows) - p.AddBindKeysFn(p.bindKeys) +// // NewPopeye returns a new view. +// func NewPopeye(gvr client.GVR) ResourceViewer { +// p := Popeye{ +// ResourceViewer: NewBrowser(gvr), +// } +// p.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) +// p.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone)) +// p.GetTable().SetSortCol("SCORE%", true) +// p.GetTable().SetDecorateFn(p.decorateRows) +// p.AddBindKeysFn(p.bindKeys) - return &p -} +// return &p +// } -// Init initializes the view. -func (p *Popeye) Init(ctx context.Context) error { - if err := p.ResourceViewer.Init(ctx); err != nil { - return err - } - p.GetTable().GetModel().SetRefreshRate(5 * time.Second) +// // Init initializes the view. +// func (p *Popeye) Init(ctx context.Context) error { +// if err := p.ResourceViewer.Init(ctx); err != nil { +// return err +// } +// p.GetTable().GetModel().SetRefreshRate(5 * time.Second) - return nil -} +// return nil +// } -func (p *Popeye) decorateRows(data *render.TableData) { - var sum int - for _, re := range data.RowEvents { - n, err := strconv.Atoi(re.Row.Fields[1]) - if err != nil { - continue - } - sum += n - } - score, letter := 0, render.NAValue - if len(data.RowEvents) > 0 { - score = sum / len(data.RowEvents) - letter = grade(score) - } - p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, letter) -} +// func (p *Popeye) decorateRows(data *model1.TableData) { +// var sum int +// for _, re := range data.RowEvents { +// n, err := strconv.Atoi(re.Row.Fields[1]) +// if err != nil { +// continue +// } +// sum += n +// } +// score, letter := 0, render.NAValue +// if len(data.RowEvents) > 0 { +// score = sum / len(data.RowEvents) +// letter = grade(score) +// } +// p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, letter) +// } -func (p *Popeye) bindKeys(aa ui.KeyActions) { - aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Goto", p.gotoCmd, true), - ui.KeyShiftR: ui.NewKeyAction("Sort Resource", p.GetTable().SortColCmd("RESOURCE", true), false), - ui.KeyShiftS: ui.NewKeyAction("Sort Score", p.GetTable().SortColCmd("SCORE%", true), false), - ui.KeyShiftO: ui.NewKeyAction("Sort OK", p.GetTable().SortColCmd("OK", true), false), - ui.KeyShiftI: ui.NewKeyAction("Sort Info", p.GetTable().SortColCmd("INFO", true), false), - ui.KeyShiftW: ui.NewKeyAction("Sort Warning", p.GetTable().SortColCmd("WARNING", true), false), - ui.KeyShiftE: ui.NewKeyAction("Sort Error", p.GetTable().SortColCmd("ERROR", true), false), - }) -} +// func (p *Popeye) bindKeys(aa ui.KeyActions) { +// aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) +// aa.Add(ui.KeyActions{ +// tcell.KeyEnter: ui.NewKeyAction("Goto", p.gotoCmd, true), +// ui.KeyShiftR: ui.NewKeyAction("Sort Resource", p.GetTable().SortColCmd("RESOURCE", true), false), +// ui.KeyShiftS: ui.NewKeyAction("Sort Score", p.GetTable().SortColCmd("SCORE%", true), false), +// ui.KeyShiftO: ui.NewKeyAction("Sort OK", p.GetTable().SortColCmd("OK", true), false), +// ui.KeyShiftI: ui.NewKeyAction("Sort Info", p.GetTable().SortColCmd("INFO", true), false), +// ui.KeyShiftW: ui.NewKeyAction("Sort Warning", p.GetTable().SortColCmd("WARNING", true), false), +// ui.KeyShiftE: ui.NewKeyAction("Sort Error", p.GetTable().SortColCmd("ERROR", true), false), +// }) +// } -func (p *Popeye) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { - path := p.GetTable().GetSelectedItem() - if path == "" { - return evt - } - v := NewSanitizer(client.NewGVR("sanitizer")) - v.SetContextFn(sanitizerCtx(path)) - if err := p.App().inject(v, false); err != nil { - p.App().Flash().Err(err) - } +// func (p *Popeye) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { +// path := p.GetTable().GetSelectedItem() +// if path == "" { +// return evt +// } +// v := NewSanitizer(client.NewGVR("sanitizer")) +// v.SetContextFn(sanitizerCtx(path)) +// if err := p.App().inject(v, false); err != nil { +// p.App().Flash().Err(err) +// } - return nil -} +// return nil +// } -func sanitizerCtx(path string) ContextFunc { - return func(ctx context.Context) context.Context { - ctx = context.WithValue(ctx, internal.KeyPath, path) - return ctx - } -} +// func sanitizerCtx(path string) ContextFunc { +// return func(ctx context.Context) context.Context { +// ctx = context.WithValue(ctx, internal.KeyPath, path) +// return ctx +// } +// } -// Helpers... +// // Helpers... -func grade(score int) string { - switch { - case score >= 90: - return "A" - case score >= 80: - return "B" - case score >= 70: - return "C" - case score >= 60: - return "D" - case score >= 50: - return "E" - default: - return "F" - } -} +// func grade(score int) string { +// switch { +// case score >= 90: +// return "A" +// case score >= 80: +// return "B" +// case score >= 70: +// return "C" +// case score >= 60: +// return "D" +// case score >= 50: +// return "E" +// default: +// return "F" +// } +// } diff --git a/internal/view/priorityclass.go b/internal/view/priorityclass.go index fc89ef5010..7f9ef90908 100644 --- a/internal/view/priorityclass.go +++ b/internal/view/priorityclass.go @@ -25,10 +25,8 @@ func NewPriorityClass(gvr client.GVR) ResourceViewer { return &s } -func (s *PriorityClass) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true), - }) +func (s *PriorityClass) bindKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyU, ui.NewKeyAction("UsedBy", s.refCmd, true)) } func (s *PriorityClass) refCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/pulse.go b/internal/view/pulse.go index c323f64d01..6d36e7a5fd 100644 --- a/internal/view/pulse.go +++ b/internal/view/pulse.go @@ -64,7 +64,7 @@ type Pulse struct { gvr client.GVR model *model.Pulse cancelFn context.CancelFunc - actions ui.KeyActions + actions *ui.KeyActions charts []Graphable } @@ -73,7 +73,7 @@ func NewPulse(gvr client.GVR) ResourceViewer { return &Pulse{ Grid: tview.NewGrid(), model: model.NewPulse(gvr.String()), - actions: make(ui.KeyActions), + actions: ui.NewKeyActions(), } } @@ -207,15 +207,15 @@ func (p *Pulse) PulseFailed(err error) { } func (p *Pulse) bindKeys() { - p.actions.Add(ui.KeyActions{ + p.actions.Merge(ui.NewKeyActionsFromMap(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("Goto", p.enterCmd, true), tcell.KeyTab: ui.NewKeyAction("Next", p.nextFocusCmd(1), true), tcell.KeyBacktab: ui.NewKeyAction("Prev", p.nextFocusCmd(-1), true), - }) + })) for i, v := range p.charts { t := cases.Title(language.Und, cases.NoLower).String(client.NewGVR(v.ID()).R()) - p.actions[ui.NumKeys[i]] = ui.NewKeyAction(t, p.sparkFocusCmd(i), true) + p.actions.Add(ui.NumKeys[i], ui.NewKeyAction(t, p.sparkFocusCmd(i), true)) } } @@ -224,7 +224,7 @@ func (p *Pulse) keyboard(evt *tcell.EventKey) *tcell.EventKey { if key == tcell.KeyRune { key = tcell.Key(evt.Rune()) } - if a, ok := p.actions[key]; ok { + if a, ok := p.actions.Get(key); ok { return a.Action(evt) } @@ -289,7 +289,7 @@ func (p *Pulse) GetTable() *Table { } // Actions returns active menu bindings. -func (p *Pulse) Actions() ui.KeyActions { +func (p *Pulse) Actions() *ui.KeyActions { return p.actions } diff --git a/internal/view/pvc.go b/internal/view/pvc.go index 486fb46383..27d15749de 100644 --- a/internal/view/pvc.go +++ b/internal/view/pvc.go @@ -25,8 +25,8 @@ func NewPersistentVolumeClaim(gvr client.GVR) ResourceViewer { return &v } -func (p *PersistentVolumeClaim) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (p *PersistentVolumeClaim) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyU: ui.NewKeyAction("UsedBy", p.refCmd, true), ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd("STATUS", true), false), ui.KeyShiftV: ui.NewKeyAction("Sort Volume", p.GetTable().SortColCmd("VOLUME", true), false), diff --git a/internal/view/rbac.go b/internal/view/rbac.go index 2ea21464e1..5583122861 100644 --- a/internal/view/rbac.go +++ b/internal/view/rbac.go @@ -29,11 +29,9 @@ func NewRbac(gvr client.GVR) ResourceViewer { return &r } -func (r *Rbac) bindKeys(aa ui.KeyActions) { +func (r *Rbac) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ - ui.KeyShiftA: ui.NewKeyAction("Sort API-Group", r.GetTable().SortColCmd("API-GROUP", true), false), - }) + aa.Add(ui.KeyShiftA, ui.NewKeyAction("Sort API-Group", r.GetTable().SortColCmd("API-GROUP", true), false)) } func showRules(app *App, _ ui.Tabular, gvr client.GVR, path string) { diff --git a/internal/view/reference.go b/internal/view/reference.go index 5519eae907..2f08cc7bba 100644 --- a/internal/view/reference.go +++ b/internal/view/reference.go @@ -38,10 +38,10 @@ func (r *Reference) Init(ctx context.Context) error { return nil } -func (r *Reference) bindKeys(aa ui.KeyActions) { +func (r *Reference) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlZ) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("Goto", r.gotoCmd, true), ui.KeyShiftV: ui.NewKeyAction("Sort GVR", r.GetTable().SortColCmd("GVR", true), false), }) diff --git a/internal/view/registrar.go b/internal/view/registrar.go index cde177b9c1..d199a44988 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -5,7 +5,6 @@ package view import ( "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/ui" ) func loadCustomViewers() MetaViewers { @@ -15,7 +14,7 @@ func loadCustomViewers() MetaViewers { appsViewers(m) rbacViewers(m) batchViewers(m) - extViewers(m) + crdViewers(m) helmViewers(m) return m @@ -91,9 +90,10 @@ func miscViewers(vv MetaViewers) { vv[client.NewGVR("pulses")] = MetaViewer{ viewerFn: NewPulse, } - vv[client.NewGVR("popeye")] = MetaViewer{ - viewerFn: NewPopeye, - } + // !!BOZO!! Popeye + // vv[client.NewGVR("popeye")] = MetaViewer{ + // viewerFn: NewPopeye, + // } vv[client.NewGVR("sanitizer")] = MetaViewer{ viewerFn: NewSanitizer, } @@ -153,13 +153,8 @@ func batchViewers(vv MetaViewers) { } } -func extViewers(vv MetaViewers) { +func crdViewers(vv MetaViewers) { vv[client.NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions")] = MetaViewer{ - enterFn: showCRD, + viewerFn: NewCRD, } } - -func showCRD(app *App, _ ui.Tabular, _ client.GVR, path string) { - _, crd := client.Namespaced(path) - app.gotoResource(crd, "", false) -} diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go index 1e83ced88c..668e9bad71 100644 --- a/internal/view/restart_extender.go +++ b/internal/view/restart_extender.go @@ -29,16 +29,16 @@ func NewRestartExtender(v ResourceViewer) ResourceViewer { } // BindKeys creates additional menu actions. -func (r *RestartExtender) bindKeys(aa ui.KeyActions) { +func (r *RestartExtender) bindKeys(aa *ui.KeyActions) { if r.App().Config.K9s.IsReadOnly() { return } - aa.Add(ui.KeyActions{ - ui.KeyR: ui.NewKeyActionWithOpts("Restart", r.restartCmd, ui.ActionOpts{ + aa.Add(ui.KeyR, ui.NewKeyActionWithOpts("Restart", r.restartCmd, + ui.ActionOpts{ Visible: true, Dangerous: true, - }), - }) + }, + )) } func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/rs.go b/internal/view/rs.go index 3650b8ff15..86e2c5956b 100644 --- a/internal/view/rs.go +++ b/internal/view/rs.go @@ -29,8 +29,8 @@ func NewReplicaSet(gvr client.GVR) ResourceViewer { return &r } -func (r *ReplicaSet) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (r *ReplicaSet) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyShiftD: ui.NewKeyAction("Sort Desired", r.GetTable().SortColCmd("DESIRED", true), false), ui.KeyShiftC: ui.NewKeyAction("Sort Current", r.GetTable().SortColCmd("CURRENT", true), false), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", r.GetTable().SortColCmd(readyCol, true), false), diff --git a/internal/view/sa.go b/internal/view/sa.go index c1433c2cce..63145e7cd4 100644 --- a/internal/view/sa.go +++ b/internal/view/sa.go @@ -29,8 +29,8 @@ func NewServiceAccount(gvr client.GVR) ResourceViewer { return &s } -func (s *ServiceAccount) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (s *ServiceAccount) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true), tcell.KeyEnter: ui.NewKeyAction("Rules", s.policyCmd, true), }) diff --git a/internal/view/sanitizer.go b/internal/view/sanitizer.go index 51388cebec..9ec116d73e 100644 --- a/internal/view/sanitizer.go +++ b/internal/view/sanitizer.go @@ -110,7 +110,7 @@ func (s *Sanitizer) ExtraHints() map[string]string { func (s *Sanitizer) SetInstance(string) {} func (s *Sanitizer) bindKeys() { - s.Actions().Add(ui.KeyActions{ + s.Actions().Bulk(ui.KeyMap{ ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", s.activateCmd, false), tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", s.resetCmd, false), tcell.KeyEnter: ui.NewKeyAction("Goto", s.gotoCmd, true), @@ -209,7 +209,7 @@ func (s *Sanitizer) resetCmd(evt *tcell.EventKey) *tcell.EventKey { func (s *Sanitizer) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if s.CmdBuff().IsActive() { - if ui.IsLabelSelector(s.CmdBuff().GetText()) { + if internal.IsLabelSelector(s.CmdBuff().GetText()) { s.Start() } s.CmdBuff().SetActive(false) @@ -238,16 +238,16 @@ func (s *Sanitizer) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { func (s *Sanitizer) filter(root *xray.TreeNode) *xray.TreeNode { q := s.CmdBuff().GetText() - if s.CmdBuff().Empty() || ui.IsLabelSelector(q) { + if s.CmdBuff().Empty() || internal.IsLabelSelector(q) { return root } s.UpdateTitle() - if f, ok := dao.HasFuzzySelector(q); ok { + if f, ok := internal.IsFuzzySelector(q); ok { return root.Filter(f, fuzzyFilter) } - if dao.IsInverseSelector(q) { + if internal.IsInverseSelector(q) { return root.Filter(q, rxInverseFilter) } @@ -427,7 +427,7 @@ func (s *Sanitizer) styleTitle() string { if buff == "" { return title } - if ui.IsLabelSelector(buff) { + if internal.IsLabelSelector(buff) { buff = ui.TrimLabelSelector(buff) } diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go index c30e289c3e..e402bd5ae8 100644 --- a/internal/view/scale_extender.go +++ b/internal/view/scale_extender.go @@ -31,17 +31,16 @@ func NewScaleExtender(r ResourceViewer) ResourceViewer { return &s } -func (s *ScaleExtender) bindKeys(aa ui.KeyActions) { +func (s *ScaleExtender) bindKeys(aa *ui.KeyActions) { if s.App().Config.K9s.IsReadOnly() { return } - aa.Add(ui.KeyActions{ - ui.KeyS: ui.NewKeyActionWithOpts("Scale", s.scaleCmd, - ui.ActionOpts{ - Visible: true, - Dangerous: true, - }), - }) + aa.Add(ui.KeyS, ui.NewKeyActionWithOpts("Scale", s.scaleCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }, + )) } func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/secret.go b/internal/view/secret.go index e37cdb7687..d59e77ee1c 100644 --- a/internal/view/secret.go +++ b/internal/view/secret.go @@ -28,8 +28,8 @@ func NewSecret(gvr client.GVR) ResourceViewer { return &s } -func (s *Secret) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (s *Secret) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyX: ui.NewKeyAction("Decode", s.decodeCmd, true), ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true), }) diff --git a/internal/view/sts.go b/internal/view/sts.go index e9e04fcae1..816dabb984 100644 --- a/internal/view/sts.go +++ b/internal/view/sts.go @@ -80,10 +80,8 @@ func (s *StatefulSet) logOptions(prev bool) (*dao.LogOptions, error) { return &opts, nil } -func (s *StatefulSet) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyShiftR: ui.NewKeyAction("Sort Ready", s.GetTable().SortColCmd(readyCol, true), false), - }) +func (s *StatefulSet) bindKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyShiftR, ui.NewKeyAction("Sort Ready", s.GetTable().SortColCmd(readyCol, true), false)) } func (s *StatefulSet) showPods(app *App, _ ui.Tabular, _ client.GVR, path string) { diff --git a/internal/view/svc.go b/internal/view/svc.go index 4abf37d772..1517116819 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -46,8 +46,8 @@ func NewService(gvr client.GVR) ResourceViewer { // Protocol... -func (s *Service) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (s *Service) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyB: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true), ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd("TYPE", true), false), }) diff --git a/internal/view/table.go b/internal/view/table.go index 48bb9c9bdd..d0f60d37a0 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -93,7 +93,7 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { return evt } - if a, ok := t.Actions()[ui.AsKey(evt)]; ok && !t.app.Content.IsTopDialog() { + if a, ok := t.Actions().Get(ui.AsKey(evt)); ok && !t.app.Content.IsTopDialog() { return a.Action(evt) } @@ -119,7 +119,7 @@ func (t *Table) EnvFn() EnvFunc { func (t *Table) defaultEnv() Env { path := t.GetSelectedItem() row := t.GetSelectedRow(path) - env := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header, row) + env := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header(), row) env["FILTER"] = t.CmdBuff().GetText() if env["FILTER"] == "" { env["NAMESPACE"], env["FILTER"] = client.Namespaced(path) @@ -186,7 +186,7 @@ func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { } func (t *Table) bindKeys() { - t.Actions().Add(ui.KeyActions{ + t.Actions().Bulk(ui.KeyMap{ ui.KeyHelp: ui.NewKeyAction("Help", t.App().helpCmd, true), ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false), tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Mark Range", t.markSpanCmd, false), diff --git a/internal/view/table_helper.go b/internal/view/table_helper.go index 820f8754e1..34dfc81e0f 100644 --- a/internal/view/table_helper.go +++ b/internal/view/table_helper.go @@ -13,7 +13,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config/data" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/rs/zerolog/log" ) @@ -41,8 +41,8 @@ func computeFilename(dumpPath, ns, title, path string) (string, error) { return strings.ToLower(filepath.Join(dir, fName)), nil } -func saveTable(dir, title, path string, data *render.TableData) (string, error) { - ns := data.Namespace +func saveTable(dir, title, path string, data *model1.TableData) (string, error) { + ns := data.GetNamespace() if client.IsClusterWide(ns) { ns = client.NamespaceAll } @@ -65,15 +65,12 @@ func saveTable(dir, title, path string, data *render.TableData) (string, error) }() w := csv.NewWriter(out) - if err := w.Write(data.Header.Columns(true)); err != nil { - return "", err - } + _ = w.Write(data.ColumnNames(true)) - for _, re := range data.RowEvents { - if err := w.Write(re.Row.Fields); err != nil { - return "", err - } - } + data.RowsRange(func(_ int, re model1.RowEvent) bool { + _ = w.Write(re.Row.Fields) + return true + }) w.Flush() if err := w.Error(); err != nil { return "", err diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index d87f9e90c3..38231eaee4 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -5,6 +5,8 @@ package view import ( "context" + "errors" + "io/fs" "os" "testing" "time" @@ -15,6 +17,7 @@ import ( "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" @@ -41,28 +44,30 @@ func TestTableNew(t *testing.T) { v := NewTable(client.NewGVR("test")) assert.NoError(t, v.Init(makeContext())) - data := render.NewTableData() - data.Header = render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "NAME", Align: tview.AlignRight}, - render.HeaderColumn{Name: "FRED"}, - render.HeaderColumn{Name: "AGE", Time: true, Decorator: render.AgeDecorator}, - } - data.RowEvents = render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "a", "10", "3m"}, - }, + data := model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "FRED"}, + model1.HeaderColumn{Name: "AGE", Time: true, Decorator: render.AgeDecorator}, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "b", "15", "1m"}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "a", "10", "3m"}, + }, }, - }, - } - data.Namespace = "" + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "b", "15", "1m"}, + }, + }, + ), + ) + cdata := v.Update(data, false) + v.UpdateUI(cdata, data) - v.Update(data, false) assert.Equal(t, 3, v.GetRowCount()) } @@ -71,6 +76,7 @@ func TestTableViewFilter(t *testing.T) { assert.NoError(t, v.Init(makeContext())) v.SetModel(&mockTableModel{}) v.Refresh() + v.CmdBuff().SetActive(true) v.CmdBuff().SetText("blee", "") @@ -80,7 +86,7 @@ func TestTableViewFilter(t *testing.T) { func TestTableViewSort(t *testing.T) { v := NewTable(client.NewGVR("test")) assert.NoError(t, v.Init(makeContext())) - v.SetModel(&mockTableModel{}) + v.SetModel(new(mockTableModel)) uu := map[string]struct { sortCol string @@ -130,9 +136,9 @@ func (t *mockTableModel) SetInstance(string) {} func (t *mockTableModel) SetLabelFilter(string) {} func (t *mockTableModel) GetLabelFilter() string { return "" } func (t *mockTableModel) Empty() bool { return false } -func (t *mockTableModel) Count() int { return 1 } +func (t *mockTableModel) RowCount() int { return 1 } func (t *mockTableModel) HasMetrics() bool { return true } -func (t *mockTableModel) Peek() *render.TableData { return makeTableData() } +func (t *mockTableModel) Peek() *model1.TableData { return makeTableData() } func (t *mockTableModel) Refresh(context.Context) error { return nil } func (t *mockTableModel) ClusterWide() bool { return false } func (t *mockTableModel) GetNamespace() string { return "blee" } @@ -160,39 +166,39 @@ func (t *mockTableModel) ToYAML(ctx context.Context, path string) (string, error func (t *mockTableModel) InNamespace(string) bool { return true } func (t *mockTableModel) SetRefreshRate(time.Duration) {} -func makeTableData() *render.TableData { - t := render.NewTableData() - t.Header = render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "NAME", Align: tview.AlignRight}, - render.HeaderColumn{Name: "FRED"}, - render.HeaderColumn{Name: "AGE", Time: true}, - } - t.RowEvents = render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "r3", "10", "3y125d"}, - }, +func makeTableData() *model1.TableData { + return model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "FRED"}, + model1.HeaderColumn{Name: "AGE", Time: true}, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "r2", "15", "2y12d"}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "r3", "10", "3y125d"}, + }, }, - Deltas: render.DeltaRow{"", "", "20", ""}, - }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "r1", "20", "19h"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "r2", "15", "2y12d"}, + }, + Deltas: model1.DeltaRow{"", "", "20", ""}, }, - }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "r0", "15", "10s"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "r1", "20", "19h"}, + }, }, - }, - } - - return t + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "r0", "15", "10s"}, + }, + }, + ), + ) } func makeContext() context.Context { @@ -201,31 +207,9 @@ func makeContext() context.Context { return context.WithValue(ctx, internal.KeyStyles, a.Styles) } -// type ks struct{} - -// func (k ks) CurrentContextName() (string, error) { -// return "test", nil -// } - -// func (k ks) CurrentClusterName() (string, error) { -// return "test", nil -// } - -// func (k ks) CurrentNamespaceName() (string, error) { -// return "test", nil -// } - -// func (k ks) ContextNames() (map[string]struct{}, error) { -// return map[string]struct{}{"test": {}}, nil -// } - -// func (k ks) NamespaceNames(nn []v1.Namespace) []string { -// return []string{"test"} -// } - func ensureDumpDir(n string) error { config.AppDumpsDir = n - if _, err := os.Stat(n); os.IsNotExist(err) { + if _, err := os.Stat(n); errors.Is(err, fs.ErrNotExist) { return os.Mkdir(n, 0700) } if err := os.RemoveAll(n); err != nil { diff --git a/internal/view/types.go b/internal/view/types.go index 070db98c79..3f9d68627a 100644 --- a/internal/view/types.go +++ b/internal/view/types.go @@ -40,7 +40,7 @@ type ( ContextFunc func(context.Context) context.Context // BindKeysFunc adds new menu actions. - BindKeysFunc func(ui.KeyActions) + BindKeysFunc func(*ui.KeyActions) ) // ActionExtender enhances a given viewer by adding new menu actions. @@ -60,7 +60,7 @@ type Viewer interface { model.Component // Actions returns active menu bindings. - Actions() ui.KeyActions + Actions() *ui.KeyActions // App returns an app handle. App() *App diff --git a/internal/view/user.go b/internal/view/user.go index 12477d9cae..94f55fd8ec 100644 --- a/internal/view/user.go +++ b/internal/view/user.go @@ -27,9 +27,9 @@ func NewUser(gvr client.GVR) ResourceViewer { return &u } -func (u *User) bindKeys(aa ui.KeyActions) { +func (u *User) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace, tcell.KeyCtrlD, ui.KeyE) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("Rules", u.policyCmd, true), ui.KeyShiftK: ui.NewKeyAction("Sort Kind", u.GetTable().SortColCmd("KIND", true), false), }) diff --git a/internal/view/value_extender.go b/internal/view/value_extender.go index 795da9f740..8d3a244c00 100644 --- a/internal/view/value_extender.go +++ b/internal/view/value_extender.go @@ -30,10 +30,8 @@ func NewValueExtender(r ResourceViewer) ResourceViewer { return &p } -func (v *ValueExtender) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyV: ui.NewKeyAction("Values", v.valuesCmd, true), - }) +func (v *ValueExtender) bindKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyV, ui.NewKeyAction("Values", v.valuesCmd, true)) } func (v *ValueExtender) valuesCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -72,10 +70,7 @@ func showValues(ctx context.Context, app *App, path string, gvr client.GVR) { } v := NewLiveView(app, "Values", vm) - v.actions.Add(ui.KeyActions{ - ui.KeyV: ui.NewKeyAction("Toggle All Values", toggleValuesCmd, true), - }) - + v.actions.Add(ui.KeyV, ui.NewKeyAction("Toggle All Values", toggleValuesCmd, true)) if err := v.app.inject(v, false); err != nil { v.app.Flash().Err(err) } diff --git a/internal/view/vul_extender.go b/internal/view/vul_extender.go index 5c1774d62c..ebc373b6d6 100644 --- a/internal/view/vul_extender.go +++ b/internal/view/vul_extender.go @@ -25,9 +25,9 @@ func NewVulnerabilityExtender(r ResourceViewer) ResourceViewer { return &v } -func (v *VulnerabilityExtender) bindKeys(aa ui.KeyActions) { +func (v *VulnerabilityExtender) bindKeys(aa *ui.KeyActions) { if v.App().Config.K9s.ImageScans.Enable { - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyV: ui.NewKeyAction("Show Vulnerabilities", v.showVulCmd, true), ui.KeyShiftV: ui.NewKeyAction("Sort Vulnerabilities", v.GetTable().SortColCmd("VS", true), false), }) diff --git a/internal/view/workload.go b/internal/view/workload.go index 0f66508e0f..4f0ba02fcd 100644 --- a/internal/view/workload.go +++ b/internal/view/workload.go @@ -36,18 +36,14 @@ func NewWorkload(gvr client.GVR) ResourceViewer { return &w } -func (w *Workload) bindDangerousKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyE: ui.NewKeyActionWithOpts( - "Edit", - w.editCmd, +func (w *Workload) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyE: ui.NewKeyActionWithOpts("Edit", w.editCmd, ui.ActionOpts{ Visible: true, Dangerous: true, }), - tcell.KeyCtrlD: ui.NewKeyActionWithOpts( - "Delete", - w.deleteCmd, + tcell.KeyCtrlD: ui.NewKeyActionWithOpts("Delete", w.deleteCmd, ui.ActionOpts{ Visible: true, Dangerous: true, @@ -55,12 +51,12 @@ func (w *Workload) bindDangerousKeys(aa ui.KeyActions) { }) } -func (w *Workload) bindKeys(aa ui.KeyActions) { +func (w *Workload) bindKeys(aa *ui.KeyActions) { if !w.App().Config.K9s.IsReadOnly() { w.bindDangerousKeys(aa) } - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyShiftK: ui.NewKeyAction("Sort Kind", w.GetTable().SortColCmd("KIND", true), false), ui.KeyShiftS: ui.NewKeyAction("Sort Status", w.GetTable().SortColCmd(statusCol, true), false), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", w.GetTable().SortColCmd("READY", true), false), @@ -114,7 +110,7 @@ func (w *Workload) defaultContext(gvr client.GVR, fqn string) context.Context { if fqn != "" { ctx = context.WithValue(ctx, internal.KeyPath, fqn) } - if ui.IsLabelSelector(w.GetTable().CmdBuff().GetText()) { + if internal.IsLabelSelector(w.GetTable().CmdBuff().GetText()) { ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(w.GetTable().CmdBuff().GetText())) } ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(w.App().Config.ActiveNamespace())) @@ -208,34 +204,3 @@ func (w *Workload) yamlCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - -// func (w *Workload) editCmd(evt *tcell.EventKey) *tcell.EventKey { -// path := w.GetTable().GetSelectedItem() -// if path == "" { -// return evt -// } -// gvr, fqn, ok := parsePath(path) -// if !ok { -// w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) -// return evt -// } - -// w.Stop() -// defer w.Start() -// { -// ns, n := client.Namespaced(fqn) -// args := make([]string, 0, 10) -// args = append(args, "edit") -// args = append(args, gvr.R()) -// args = append(args, "-n", ns) -// args = append(args, "--context", w.App().Config.K9s.CurrentContext) -// if cfg := w.App().Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { -// args = append(args, "--kubeconfig", *cfg) -// } -// if err := runK(w.App(), shellOpts{args: append(args, n)}); err != nil { -// w.App().Flash().Errf("Edit exec failed: %s", err) -// } -// } - -// return evt -// } diff --git a/internal/view/xray.go b/internal/view/xray.go index 8bab5c70c2..34e9e0ccad 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -117,7 +117,7 @@ func (x *Xray) ExtraHints() map[string]string { func (x *Xray) SetInstance(string) {} func (x *Xray) bindKeys() { - x.Actions().Add(ui.KeyActions{ + x.Actions().Bulk(ui.KeyMap{ ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", x.activateCmd, false), tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", x.resetCmd, false), tcell.KeyEnter: ui.NewKeyAction("Goto", x.gotoCmd, true), @@ -130,7 +130,7 @@ func (x *Xray) keyEntered() { } func (x *Xray) refreshActions() { - aa := make(ui.KeyActions) + aa := ui.NewKeyActions() defer func() { if err := pluginActions(x, aa); err != nil { @@ -140,7 +140,7 @@ func (x *Xray) refreshActions() { log.Warn().Err(err).Msg("HotKeys load failed") } - x.Actions().Add(aa) + x.Actions().Merge(aa) x.app.Menu().HydrateMenu(x.Hints()) }() @@ -162,14 +162,16 @@ func (x *Xray) refreshActions() { } if client.Can(x.meta.Verbs, "edit") { - aa[ui.KeyE] = ui.NewKeyAction("Edit", x.editCmd, true) + aa.Add(ui.KeyE, ui.NewKeyAction("Edit", x.editCmd, true)) } if client.Can(x.meta.Verbs, "delete") { - aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", x.deleteCmd, true) + aa.Add(tcell.KeyCtrlD, ui.NewKeyAction("Delete", x.deleteCmd, true)) } if !dao.IsK9sMeta(x.meta) { - aa[ui.KeyY] = ui.NewKeyAction(yamlAction, x.viewCmd, true) - aa[ui.KeyD] = ui.NewKeyAction("Describe", x.describeCmd, true) + aa.Bulk(ui.KeyMap{ + ui.KeyY: ui.NewKeyAction(yamlAction, x.viewCmd, true), + ui.KeyD: ui.NewKeyAction("Describe", x.describeCmd, true), + }) } switch gvr { @@ -177,16 +179,20 @@ func (x *Xray) refreshActions() { x.Actions().Delete(tcell.KeyEnter) case "containers": x.Actions().Delete(tcell.KeyEnter) - aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true) - aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true) - aa[ui.KeyP] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true) + aa.Bulk(ui.KeyMap{ + ui.KeyS: ui.NewKeyAction("Shell", x.shellCmd, true), + ui.KeyL: ui.NewKeyAction("Logs", x.logsCmd(false), true), + ui.KeyP: ui.NewKeyAction("Logs Previous", x.logsCmd(true), true), + }) case "v1/pods": - aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true) - aa[ui.KeyA] = ui.NewKeyAction("Attach", x.attachCmd, true) - aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true) - aa[ui.KeyP] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true) + aa.Bulk(ui.KeyMap{ + ui.KeyS: ui.NewKeyAction("Shell", x.shellCmd, true), + ui.KeyA: ui.NewKeyAction("Attach", x.attachCmd, true), + ui.KeyL: ui.NewKeyAction("Logs", x.logsCmd(false), true), + ui.KeyP: ui.NewKeyAction("Logs Previous", x.logsCmd(true), true), + }) } - x.Actions().Add(aa) + x.Actions().Merge(aa) } // GetSelectedPath returns the current selection as string. @@ -454,7 +460,7 @@ func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey { func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if x.CmdBuff().IsActive() { - if ui.IsLabelSelector(x.CmdBuff().GetText()) { + if internal.IsLabelSelector(x.CmdBuff().GetText()) { x.Start() } x.CmdBuff().SetActive(false) @@ -477,16 +483,16 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { func (x *Xray) filter(root *xray.TreeNode) *xray.TreeNode { q := x.CmdBuff().GetText() - if x.CmdBuff().Empty() || ui.IsLabelSelector(q) { + if x.CmdBuff().Empty() || internal.IsLabelSelector(q) { return root } x.UpdateTitle() - if f, ok := dao.HasFuzzySelector(q); ok { + if f, ok := internal.IsFuzzySelector(q); ok { return root.Filter(f, fuzzyFilter) } - if dao.IsInverseSelector(q) { + if internal.IsInverseSelector(q) { return root.Filter(q, rxInverseFilter) } @@ -661,7 +667,7 @@ func (x *Xray) styleTitle() string { if buff == "" { return title } - if ui.IsLabelSelector(buff) { + if internal.IsLabelSelector(buff) { buff = ui.TrimLabelSelector(buff) } diff --git a/internal/xray/pod.go b/internal/xray/pod.go index 69bb293f12..dbcbf92008 100644 --- a/internal/xray/pod.go +++ b/internal/xray/pod.go @@ -65,7 +65,6 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { func (p *Pod) validate(node *TreeNode, po v1.Pod) error { var re render.Pod - phase := re.Phase(&po) ss := po.Status.ContainerStatuses cr, _, _ := re.Statuses(ss) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index a8b389e7ec..090c2e652d 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: k9s base: core20 -version: 'v0.31.9' +version: 'v0.32.0' summary: K9s is a CLI to view and manage your Kubernetes clusters. description: | K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session.