diff --git a/Makefile b/Makefile index 14a039da62..c2639ec4ad 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.30.6 +VERSION ?= v0.30.7 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/change_logs/release_v0.30.7.md b/change_logs/release_v0.30.7.md new file mode 100644 index 0000000000..b6025dfbea --- /dev/null +++ b/change_logs/release_v0.30.7.md @@ -0,0 +1,51 @@ + + +# Release v0.30.7 + +## 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! + +Thank you all for pitching in and helping flesh out issues!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2414](https://github.com/derailed/k9s/issues/2414) View pods with context filter, along with namespace filter, prompts an error if the namespace exists only in the desired context +* [#2413](https://github.com/derailed/k9s/issues/2413) Typing apply -f in command bar causes k9s to crash +* [#2407](https://github.com/derailed/k9s/issues/2407) Long-running background plugins block UI rendering + +--- + +## 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!! + +* [#2415](https://github.com/derailed/k9s/pull/2415) Add boundary check for args parser +* [#2411](https://github.com/derailed/k9s/pull/2411) Use dash as a standard word separator in skin names + + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/internal/client/client.go b/internal/client/client.go index 0cc6cf790b..3a47816749 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -352,11 +352,7 @@ func (a *APIClient) Config() *Config { // HasMetrics checks if the cluster supports metrics. func (a *APIClient) HasMetrics() bool { - err := a.supportsMetricsResources() - if err != nil { - log.Debug().Msgf("Metrics server detect failed: %s", err) - } - return err == nil + return a.supportsMetricsResources() != nil } // DialLogs returns a handle to api server for logs. diff --git a/internal/config/data/ns.go b/internal/config/data/ns.go index 2800a45d91..69ab9cd521 100644 --- a/internal/config/data/ns.go +++ b/internal/config/data/ns.go @@ -84,6 +84,10 @@ func (n *Namespace) addFavNS(ns string) { } func (n *Namespace) rmFavNS(ns string) { + if n.LockFavorites { + return + } + victim := -1 for i, f := range n.Favorites { if f == ns { diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 56c8922368..decac450a2 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -177,7 +177,7 @@ func (k *K9s) ActiveContext() (*data.Context, error) { // ActivateContext initializes the active context is not present. func (k *K9s) ActivateContext(n string) (*data.Context, error) { k.activeContextName = n - ct, err := k.ks.GetContext(k.activeContextName) + ct, err := k.ks.GetContext(n) if err != nil { return nil, err } @@ -197,6 +197,21 @@ func (k *K9s) ActivateContext(n string) (*data.Context, error) { return k.activeConfig.Context, nil } +// Reload reloads the active config from disk. +func (k *K9s) Reload() error { + ct, err := k.ks.GetContext(k.activeContextName) + if err != nil { + return err + } + + k.activeConfig, err = k.dir.Load(k.activeContextName, ct) + if err != nil { + return err + } + + return nil +} + // OverrideRefreshRate set the refresh rate manually. func (k *K9s) OverrideRefreshRate(r int) { k.manualRefreshRate = r diff --git a/internal/config/mock/test_helpers.go b/internal/config/mock/test_helpers.go index 2fa94be082..49db4a24c6 100644 --- a/internal/config/mock/test_helpers.go +++ b/internal/config/mock/test_helpers.go @@ -104,11 +104,17 @@ func (m mockKubeSettings) ContextNames() (map[string]struct{}, error) { return mm, nil } -type mockConnection struct{} +type mockConnection struct { + ct string +} func NewMockConnection() mockConnection { return mockConnection{} } +func NewMockConnectionWithContext(ct string) mockConnection { + return mockConnection{ct: ct} +} + func (m mockConnection) CanI(ns, gvr string, verbs []string) (bool, error) { return true, nil } @@ -155,7 +161,7 @@ func (m mockConnection) CheckConnectivity() bool { return false } func (m mockConnection) ActiveContext() string { - return "" + return m.ct } func (m mockConnection) ActiveNamespace() string { return "" diff --git a/internal/dao/helpers.go b/internal/dao/helpers.go index d837d6ead9..f4818f4863 100644 --- a/internal/dao/helpers.go +++ b/internal/dao/helpers.go @@ -9,8 +9,6 @@ import ( "math" "regexp" - "github.com/derailed/tview" - runewidth "github.com/mattn/go-runewidth" "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -21,7 +19,7 @@ const defaultServiceAccount = "default" var ( inverseRx = regexp.MustCompile(`\A\!`) - fuzzyRx = regexp.MustCompile(`\A\-f`) + fuzzyRx = regexp.MustCompile(`\A-f\s?([\w-]+)\b`) ) func inList(ll []string, s string) bool { @@ -41,12 +39,14 @@ func IsInverseSelector(s string) bool { return inverseRx.MatchString(s) } -// IsFuzzySelector checks if filter is fuzzy or not. -func IsFuzzySelector(s string) bool { - if s == "" { - return false +// HasFuzzySelector checks if query is fuzzy. +func HasFuzzySelector(s string) (string, bool) { + mm := fuzzyRx.FindStringSubmatch(s) + if len(mm) != 2 { + return "", false } - return fuzzyRx.MatchString(s) + + return mm[1], true } func toPerc(v1, v2 float64) float64 { @@ -56,11 +56,6 @@ func toPerc(v1, v2 float64) float64 { return math.Round((v1 / v2) * 100) } -// Truncate a string to the given l and suffix ellipsis if needed. -func Truncate(str string, width int) string { - return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) -} - // ToYAML converts a resource to its YAML representation. func ToYAML(o runtime.Object, showManaged bool) (string, error) { if o == nil { diff --git a/internal/dao/log_items.go b/internal/dao/log_items.go index e84c874002..d8cd1707cf 100644 --- a/internal/dao/log_items.go +++ b/internal/dao/log_items.go @@ -174,8 +174,8 @@ func (l *LogItems) Filter(index int, q string, showTime bool) ([]int, [][]int, e if q == "" { return nil, nil, nil } - if IsFuzzySelector(q) { - mm, ii := l.fuzzyFilter(index, strings.TrimSpace(q[2:]), showTime) + if f, ok := HasFuzzySelector(q); ok { + mm, ii := l.fuzzyFilter(index, f, showTime) return mm, ii, nil } matches, indices, err := l.filterLogs(index, q, showTime) diff --git a/internal/model/describe.go b/internal/model/describe.go index 82ca20b41c..29c380caca 100644 --- a/internal/model/describe.go +++ b/internal/model/describe.go @@ -65,8 +65,8 @@ func (d *Describe) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if dao.IsFuzzySelector(q) { - return d.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + if f, ok := dao.HasFuzzySelector(q); ok { + return d.fuzzyFilter(strings.TrimSpace(f), lines) } return rxFilter(q, lines) } diff --git a/internal/model/helpers.go b/internal/model/helpers.go index 0646b1ce0f..2173149472 100644 --- a/internal/model/helpers.go +++ b/internal/model/helpers.go @@ -9,8 +9,6 @@ import ( "time" "github.com/cenkalti/backoff/v4" - "github.com/derailed/tview" - "github.com/mattn/go-runewidth" "github.com/sahilm/fuzzy" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -28,11 +26,6 @@ func FQN(ns, n string) string { return ns + "/" + n } -// Truncate a string to the given l and suffix ellipsis if needed. -func Truncate(str string, width int) string { - return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) -} - // NewExpBackOff returns a new exponential backoff timer. func NewExpBackOff(ctx context.Context, start, max time.Duration) backoff.BackOffContext { bf := backoff.NewExponentialBackOff() diff --git a/internal/model/helpers_test.go b/internal/model/helpers_test.go index 813889e27e..69ed2529e5 100644 --- a/internal/model/helpers_test.go +++ b/internal/model/helpers_test.go @@ -33,34 +33,3 @@ func TestMetaFQN(t *testing.T) { }) } } - -func TestTruncate(t *testing.T) { - uu := map[string]struct { - data string - size int - e string - }{ - "same": { - data: "fred", - size: 4, - e: "fred", - }, - "small": { - data: "fred", - size: 10, - e: "fred", - }, - "larger": { - data: "fred", - size: 3, - e: "fr…", - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, model.Truncate(u.data, u.size)) - }) - } -} diff --git a/internal/model/rev_values.go b/internal/model/rev_values.go index e25ef7b410..08badab6d5 100644 --- a/internal/model/rev_values.go +++ b/internal/model/rev_values.go @@ -84,8 +84,8 @@ func (v *RevValues) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if dao.IsFuzzySelector(q) { - return v.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + if f, ok := dao.HasFuzzySelector(q); ok { + return v.fuzzyFilter(strings.TrimSpace(f), lines) } return rxFilter(q, lines) } diff --git a/internal/model/text.go b/internal/model/text.go index d5c5fe893b..64e4d4f90a 100644 --- a/internal/model/text.go +++ b/internal/model/text.go @@ -111,8 +111,8 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if dao.IsFuzzySelector(q) { - return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + if f, ok := dao.HasFuzzySelector(q); ok { + return t.fuzzyFilter(strings.TrimSpace(f), lines) } return rxFilter(q, lines) } diff --git a/internal/model/values.go b/internal/model/values.go index 890facd0b9..25870d5a3b 100644 --- a/internal/model/values.go +++ b/internal/model/values.go @@ -113,8 +113,8 @@ func (v *Values) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if dao.IsFuzzySelector(q) { - return v.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + if f, ok := dao.HasFuzzySelector(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 8ef8d64d1b..7e7dd1a243 100644 --- a/internal/model/yaml.go +++ b/internal/model/yaml.go @@ -74,8 +74,8 @@ func (y *YAML) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if dao.IsFuzzySelector(q) { - return y.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + if f, ok := dao.HasFuzzySelector(q); ok { + return y.fuzzyFilter(strings.TrimSpace(f), lines) } return rxFilter(q, lines) } diff --git a/internal/render/helpers.go b/internal/render/helpers.go index 548912f8e0..bafe900a20 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -13,7 +13,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/vul" "github.com/derailed/tview" - runewidth "github.com/mattn/go-runewidth" + "github.com/mattn/go-runewidth" "github.com/rs/zerolog/log" "golang.org/x/text/language" "golang.org/x/text/message" diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go index 26eb18ca17..05b0f89d6b 100644 --- a/internal/render/helpers_test.go +++ b/internal/render/helpers_test.go @@ -224,18 +224,33 @@ func TestNa(t *testing.T) { } func TestTruncate(t *testing.T) { - uu := []struct { - s string - l int - e string + uu := map[string]struct { + data string + size int + e string }{ - {"fred", 3, "fr…"}, - {"fred", 2, "f…"}, - {"fred", 10, "fred"}, + "same": { + data: "fred", + size: 4, + e: "fred", + }, + "small": { + data: "fred", + size: 10, + e: "fred", + }, + "larger": { + data: "fred", + size: 3, + e: "fr…", + }, } - for _, u := range uu { - assert.Equal(t, u.e, Truncate(u.s, u.l)) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, Truncate(u.data, u.size)) + }) } } diff --git a/internal/ui/app.go b/internal/ui/app.go index b7b4d22816..1b843f9136 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -39,7 +39,7 @@ func NewApp(cfg *config.Config, context string) *App { flash: model.NewFlash(model.DefaultFlashDelay), cmdBuff: model.NewFishBuff(':', model.CommandBuffer), } - a.ReloadStyles(context) + a.ReloadStyles() a.views = map[string]tview.Primitive{ "menu": NewMenu(a.Styles), @@ -135,8 +135,8 @@ func (a *App) StylesChanged(s *config.Styles) { } // ReloadStyles reloads skin file. -func (a *App) ReloadStyles(context string) { - a.RefreshStyles(context) +func (a *App) ReloadStyles() { + a.RefreshStyles() } // Conn returns an api server connection. diff --git a/internal/ui/config.go b/internal/ui/config.go index 11539ea3d8..1ddb04f1fb 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -7,6 +7,7 @@ import ( "context" "errors" "os" + "path/filepath" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/render" @@ -82,8 +83,8 @@ func (c *Configurator) RefreshCustomViews() error { return c.CustomView.Load(config.AppViewsFile) } -// StylesWatcher watches for skin file changes. -func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error { +// SkinsDirWatcher watches for skin directory file changes. +func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) error { if !c.HasSkin() { return nil } @@ -100,7 +101,7 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error if evt.Name == c.skinFile && evt.Op != fsnotify.Chmod { log.Debug().Msgf("Skin changed: %s", c.skinFile) s.QueueUpdateDraw(func() { - c.RefreshStyles(c.Config.K9s.ActiveContextName()) + c.RefreshStyles() }) } case err := <-w.Errors: @@ -120,48 +121,127 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error return w.Add(config.AppSkinsDir) } -// RefreshStyles load for skin configuration changes. -func (c *Configurator) RefreshStyles(context string) { - cluster := "na" - if c.Config != nil { - if ct, err := c.Config.K9s.ActiveContext(); err == nil { - cluster = ct.ClusterName +// ConfigWatcher watches for skin settings changes. +func (c *Configurator) ConfigWatcher(ctx context.Context, s synchronizer) error { + w, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + go func() { + for { + select { + case evt := <-w.Events: + if evt.Has(fsnotify.Create) || evt.Has(fsnotify.Write) { + log.Debug().Msgf("ConfigWatcher file changed: %s -- %#v", evt.Name, evt.Op.String()) + if evt.Name == config.AppConfigFile { + if err := c.Config.Load(evt.Name); err != nil { + log.Error().Err(err).Msgf("Config reload failed") + } + } else { + if err := c.Config.K9s.Reload(); err != nil { + log.Error().Err(err).Msgf("Context config reload failed") + } + } + s.QueueUpdateDraw(func() { + c.RefreshStyles() + }) + } + case err := <-w.Errors: + log.Info().Err(err).Msg("ConfigWatcher failed") + return + case <-ctx.Done(): + log.Debug().Msg("ConfigWatcher CANCELED") + if err := w.Close(); err != nil { + log.Error().Err(err).Msg("Canceling ConfigWatcher") + } + return + } } + }() + + log.Debug().Msgf("ConfigWatcher watching: %q", config.AppConfigFile) + if err := w.Add(config.AppConfigFile); err != nil { + return err } - if bc, err := config.EnsureBenchmarksCfgFile(cluster, context); err != nil { - log.Warn().Err(err).Msgf("No benchmark config file found for context: %s", context) - } else { - c.BenchFile = bc + cl, ct, ok := c.activeConfig() + if !ok { + return nil } + ctConfigFile := filepath.Join(config.AppContextConfig(cl, ct)) + log.Debug().Msgf("ConfigWatcher watching: %q", ctConfigFile) + return w.Add(ctConfigFile) +} + +func (c *Configurator) activeSkin() (string, bool) { + var skin string + if c.Config == nil || c.Config.K9s == nil { + return skin, false + } + + if ct, err := c.Config.K9s.ActiveContext(); err == nil { + skin = ct.Skin + } + if skin == "" { + skin = c.Config.K9s.UI.Skin + } + + return skin, skin != "" +} + +func (c *Configurator) activeConfig() (cluster string, context string, ok bool) { + if c.Config == nil || c.Config.K9s == nil { + return + } + ct, err := c.Config.K9s.ActiveContext() + if err != nil { + return + } + cluster, context = ct.ClusterName, c.Config.K9s.ActiveContextName() + if cluster != "" && context != "" { + ok = true + } + + return +} + +// RefreshStyles load for skin configuration changes. +func (c *Configurator) RefreshStyles() { if c.Styles == nil { c.Styles = config.NewStyles() } else { c.Styles.Reset() } - var skin string - if c.Config != nil { - skin = c.Config.K9s.UI.Skin - if ct, err := c.Config.K9s.ActiveContext(); err != nil { - log.Warn().Msgf("No active context found. Using default skin") - } else if ct.Skin != "" { - skin = ct.Skin - } + cl, ct, ok := c.activeConfig() + if !ok { + return } - if skin == "" { + + if bc, err := config.EnsureBenchmarksCfgFile(cl, ct); err != nil { + log.Warn().Err(err).Msgf("No benchmark config file found: %q@%q", cl, ct) + } else { + c.BenchFile = bc + } + + skin, ok := c.activeSkin() + if !ok { + log.Debug().Msgf("No custom skin found. Loading default") c.updateStyles("") return } - var skinFile = config.SkinFileFromName(skin) + skinFile := config.SkinFileFromName(skin) if err := c.Styles.Load(skinFile); err != nil { if errors.Is(err, os.ErrNotExist) { log.Warn().Msgf("Skin file %q not found in skins dir: %s", skinFile, config.AppSkinsDir) } else { log.Error().Msgf("Failed to parse skin file -- %s: %s.", skinFile, err) } + c.updateStyles("") } else { + log.Debug().Msgf("Loading skin file: %q", skinFile) c.updateStyles(skinFile) } } diff --git a/internal/ui/config_int_test.go b/internal/ui/config_int_test.go new file mode 100644 index 0000000000..8f4c9d62eb --- /dev/null +++ b/internal/ui/config_int_test.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package ui + +import ( + "os" + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" + "github.com/stretchr/testify/assert" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +func Test_activeConfig(t *testing.T) { + os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config") + assert.NoError(t, config.InitLocs()) + + cl, ct := "cl-1", "ct-1-1" + uu := map[string]struct { + cl, ct string + cfg *Configurator + ok bool + }{ + "empty": { + cfg: &Configurator{}, + }, + + "plain": { + cfg: &Configurator{Config: config.NewConfig( + mock.NewMockKubeSettings(&genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + }))}, + cl: cl, + ct: ct, + ok: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := u.cfg + if cfg.Config != nil { + _, err := cfg.Config.K9s.ActivateContext(ct) + assert.NoError(t, err) + } + cl, ct, ok := cfg.activeConfig() + assert.Equal(t, u.ok, ok) + if ok { + assert.Equal(t, u.cl, cl) + assert.Equal(t, u.ct, ct) + } + }) + } +} diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go index 3a91554ccf..b8a81c27c4 100644 --- a/internal/ui/config_test.go +++ b/internal/ui/config_test.go @@ -18,18 +18,9 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" ) -func TestBenchConfig(t *testing.T) { - os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config") - assert.NoError(t, config.InitLocs()) - defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir)) - - bc, error := config.EnsureBenchmarksCfgFile("cl-1", "ct-1") - assert.NoError(t, error) - assert.Equal(t, "/tmp/test-config/clusters/cl-1/ct-1/benchmarks.yaml", bc) -} - func TestSkinnedContext(t *testing.T) { os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config") + assert.NoError(t, config.InitLocs()) defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir)) @@ -50,10 +41,22 @@ func TestSkinnedContext(t *testing.T) { cfg.Config.K9s = config.NewK9s( mock.NewMockConnection(), mock.NewMockKubeSettings(&flags)) + _, err = cfg.Config.K9s.ActivateContext("ct-1-1") + assert.NoError(t, err) cfg.Config.K9s.UI = config.UI{Skin: "black_and_wtf"} - cfg.RefreshStyles("ct-1") + cfg.RefreshStyles() assert.True(t, cfg.HasSkin()) assert.Equal(t, tcell.ColorGhostWhite.TrueColor(), render.StdColor) assert.Equal(t, tcell.ColorWhiteSmoke.TrueColor(), render.ErrColor) } + +func TestBenchConfig(t *testing.T) { + os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config") + assert.NoError(t, config.InitLocs()) + defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir)) + + bc, error := config.EnsureBenchmarksCfgFile("cl-1", "ct-1") + assert.NoError(t, error) + assert.Equal(t, "/tmp/test-config/clusters/cl-1/ct-1/benchmarks.yaml", bc) +} diff --git a/internal/ui/table.go b/internal/ui/table.go index acf4d866ba..4f51a2f97b 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -12,6 +12,7 @@ import ( "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/render" "github.com/derailed/k9s/internal/vul" @@ -407,14 +408,13 @@ func (t *Table) filtered(data *render.TableData) *render.TableData { } q := t.cmdBuff.GetText() - if IsFuzzySelector(q) { - return fuzzyFilter(q[2:], filtered) + if f, ok := dao.HasFuzzySelector(q); ok { + return fuzzyFilter(f, filtered) } - filtered, err := rxFilter(q, IsInverseSelector(q), filtered) + filtered, err := rxFilter(q, dao.IsInverseSelector(q), filtered) if err != nil { - log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp") - // t.cmdBuff.ClearText(true) + log.Error().Err(errors.New("invalid filter expression")).Msg("Regexp") } return filtered @@ -471,9 +471,9 @@ func (t *Table) styleTitle() string { buff := t.cmdBuff.GetText() if IsLabelSelector(buff) { - buff = truncate(TrimLabelSelector(buff), maxTruncate) + buff = render.Truncate(TrimLabelSelector(buff), maxTruncate) } else if l := t.GetModel().GetLabelFilter(); l != "" { - buff = truncate(l, maxTruncate) + buff = render.Truncate(l, maxTruncate) } if buff == "" { diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index 68650b7c64..bd6ea48118 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -42,9 +42,7 @@ const ( var ( // LabelRx identifies a label query. - LabelRx = regexp.MustCompile(`\A\-l`) - inverseRx = regexp.MustCompile(`\A\!`) - fuzzyRx = regexp.MustCompile(`\A\-f`) + LabelRx = regexp.MustCompile(`\A\-l`) ) func mustExtractStyles(ctx context.Context) *config.Styles { @@ -67,9 +65,6 @@ func TrimCell(tv *SelectTable, row, col int) string { // IsLabelSelector checks if query is a label query. func IsLabelSelector(s string) bool { - if s == "" { - return false - } if LabelRx.MatchString(s) { return true } @@ -77,22 +72,6 @@ func IsLabelSelector(s string) bool { return !strings.Contains(s, " ") && cmd.ToLabels(s) != nil } -// IsFuzzySelector checks if query is fuzzy. -func IsFuzzySelector(s string) bool { - if s == "" { - return false - } - return fuzzyRx.MatchString(s) -} - -// IsInverseSelector checks if inverse char has been provided. -func IsInverseSelector(s string) bool { - if s == "" { - return false - } - return inverseRx.MatchString(s) -} - // TrimLabelSelector extracts label query. func TrimLabelSelector(s string) string { if strings.Index(s, "-l") == 0 { @@ -102,14 +81,6 @@ func TrimLabelSelector(s string) string { return s } -func truncate(s string, max int) string { - if len(s) < max { - return s - } - - return s[:max] + "..." -} - // SkinTitle decorates a title. func SkinTitle(fmat string, style config.Frame) string { bgColor := style.Title.BgColor diff --git a/internal/ui/table_helper_test.go b/internal/ui/table_helper_test.go index dc86cd396c..219ffaa3d3 100644 --- a/internal/ui/table_helper_test.go +++ b/internal/ui/table_helper_test.go @@ -6,6 +6,7 @@ package ui import ( "testing" + "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) @@ -16,7 +17,7 @@ func TestTruncate(t *testing.T) { "empty": {}, "max": { s: "/app.kubernetes.io/instance=prom,app.kubernetes.io/name=prometheus,app.kubernetes.io/component=server", - e: "/app.kubernetes.io/instance=prom,app.kubernetes.io...", + e: "/app.kubernetes.io/instance=prom,app.kubernetes.i…", }, "less": { s: "app=fred,env=blee", @@ -27,7 +28,7 @@ func TestTruncate(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, truncate(u.s, 50)) + assert.Equal(t, u.e, render.Truncate(u.s, 50)) }) } } diff --git a/internal/view/actions.go b/internal/view/actions.go index 324e623318..7ce1e6da6b 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -142,9 +142,9 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler { pipes: p.Pipes, args: args, } - suspend, errChan := run(r.App(), opts) + suspend, errChan, statusChan := run(r.App(), opts) if !suspend { - r.App().Flash().Info("Plugin command failed!") + r.App().Flash().Infof("Plugin command failed: %q", p.Description) return } var errs error @@ -155,7 +155,12 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler { r.App().cowCmd(errs.Error()) return } - r.App().Flash().Info("Plugin command launched successfully!") + go func() { + for st := range statusChan { + r.App().Flash().Infof("Plugin command launched successfully: %q", st) + } + }() + } if p.Confirm { msg := fmt.Sprintf("Run?\n%s %s", p.Command, strings.Join(args, " ")) diff --git a/internal/view/app.go b/internal/view/app.go index e6bfd1a1c5..20ceb2c897 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -324,8 +324,12 @@ func (a *App) Resume() { ctx, a.cancelFn = context.WithCancel(context.Background()) go a.clusterUpdater(ctx) - if err := a.StylesWatcher(ctx, a); err != nil { - log.Warn().Err(err).Msgf("Styles watcher failed") + if err := a.ConfigWatcher(ctx, a); err != nil { + log.Warn().Err(err).Msgf("ConfigWatcher failed") + } + + if err := a.SkinsDirWatcher(ctx, a); err != nil { + log.Warn().Err(err).Msgf("SkinsWatcher failed") } if err := a.CustomViewsWatcher(ctx, a); err != nil { log.Warn().Err(err).Msgf("CustomView watcher failed") @@ -408,17 +412,9 @@ func (a *App) switchNS(ns string) error { if a.Config.ActiveNamespace() == ns { return nil } - if ns == client.ClusterScope { ns = client.BlankNamespace } - ok, err := a.isValidNS(ns) - if err != nil { - return err - } - if !ok { - return fmt.Errorf("switchns - invalid namespace: %q", ns) - } if err := a.Config.SetActiveNamespace(ns); err != nil { return err } @@ -469,6 +465,7 @@ func (a *App) switchContext(ci *cmd.Interpreter) error { } ns := a.Config.ActiveNamespace() if !a.Conn().IsValidNamespace(ns) { + a.Flash().Errf("Unable to validate namespace %q. Using %q namespace", ns, client.DefaultNamespace) ns = client.DefaultNamespace if err := a.Config.SetActiveNamespace(ns); err != nil { return err @@ -481,7 +478,7 @@ func (a *App) switchContext(ci *cmd.Interpreter) error { log.Debug().Msgf("--> Switching Context %q -- %q -- %q", name, ns, a.Config.ActiveView()) a.Flash().Infof("Switching context to %q::%q", name, ns) - a.ReloadStyles(name) + a.ReloadStyles() a.gotoResource(a.Config.ActiveView(), "", true) a.clusterModel.Reset(a.factory) } diff --git a/internal/view/cmd/args.go b/internal/view/cmd/args.go index d242941429..a594cf55ea 100644 --- a/internal/view/cmd/args.go +++ b/internal/view/cmd/args.go @@ -11,6 +11,7 @@ const ( nsKey = "ns" topicKey = "topic" filterKey = "filter" + fuzzyKey = "fuzzy" labelKey = "labels" contextKey = "context" ) @@ -30,8 +31,12 @@ func newArgs(p *Interpreter, aa []string) args { args[contextKey] = a[1:] case strings.Index(a, fuzzyFlag) == 0: - if i++; i < len(aa) { - args[filterKey] = strings.ToLower(strings.TrimSpace(aa[i])) + if a == fuzzyFlag { + if i++; i < len(aa) { + args[fuzzyKey] = strings.ToLower(strings.TrimSpace(aa[i])) + } + } else { + args[fuzzyKey] = strings.ToLower(a[2:]) } case strings.Index(a, filterFlag) == 0: @@ -67,7 +72,8 @@ func newArgs(p *Interpreter, aa []string) args { func (a args) hasFilters() bool { _, fok := a[filterKey] + _, zok := a[fuzzyKey] _, lok := a[labelKey] - return fok || lok + return fok || zok || lok } diff --git a/internal/view/cmd/args_test.go b/internal/view/cmd/args_test.go index 8efe229ef3..9c1a7e2a28 100644 --- a/internal/view/cmd/args_test.go +++ b/internal/view/cmd/args_test.go @@ -42,7 +42,12 @@ func TestFlagsNew(t *testing.T) { "fuzzy-filter": { i: NewInterpreter("po"), aa: []string{"-f", "fred"}, - ll: args{filterKey: "fred"}, + ll: args{fuzzyKey: "fred"}, + }, + "fuzzy-filter-nospace": { + i: NewInterpreter("po"), + aa: []string{"-ffred"}, + ll: args{fuzzyKey: "fred"}, }, "filter+ns": { i: NewInterpreter("po"), @@ -72,23 +77,43 @@ func TestFlagsNew(t *testing.T) { "full-monty": { i: NewInterpreter("po"), aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg"}, - ll: args{filterKey: "zorg", labelKey: "app=fred", nsKey: "ns1"}, + ll: args{ + filterKey: "zorg", + fuzzyKey: "blee", + labelKey: "app=fred", + nsKey: "ns1", + }, }, "full-monty+ctx": { i: NewInterpreter("po"), aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg", "@ctx1"}, - ll: args{filterKey: "zorg", labelKey: "app=fred", nsKey: "ns1", contextKey: "ctx1"}, + ll: args{ + filterKey: "zorg", + fuzzyKey: "blee", + labelKey: "app=fred", + nsKey: "ns1", + contextKey: "ctx1", + }, }, "caps": { i: NewInterpreter("po"), aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg", "@Dev"}, - ll: args{filterKey: "zorg", labelKey: "app=fred", nsKey: "ns1", contextKey: "Dev"}, + ll: args{ + filterKey: "zorg", + fuzzyKey: "blee", + labelKey: "app=fred", + nsKey: "ns1", + contextKey: "Dev"}, }, "ctx": { i: NewInterpreter("ctx"), aa: []string{"Dev"}, ll: args{contextKey: "Dev"}, }, + "bork": { + i: NewInterpreter("apply -f"), + ll: args{}, + }, } for k := range uu { diff --git a/internal/view/cmd/helpers.go b/internal/view/cmd/helpers.go index 6ccbcfb7c6..9ceec0dae8 100644 --- a/internal/view/cmd/helpers.go +++ b/internal/view/cmd/helpers.go @@ -11,8 +11,10 @@ import ( ) func ToLabels(s string) map[string]string { - ll := strings.Split(s, ",") - lbls := make(map[string]string, len(ll)) + var ( + ll = strings.Split(s, ",") + lbls = make(map[string]string, len(ll)) + ) for _, l := range ll { kv := strings.Split(l, "=") if len(kv) < 2 || kv[0] == "" || kv[1] == "" { @@ -20,7 +22,6 @@ func ToLabels(s string) map[string]string { } lbls[kv[0]] = kv[1] } - if len(lbls) == 0 { return nil } diff --git a/internal/view/cmd/interpreter.go b/internal/view/cmd/interpreter.go index b95149c5b9..e47a911d30 100644 --- a/internal/view/cmd/interpreter.go +++ b/internal/view/cmd/interpreter.go @@ -7,12 +7,14 @@ import ( "strings" ) +// Interpreter tracks user prompt input. type Interpreter struct { line string cmd string args args } +// NewInterpreter returns a new instance. func NewInterpreter(s string) *Interpreter { c := Interpreter{ line: s, @@ -32,20 +34,24 @@ func (c *Interpreter) grok() { c.args = newArgs(c, ff[1:]) } +// HasNS returns true if ns is present in prompt. func (c *Interpreter) HasNS() bool { ns, ok := c.args[nsKey] return ok && ns != "" } +// Cmd returns the command. func (c *Interpreter) Cmd() string { return c.cmd } +// IsBlank returns true if prompt is empty. func (c *Interpreter) IsBlank() bool { return c.line == "" } +// Amend merges prompts. func (c *Interpreter) Amend(c1 *Interpreter) { c.cmd = c1.cmd if c.args == nil { @@ -58,6 +64,7 @@ func (c *Interpreter) Amend(c1 *Interpreter) { } } +// Reset resets with new command. func (c *Interpreter) Reset(s string) *Interpreter { c.line = s c.grok() @@ -65,50 +72,60 @@ func (c *Interpreter) Reset(s string) *Interpreter { return c } +// GetLine teturns the prompt. func (c *Interpreter) GetLine() string { return strings.TrimSpace(c.line) } +// IsCowCmd returns true if cow cmd is detected. func (c *Interpreter) IsCowCmd() bool { return c.cmd == cowCmd } +// IsHelpCmd returns true if help cmd is detected. func (c *Interpreter) IsHelpCmd() bool { _, ok := helpCmd[c.cmd] return ok } +// IsBailCmd returns true if quit cmd is detected. func (c *Interpreter) IsBailCmd() bool { _, ok := bailCmd[c.cmd] return ok } +// IsAliasCmd returns true if alias cmd is detected. func (c *Interpreter) IsAliasCmd() bool { _, ok := aliasCmd[c.cmd] return ok } +// IsXrayCmd returns true if xray cmd is detected. func (c *Interpreter) IsXrayCmd() bool { _, ok := xrayCmd[c.cmd] return ok } +// IsContextCmd returns true if context cmd is detected. func (c *Interpreter) IsContextCmd() bool { _, ok := contextCmd[c.cmd] return ok } +// IsDirCmd returns true if dir cmd is detected. func (c *Interpreter) IsDirCmd() bool { _, ok := dirCmd[c.cmd] return ok } +// IsRBACCmd returns true if rbac cmd is detected. func (c *Interpreter) IsRBACCmd() bool { return c.cmd == canCmd } +// ContextArg returns context cmd arg. func (c *Interpreter) ContextArg() (string, bool) { if !c.IsContextCmd() { return "", false @@ -117,26 +134,32 @@ func (c *Interpreter) ContextArg() (string, bool) { return c.args[contextKey], true } +// ResetContextArg deletes context arg. func (c *Interpreter) ResetContextArg() { delete(c.args, contextFlag) } +// DirArg returns the directory is present. func (c *Interpreter) DirArg() (string, bool) { - if !c.IsDirCmd() || c.args[topicKey] == "" { + if !c.IsDirCmd() { return "", false } + d, ok := c.args[topicKey] - return c.args[topicKey], true + return d, ok && d != "" } +// CowArg returns the cow message. func (c *Interpreter) CowArg() (string, bool) { - if !c.IsCowCmd() || c.args[nsKey] == "" { + if !c.IsCowCmd() { return "", false } + m, ok := c.args[nsKey] - return c.args[nsKey], true + return m, ok && m != "" } +// RBACArgs returns the subject and topic is any. func (c *Interpreter) RBACArgs() (string, string, bool) { if !c.IsRBACCmd() { return "", "", false @@ -149,6 +172,7 @@ func (c *Interpreter) RBACArgs() (string, string, bool) { return tt[1], tt[2], true } +// XRayArgs return the gvr and ns if any. func (c *Interpreter) XrayArgs() (string, string, bool) { if !c.IsXrayCmd() { return "", "", false @@ -169,32 +193,37 @@ func (c *Interpreter) XrayArgs() (string, string, bool) { } } +// FilterArg returns the current filter if any. func (c *Interpreter) FilterArg() (string, bool) { f, ok := c.args[filterKey] - return f, ok + return f, ok && f != "" } +// FuzzyArg returns the fuzzy filter if any. +func (c *Interpreter) FuzzyArg() (string, bool) { + f, ok := c.args[fuzzyKey] + + return f, ok && f != "" +} + +// NSArg returns the current ns if any. func (c *Interpreter) NSArg() (string, bool) { ns, ok := c.args[nsKey] - return ns, ok + return ns, ok && ns != "" } +// HasContext returns the current context if any. func (c *Interpreter) HasContext() (string, bool) { ctx, ok := c.args[contextKey] - if !ok || ctx == "" { - return "", false - } - return ctx, ok + return ctx, ok && ctx != "" } +// LabelsArg return the labels map if any. func (c *Interpreter) LabelsArg() (map[string]string, bool) { ll, ok := c.args[labelKey] - if !ok { - return nil, false - } - return ToLabels(ll), true + return ToLabels(ll), ok } diff --git a/internal/view/cmd/interpreter_test.go b/internal/view/cmd/interpreter_test.go index cc5c9ca989..7266043dff 100644 --- a/internal/view/cmd/interpreter_test.go +++ b/internal/view/cmd/interpreter_test.go @@ -317,11 +317,6 @@ func TestContextCmd(t *testing.T) { ctx string }{ "empty": {}, - "plain": { - cmd: "context", - ok: true, - ctx: "", - }, "happy-full": { cmd: "context ctx1", ok: true, diff --git a/internal/view/command.go b/internal/view/command.go index 3604c89aa1..30f2e3cb2f 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -79,13 +79,13 @@ func allowedXRay(gvr client.GVR) bool { } func (c *Command) contextCmd(p *cmd.Interpreter) error { - ctx, ok := p.ContextArg() + ct, ok := p.ContextArg() if !ok { return fmt.Errorf("invalid command use `context xxx`") } - if ctx != "" { - return useContext(c.app, ctx) + if ct != "" { + return useContext(c.app, ct) } gvr, v, err := c.viewMetaFor(p) @@ -93,7 +93,7 @@ func (c *Command) contextCmd(p *cmd.Interpreter) error { return err } - return c.exec(p, gvr, c.componentFor(gvr, ctx, v), true) + return c.exec(p, gvr, c.componentFor(gvr, ct, v), true) } func (c *Command) xrayCmd(p *cmd.Interpreter) error { @@ -169,6 +169,9 @@ func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack bool) error { if f, ok := p.FilterArg(); ok { co.SetFilter(f) } + if f, ok := p.FuzzyArg(); ok { + co.SetFilter("-f " + f) + } if ll, ok := p.LabelsArg(); ok { co.SetLabelFilter(ll) } diff --git a/internal/view/exec.go b/internal/view/exec.go index ed54d98888..effe8c9502 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -16,6 +16,8 @@ import ( "syscall" "time" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" @@ -75,7 +77,7 @@ func runK(a *App, opts shellOpts) error { } opts.binary = bin - suspended, errChan := run(a, opts) + suspended, errChan, _ := run(a, opts) if !suspended { return fmt.Errorf("unable to run command") } @@ -87,28 +89,29 @@ func runK(a *App, opts shellOpts) error { return errs } -func run(a *App, opts shellOpts) (bool, chan error) { +func run(a *App, opts shellOpts) (bool, chan error, chan string) { errChan := make(chan error, 1) + statusChan := make(chan string, 1) if opts.background { - if err := execute(opts); err != nil { + if err := execute(opts, statusChan); err != nil { errChan <- err a.Flash().Errf("Exec failed %q: %s", opts, err) } close(errChan) - return true, errChan + return true, errChan, statusChan } a.Halt() defer a.Resume() return a.Suspend(func() { - if err := execute(opts); err != nil { + if err := execute(opts, statusChan); err != nil { errChan <- err a.Flash().Errf("Exec failed %q: %s", opts, err) } close(errChan) - }), errChan + }), errChan, statusChan } func edit(a *App, opts shellOpts) bool { @@ -122,18 +125,20 @@ func edit(a *App, opts shellOpts) bool { } opts.binary, opts.background = bin, false - suspended, errChan := run(a, opts) + suspended, errChan, _ := run(a, opts) if !suspended { a.Flash().Errf("edit command failed") } + status := true for e := range errChan { a.Flash().Err(e) - return false + status = false } - return true + + return status } -func execute(opts shellOpts) error { +func execute(opts shellOpts, statusChan chan<- string) error { if opts.clear { clearScreen() } @@ -174,7 +179,7 @@ func execute(opts shellOpts) error { } var o, e bytes.Buffer - err := pipe(ctx, opts, &o, &e, cmds...) + err := pipe(ctx, opts, statusChan, &o, &e, cmds...) if err != nil { log.Err(err).Msgf("Command failed") return errors.Join(err, fmt.Errorf("%s", e.String())) @@ -458,7 +463,7 @@ func asResource(r config.Limits) v1.ResourceRequirements { } } -func pipe(_ context.Context, opts shellOpts, w, e io.Writer, cmds ...*exec.Cmd) error { +func pipe(_ context.Context, opts shellOpts, statusChan chan<- string, w, e io.Writer, cmds ...*exec.Cmd) error { if len(cmds) == 0 { return nil } @@ -466,8 +471,17 @@ func pipe(_ context.Context, opts shellOpts, w, e io.Writer, cmds ...*exec.Cmd) if len(cmds) == 1 { cmd := cmds[0] if opts.background { - cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, w, e - return cmd.Run() + go func() { + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, w, e + if err := cmd.Run(); err != nil { + log.Error().Err(err).Msgf("Command failed: %s", err) + } else { + statusChan <- fmt.Sprintf("Command completed successfully: %q", render.Truncate(cmd.String(), 20)) + log.Info().Msgf("Command completed successfully: %q", cmd.String()) + } + close(statusChan) + }() + return nil } cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr _, _ = cmd.Stdout.Write([]byte(opts.banner)) @@ -475,6 +489,10 @@ func pipe(_ context.Context, opts shellOpts, w, e io.Writer, cmds ...*exec.Cmd) log.Debug().Msgf("Running Start") err := cmd.Run() log.Debug().Msgf("Running Done: %s", err) + if err == nil { + statusChan <- fmt.Sprintf("Command completed successfully: %q", cmd.String()) + } + close(statusChan) return err } diff --git a/internal/view/img_scan.go b/internal/view/img_scan.go index edc6501129..f4f5290b8f 100644 --- a/internal/view/img_scan.go +++ b/internal/view/img_scan.go @@ -4,6 +4,7 @@ package view import ( + "errors" "runtime" "strings" @@ -68,7 +69,7 @@ func (s *ImageScan) viewCVE(app *App, _ ui.Tabular, _ client.GVR, path string) { } site += cve - ok, errChan := run(app, shellOpts{ + ok, errChan, _ := run(app, shellOpts{ background: true, binary: bin, args: []string{site}, @@ -77,9 +78,11 @@ func (s *ImageScan) viewCVE(app *App, _ ui.Tabular, _ client.GVR, path string) { app.Flash().Errf("unable to run browser command") return } + var errs error for e := range errChan { - if e != nil { - app.Flash().Err(e) - } + errs = errors.Join(e) + } + if errs != nil { + app.Flash().Err(errs) } } diff --git a/internal/view/sanitizer.go b/internal/view/sanitizer.go index bae27e7fa0..51388cebec 100644 --- a/internal/view/sanitizer.go +++ b/internal/view/sanitizer.go @@ -243,11 +243,11 @@ func (s *Sanitizer) filter(root *xray.TreeNode) *xray.TreeNode { } s.UpdateTitle() - if ui.IsFuzzySelector(q) { - return root.Filter(q, fuzzyFilter) + if f, ok := dao.HasFuzzySelector(q); ok { + return root.Filter(f, fuzzyFilter) } - if ui.IsInverseSelector(q) { + if dao.IsInverseSelector(q) { return root.Filter(q, rxInverseFilter) } diff --git a/internal/view/table.go b/internal/view/table.go index 7e24dcf816..8a38693382 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -5,12 +5,14 @@ package view import ( "context" + "path/filepath" "strings" "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" "github.com/rs/zerolog/log" @@ -173,7 +175,7 @@ func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { if path, err := saveTable(t.app.Config.K9s.ActiveScreenDumpsDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil { t.app.Flash().Err(err) } else { - t.app.Flash().Infof("File %s saved successfully!", path) + t.app.Flash().Infof("File saved successfully: %q", render.Truncate(filepath.Base(path), 50)) } return nil diff --git a/internal/view/xray.go b/internal/view/xray.go index 26c9caff77..564ed188f5 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -478,11 +478,11 @@ func (x *Xray) filter(root *xray.TreeNode) *xray.TreeNode { } x.UpdateTitle() - if ui.IsFuzzySelector(q) { - return root.Filter(q, fuzzyFilter) + if f, ok := dao.HasFuzzySelector(q); ok { + return root.Filter(f, fuzzyFilter) } - if ui.IsInverseSelector(q) { + if dao.IsInverseSelector(q) { return root.Filter(q, rxInverseFilter) } diff --git a/internal/vul/scanner.go b/internal/vul/scanner.go index 64cbfc5794..c22567036b 100644 --- a/internal/vul/scanner.go +++ b/internal/vul/scanner.go @@ -67,6 +67,7 @@ func (s *imageScanner) GetScan(img string) (*Scan, bool) { func (s *imageScanner) setScan(img string, sc *Scan) { s.mx.Lock() defer s.mx.Unlock() + s.scans[img] = sc } diff --git a/skins/axual.yaml b/skins/axual.yaml index bc7b8701df..816eccc2cf 100644 --- a/skins/axual.yaml +++ b/skins/axual.yaml @@ -111,7 +111,7 @@ k9s: indicator: fgColor: *red bgColor: *blue - toggleOnColor: *green + toggleOnColor: *yellow toggleOffColor: *grey # Chart drawing diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 9c915a8b4c..7515aa753a 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: k9s base: core20 -version: 'v0.30.6' +version: 'v0.30.7' 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.