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.