K9s/release v0.30.7 (#2416)

* [Bug] Fix #2413

* [Bug] Fix #2414

* [Bug] Fix #2407

* cleaning up

* Add config files watcher

* rel notes
mine
Fernand Galiana 2024-01-02 23:57:07 -07:00 committed by GitHub
parent d93041b187
commit 982bf6a728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 474 additions and 232 deletions

View File

@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
else else
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
endif endif
VERSION ?= v0.30.6 VERSION ?= v0.30.7
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,51 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# 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
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -352,11 +352,7 @@ func (a *APIClient) Config() *Config {
// HasMetrics checks if the cluster supports metrics. // HasMetrics checks if the cluster supports metrics.
func (a *APIClient) HasMetrics() bool { func (a *APIClient) HasMetrics() bool {
err := a.supportsMetricsResources() return a.supportsMetricsResources() != nil
if err != nil {
log.Debug().Msgf("Metrics server detect failed: %s", err)
}
return err == nil
} }
// DialLogs returns a handle to api server for logs. // DialLogs returns a handle to api server for logs.

View File

@ -84,6 +84,10 @@ func (n *Namespace) addFavNS(ns string) {
} }
func (n *Namespace) rmFavNS(ns string) { func (n *Namespace) rmFavNS(ns string) {
if n.LockFavorites {
return
}
victim := -1 victim := -1
for i, f := range n.Favorites { for i, f := range n.Favorites {
if f == ns { if f == ns {

View File

@ -177,7 +177,7 @@ func (k *K9s) ActiveContext() (*data.Context, error) {
// ActivateContext initializes the active context is not present. // ActivateContext initializes the active context is not present.
func (k *K9s) ActivateContext(n string) (*data.Context, error) { func (k *K9s) ActivateContext(n string) (*data.Context, error) {
k.activeContextName = n k.activeContextName = n
ct, err := k.ks.GetContext(k.activeContextName) ct, err := k.ks.GetContext(n)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -197,6 +197,21 @@ func (k *K9s) ActivateContext(n string) (*data.Context, error) {
return k.activeConfig.Context, nil 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. // OverrideRefreshRate set the refresh rate manually.
func (k *K9s) OverrideRefreshRate(r int) { func (k *K9s) OverrideRefreshRate(r int) {
k.manualRefreshRate = r k.manualRefreshRate = r

View File

@ -104,11 +104,17 @@ func (m mockKubeSettings) ContextNames() (map[string]struct{}, error) {
return mm, nil return mm, nil
} }
type mockConnection struct{} type mockConnection struct {
ct string
}
func NewMockConnection() mockConnection { func NewMockConnection() mockConnection {
return mockConnection{} return mockConnection{}
} }
func NewMockConnectionWithContext(ct string) mockConnection {
return mockConnection{ct: ct}
}
func (m mockConnection) CanI(ns, gvr string, verbs []string) (bool, error) { func (m mockConnection) CanI(ns, gvr string, verbs []string) (bool, error) {
return true, nil return true, nil
} }
@ -155,7 +161,7 @@ func (m mockConnection) CheckConnectivity() bool {
return false return false
} }
func (m mockConnection) ActiveContext() string { func (m mockConnection) ActiveContext() string {
return "" return m.ct
} }
func (m mockConnection) ActiveNamespace() string { func (m mockConnection) ActiveNamespace() string {
return "" return ""

View File

@ -9,8 +9,6 @@ import (
"math" "math"
"regexp" "regexp"
"github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -21,7 +19,7 @@ const defaultServiceAccount = "default"
var ( var (
inverseRx = regexp.MustCompile(`\A\!`) inverseRx = regexp.MustCompile(`\A\!`)
fuzzyRx = regexp.MustCompile(`\A\-f`) fuzzyRx = regexp.MustCompile(`\A-f\s?([\w-]+)\b`)
) )
func inList(ll []string, s string) bool { func inList(ll []string, s string) bool {
@ -41,12 +39,14 @@ func IsInverseSelector(s string) bool {
return inverseRx.MatchString(s) return inverseRx.MatchString(s)
} }
// IsFuzzySelector checks if filter is fuzzy or not. // HasFuzzySelector checks if query is fuzzy.
func IsFuzzySelector(s string) bool { func HasFuzzySelector(s string) (string, bool) {
if s == "" { mm := fuzzyRx.FindStringSubmatch(s)
return false if len(mm) != 2 {
return "", false
} }
return fuzzyRx.MatchString(s)
return mm[1], true
} }
func toPerc(v1, v2 float64) float64 { func toPerc(v1, v2 float64) float64 {
@ -56,11 +56,6 @@ func toPerc(v1, v2 float64) float64 {
return math.Round((v1 / v2) * 100) 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. // ToYAML converts a resource to its YAML representation.
func ToYAML(o runtime.Object, showManaged bool) (string, error) { func ToYAML(o runtime.Object, showManaged bool) (string, error) {
if o == nil { if o == nil {

View File

@ -174,8 +174,8 @@ func (l *LogItems) Filter(index int, q string, showTime bool) ([]int, [][]int, e
if q == "" { if q == "" {
return nil, nil, nil return nil, nil, nil
} }
if IsFuzzySelector(q) { if f, ok := HasFuzzySelector(q); ok {
mm, ii := l.fuzzyFilter(index, strings.TrimSpace(q[2:]), showTime) mm, ii := l.fuzzyFilter(index, f, showTime)
return mm, ii, nil return mm, ii, nil
} }
matches, indices, err := l.filterLogs(index, q, showTime) matches, indices, err := l.filterLogs(index, q, showTime)

View File

@ -65,8 +65,8 @@ func (d *Describe) filter(q string, lines []string) fuzzy.Matches {
if q == "" { if q == "" {
return nil return nil
} }
if dao.IsFuzzySelector(q) { if f, ok := dao.HasFuzzySelector(q); ok {
return d.fuzzyFilter(strings.TrimSpace(q[2:]), lines) return d.fuzzyFilter(strings.TrimSpace(f), lines)
} }
return rxFilter(q, lines) return rxFilter(q, lines)
} }

View File

@ -9,8 +9,6 @@ import (
"time" "time"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/derailed/tview"
"github.com/mattn/go-runewidth"
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@ -28,11 +26,6 @@ func FQN(ns, n string) string {
return ns + "/" + n 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. // NewExpBackOff returns a new exponential backoff timer.
func NewExpBackOff(ctx context.Context, start, max time.Duration) backoff.BackOffContext { func NewExpBackOff(ctx context.Context, start, max time.Duration) backoff.BackOffContext {
bf := backoff.NewExponentialBackOff() bf := backoff.NewExponentialBackOff()

View File

@ -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))
})
}
}

View File

@ -84,8 +84,8 @@ func (v *RevValues) filter(q string, lines []string) fuzzy.Matches {
if q == "" { if q == "" {
return nil return nil
} }
if dao.IsFuzzySelector(q) { if f, ok := dao.HasFuzzySelector(q); ok {
return v.fuzzyFilter(strings.TrimSpace(q[2:]), lines) return v.fuzzyFilter(strings.TrimSpace(f), lines)
} }
return rxFilter(q, lines) return rxFilter(q, lines)
} }

View File

@ -111,8 +111,8 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches {
if q == "" { if q == "" {
return nil return nil
} }
if dao.IsFuzzySelector(q) { if f, ok := dao.HasFuzzySelector(q); ok {
return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines) return t.fuzzyFilter(strings.TrimSpace(f), lines)
} }
return rxFilter(q, lines) return rxFilter(q, lines)
} }

View File

@ -113,8 +113,8 @@ func (v *Values) filter(q string, lines []string) fuzzy.Matches {
if q == "" { if q == "" {
return nil return nil
} }
if dao.IsFuzzySelector(q) { if f, ok := dao.HasFuzzySelector(q); ok {
return v.fuzzyFilter(strings.TrimSpace(q[2:]), lines) return v.fuzzyFilter(strings.TrimSpace(f), lines)
} }
return rxFilter(q, lines) return rxFilter(q, lines)
} }

View File

@ -74,8 +74,8 @@ func (y *YAML) filter(q string, lines []string) fuzzy.Matches {
if q == "" { if q == "" {
return nil return nil
} }
if dao.IsFuzzySelector(q) { if f, ok := dao.HasFuzzySelector(q); ok {
return y.fuzzyFilter(strings.TrimSpace(q[2:]), lines) return y.fuzzyFilter(strings.TrimSpace(f), lines)
} }
return rxFilter(q, lines) return rxFilter(q, lines)
} }

View File

@ -13,7 +13,7 @@ import (
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/vul" "github.com/derailed/k9s/internal/vul"
"github.com/derailed/tview" "github.com/derailed/tview"
runewidth "github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/text/language" "golang.org/x/text/language"
"golang.org/x/text/message" "golang.org/x/text/message"

View File

@ -224,18 +224,33 @@ func TestNa(t *testing.T) {
} }
func TestTruncate(t *testing.T) { func TestTruncate(t *testing.T) {
uu := []struct { uu := map[string]struct {
s string data string
l int size int
e string e string
}{ }{
{"fred", 3, "fr…"}, "same": {
{"fred", 2, "f…"}, data: "fred",
{"fred", 10, "fred"}, size: 4,
e: "fred",
},
"small": {
data: "fred",
size: 10,
e: "fred",
},
"larger": {
data: "fred",
size: 3,
e: "fr…",
},
} }
for _, u := range uu { for k := range uu {
assert.Equal(t, u.e, Truncate(u.s, u.l)) u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, Truncate(u.data, u.size))
})
} }
} }

View File

@ -39,7 +39,7 @@ func NewApp(cfg *config.Config, context string) *App {
flash: model.NewFlash(model.DefaultFlashDelay), flash: model.NewFlash(model.DefaultFlashDelay),
cmdBuff: model.NewFishBuff(':', model.CommandBuffer), cmdBuff: model.NewFishBuff(':', model.CommandBuffer),
} }
a.ReloadStyles(context) a.ReloadStyles()
a.views = map[string]tview.Primitive{ a.views = map[string]tview.Primitive{
"menu": NewMenu(a.Styles), "menu": NewMenu(a.Styles),
@ -135,8 +135,8 @@ func (a *App) StylesChanged(s *config.Styles) {
} }
// ReloadStyles reloads skin file. // ReloadStyles reloads skin file.
func (a *App) ReloadStyles(context string) { func (a *App) ReloadStyles() {
a.RefreshStyles(context) a.RefreshStyles()
} }
// Conn returns an api server connection. // Conn returns an api server connection.

View File

@ -7,6 +7,7 @@ import (
"context" "context"
"errors" "errors"
"os" "os"
"path/filepath"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
@ -82,8 +83,8 @@ func (c *Configurator) RefreshCustomViews() error {
return c.CustomView.Load(config.AppViewsFile) return c.CustomView.Load(config.AppViewsFile)
} }
// StylesWatcher watches for skin file changes. // SkinsDirWatcher watches for skin directory file changes.
func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error { func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) error {
if !c.HasSkin() { if !c.HasSkin() {
return nil 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 { if evt.Name == c.skinFile && evt.Op != fsnotify.Chmod {
log.Debug().Msgf("Skin changed: %s", c.skinFile) log.Debug().Msgf("Skin changed: %s", c.skinFile)
s.QueueUpdateDraw(func() { s.QueueUpdateDraw(func() {
c.RefreshStyles(c.Config.K9s.ActiveContextName()) c.RefreshStyles()
}) })
} }
case err := <-w.Errors: case err := <-w.Errors:
@ -120,48 +121,127 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error
return w.Add(config.AppSkinsDir) return w.Add(config.AppSkinsDir)
} }
// RefreshStyles load for skin configuration changes. // ConfigWatcher watches for skin settings changes.
func (c *Configurator) RefreshStyles(context string) { func (c *Configurator) ConfigWatcher(ctx context.Context, s synchronizer) error {
cluster := "na" w, err := fsnotify.NewWatcher()
if c.Config != nil { if err != nil {
if ct, err := c.Config.K9s.ActiveContext(); err == nil { return err
cluster = ct.ClusterName
}
} }
if bc, err := config.EnsureBenchmarksCfgFile(cluster, context); err != nil { go func() {
log.Warn().Err(err).Msgf("No benchmark config file found for context: %s", context) 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 { } else {
c.BenchFile = bc 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
} }
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 { if c.Styles == nil {
c.Styles = config.NewStyles() c.Styles = config.NewStyles()
} else { } else {
c.Styles.Reset() c.Styles.Reset()
} }
var skin string cl, ct, ok := c.activeConfig()
if c.Config != nil { if !ok {
skin = c.Config.K9s.UI.Skin return
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
} }
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
} }
if skin == "" {
skin, ok := c.activeSkin()
if !ok {
log.Debug().Msgf("No custom skin found. Loading default")
c.updateStyles("") c.updateStyles("")
return return
} }
var skinFile = config.SkinFileFromName(skin) skinFile := config.SkinFileFromName(skin)
if err := c.Styles.Load(skinFile); err != nil { if err := c.Styles.Load(skinFile); err != nil {
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
log.Warn().Msgf("Skin file %q not found in skins dir: %s", skinFile, config.AppSkinsDir) log.Warn().Msgf("Skin file %q not found in skins dir: %s", skinFile, config.AppSkinsDir)
} else { } else {
log.Error().Msgf("Failed to parse skin file -- %s: %s.", skinFile, err) log.Error().Msgf("Failed to parse skin file -- %s: %s.", skinFile, err)
} }
c.updateStyles("")
} else { } else {
log.Debug().Msgf("Loading skin file: %q", skinFile)
c.updateStyles(skinFile) c.updateStyles(skinFile)
} }
} }

View File

@ -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)
}
})
}
}

View File

@ -18,18 +18,9 @@ import (
"k8s.io/cli-runtime/pkg/genericclioptions" "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) { func TestSkinnedContext(t *testing.T) {
os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config") os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")
assert.NoError(t, config.InitLocs()) assert.NoError(t, config.InitLocs())
defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir)) defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir))
@ -50,10 +41,22 @@ func TestSkinnedContext(t *testing.T) {
cfg.Config.K9s = config.NewK9s( cfg.Config.K9s = config.NewK9s(
mock.NewMockConnection(), mock.NewMockConnection(),
mock.NewMockKubeSettings(&flags)) 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.Config.K9s.UI = config.UI{Skin: "black_and_wtf"}
cfg.RefreshStyles("ct-1") cfg.RefreshStyles()
assert.True(t, cfg.HasSkin()) assert.True(t, cfg.HasSkin())
assert.Equal(t, tcell.ColorGhostWhite.TrueColor(), render.StdColor) assert.Equal(t, tcell.ColorGhostWhite.TrueColor(), render.StdColor)
assert.Equal(t, tcell.ColorWhiteSmoke.TrueColor(), render.ErrColor) 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)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/vul" "github.com/derailed/k9s/internal/vul"
@ -407,14 +408,13 @@ func (t *Table) filtered(data *render.TableData) *render.TableData {
} }
q := t.cmdBuff.GetText() q := t.cmdBuff.GetText()
if IsFuzzySelector(q) { if f, ok := dao.HasFuzzySelector(q); ok {
return fuzzyFilter(q[2:], filtered) return fuzzyFilter(f, filtered)
} }
filtered, err := rxFilter(q, IsInverseSelector(q), filtered) filtered, err := rxFilter(q, dao.IsInverseSelector(q), filtered)
if err != nil { if err != nil {
log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp") log.Error().Err(errors.New("invalid filter expression")).Msg("Regexp")
// t.cmdBuff.ClearText(true)
} }
return filtered return filtered
@ -471,9 +471,9 @@ func (t *Table) styleTitle() string {
buff := t.cmdBuff.GetText() buff := t.cmdBuff.GetText()
if IsLabelSelector(buff) { if IsLabelSelector(buff) {
buff = truncate(TrimLabelSelector(buff), maxTruncate) buff = render.Truncate(TrimLabelSelector(buff), maxTruncate)
} else if l := t.GetModel().GetLabelFilter(); l != "" { } else if l := t.GetModel().GetLabelFilter(); l != "" {
buff = truncate(l, maxTruncate) buff = render.Truncate(l, maxTruncate)
} }
if buff == "" { if buff == "" {

View File

@ -43,8 +43,6 @@ const (
var ( var (
// LabelRx identifies a label query. // LabelRx identifies a label query.
LabelRx = regexp.MustCompile(`\A\-l`) LabelRx = regexp.MustCompile(`\A\-l`)
inverseRx = regexp.MustCompile(`\A\!`)
fuzzyRx = regexp.MustCompile(`\A\-f`)
) )
func mustExtractStyles(ctx context.Context) *config.Styles { 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. // IsLabelSelector checks if query is a label query.
func IsLabelSelector(s string) bool { func IsLabelSelector(s string) bool {
if s == "" {
return false
}
if LabelRx.MatchString(s) { if LabelRx.MatchString(s) {
return true return true
} }
@ -77,22 +72,6 @@ func IsLabelSelector(s string) bool {
return !strings.Contains(s, " ") && cmd.ToLabels(s) != nil 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. // TrimLabelSelector extracts label query.
func TrimLabelSelector(s string) string { func TrimLabelSelector(s string) string {
if strings.Index(s, "-l") == 0 { if strings.Index(s, "-l") == 0 {
@ -102,14 +81,6 @@ func TrimLabelSelector(s string) string {
return s return s
} }
func truncate(s string, max int) string {
if len(s) < max {
return s
}
return s[:max] + "..."
}
// SkinTitle decorates a title. // SkinTitle decorates a title.
func SkinTitle(fmat string, style config.Frame) string { func SkinTitle(fmat string, style config.Frame) string {
bgColor := style.Title.BgColor bgColor := style.Title.BgColor

View File

@ -6,6 +6,7 @@ package ui
import ( import (
"testing" "testing"
"github.com/derailed/k9s/internal/render"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -16,7 +17,7 @@ func TestTruncate(t *testing.T) {
"empty": {}, "empty": {},
"max": { "max": {
s: "/app.kubernetes.io/instance=prom,app.kubernetes.io/name=prometheus,app.kubernetes.io/component=server", 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": { "less": {
s: "app=fred,env=blee", s: "app=fred,env=blee",
@ -27,7 +28,7 @@ func TestTruncate(t *testing.T) {
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { 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))
}) })
} }
} }

View File

@ -142,9 +142,9 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler {
pipes: p.Pipes, pipes: p.Pipes,
args: args, args: args,
} }
suspend, errChan := run(r.App(), opts) suspend, errChan, statusChan := run(r.App(), opts)
if !suspend { if !suspend {
r.App().Flash().Info("Plugin command failed!") r.App().Flash().Infof("Plugin command failed: %q", p.Description)
return return
} }
var errs error var errs error
@ -155,7 +155,12 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler {
r.App().cowCmd(errs.Error()) r.App().cowCmd(errs.Error())
return 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 { if p.Confirm {
msg := fmt.Sprintf("Run?\n%s %s", p.Command, strings.Join(args, " ")) msg := fmt.Sprintf("Run?\n%s %s", p.Command, strings.Join(args, " "))

View File

@ -324,8 +324,12 @@ func (a *App) Resume() {
ctx, a.cancelFn = context.WithCancel(context.Background()) ctx, a.cancelFn = context.WithCancel(context.Background())
go a.clusterUpdater(ctx) go a.clusterUpdater(ctx)
if err := a.StylesWatcher(ctx, a); err != nil { if err := a.ConfigWatcher(ctx, a); err != nil {
log.Warn().Err(err).Msgf("Styles watcher failed") 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 { if err := a.CustomViewsWatcher(ctx, a); err != nil {
log.Warn().Err(err).Msgf("CustomView watcher failed") log.Warn().Err(err).Msgf("CustomView watcher failed")
@ -408,17 +412,9 @@ func (a *App) switchNS(ns string) error {
if a.Config.ActiveNamespace() == ns { if a.Config.ActiveNamespace() == ns {
return nil return nil
} }
if ns == client.ClusterScope { if ns == client.ClusterScope {
ns = client.BlankNamespace 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 { if err := a.Config.SetActiveNamespace(ns); err != nil {
return err return err
} }
@ -469,6 +465,7 @@ func (a *App) switchContext(ci *cmd.Interpreter) error {
} }
ns := a.Config.ActiveNamespace() ns := a.Config.ActiveNamespace()
if !a.Conn().IsValidNamespace(ns) { if !a.Conn().IsValidNamespace(ns) {
a.Flash().Errf("Unable to validate namespace %q. Using %q namespace", ns, client.DefaultNamespace)
ns = client.DefaultNamespace ns = client.DefaultNamespace
if err := a.Config.SetActiveNamespace(ns); err != nil { if err := a.Config.SetActiveNamespace(ns); err != nil {
return err 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()) log.Debug().Msgf("--> Switching Context %q -- %q -- %q", name, ns, a.Config.ActiveView())
a.Flash().Infof("Switching context to %q::%q", name, ns) a.Flash().Infof("Switching context to %q::%q", name, ns)
a.ReloadStyles(name) a.ReloadStyles()
a.gotoResource(a.Config.ActiveView(), "", true) a.gotoResource(a.Config.ActiveView(), "", true)
a.clusterModel.Reset(a.factory) a.clusterModel.Reset(a.factory)
} }

View File

@ -11,6 +11,7 @@ const (
nsKey = "ns" nsKey = "ns"
topicKey = "topic" topicKey = "topic"
filterKey = "filter" filterKey = "filter"
fuzzyKey = "fuzzy"
labelKey = "labels" labelKey = "labels"
contextKey = "context" contextKey = "context"
) )
@ -30,8 +31,12 @@ func newArgs(p *Interpreter, aa []string) args {
args[contextKey] = a[1:] args[contextKey] = a[1:]
case strings.Index(a, fuzzyFlag) == 0: case strings.Index(a, fuzzyFlag) == 0:
if a == fuzzyFlag {
if i++; i < len(aa) { if i++; i < len(aa) {
args[filterKey] = strings.ToLower(strings.TrimSpace(aa[i])) args[fuzzyKey] = strings.ToLower(strings.TrimSpace(aa[i]))
}
} else {
args[fuzzyKey] = strings.ToLower(a[2:])
} }
case strings.Index(a, filterFlag) == 0: case strings.Index(a, filterFlag) == 0:
@ -67,7 +72,8 @@ func newArgs(p *Interpreter, aa []string) args {
func (a args) hasFilters() bool { func (a args) hasFilters() bool {
_, fok := a[filterKey] _, fok := a[filterKey]
_, zok := a[fuzzyKey]
_, lok := a[labelKey] _, lok := a[labelKey]
return fok || lok return fok || zok || lok
} }

View File

@ -42,7 +42,12 @@ func TestFlagsNew(t *testing.T) {
"fuzzy-filter": { "fuzzy-filter": {
i: NewInterpreter("po"), i: NewInterpreter("po"),
aa: []string{"-f", "fred"}, 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": { "filter+ns": {
i: NewInterpreter("po"), i: NewInterpreter("po"),
@ -72,23 +77,43 @@ func TestFlagsNew(t *testing.T) {
"full-monty": { "full-monty": {
i: NewInterpreter("po"), i: NewInterpreter("po"),
aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg"}, 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": { "full-monty+ctx": {
i: NewInterpreter("po"), i: NewInterpreter("po"),
aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg", "@ctx1"}, 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": { "caps": {
i: NewInterpreter("po"), i: NewInterpreter("po"),
aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg", "@Dev"}, 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": { "ctx": {
i: NewInterpreter("ctx"), i: NewInterpreter("ctx"),
aa: []string{"Dev"}, aa: []string{"Dev"},
ll: args{contextKey: "Dev"}, ll: args{contextKey: "Dev"},
}, },
"bork": {
i: NewInterpreter("apply -f"),
ll: args{},
},
} }
for k := range uu { for k := range uu {

View File

@ -11,8 +11,10 @@ import (
) )
func ToLabels(s string) map[string]string { func ToLabels(s string) map[string]string {
ll := strings.Split(s, ",") var (
lbls := make(map[string]string, len(ll)) ll = strings.Split(s, ",")
lbls = make(map[string]string, len(ll))
)
for _, l := range ll { for _, l := range ll {
kv := strings.Split(l, "=") kv := strings.Split(l, "=")
if len(kv) < 2 || kv[0] == "" || kv[1] == "" { if len(kv) < 2 || kv[0] == "" || kv[1] == "" {
@ -20,7 +22,6 @@ func ToLabels(s string) map[string]string {
} }
lbls[kv[0]] = kv[1] lbls[kv[0]] = kv[1]
} }
if len(lbls) == 0 { if len(lbls) == 0 {
return nil return nil
} }

View File

@ -7,12 +7,14 @@ import (
"strings" "strings"
) )
// Interpreter tracks user prompt input.
type Interpreter struct { type Interpreter struct {
line string line string
cmd string cmd string
args args args args
} }
// NewInterpreter returns a new instance.
func NewInterpreter(s string) *Interpreter { func NewInterpreter(s string) *Interpreter {
c := Interpreter{ c := Interpreter{
line: s, line: s,
@ -32,20 +34,24 @@ func (c *Interpreter) grok() {
c.args = newArgs(c, ff[1:]) c.args = newArgs(c, ff[1:])
} }
// HasNS returns true if ns is present in prompt.
func (c *Interpreter) HasNS() bool { func (c *Interpreter) HasNS() bool {
ns, ok := c.args[nsKey] ns, ok := c.args[nsKey]
return ok && ns != "" return ok && ns != ""
} }
// Cmd returns the command.
func (c *Interpreter) Cmd() string { func (c *Interpreter) Cmd() string {
return c.cmd return c.cmd
} }
// IsBlank returns true if prompt is empty.
func (c *Interpreter) IsBlank() bool { func (c *Interpreter) IsBlank() bool {
return c.line == "" return c.line == ""
} }
// Amend merges prompts.
func (c *Interpreter) Amend(c1 *Interpreter) { func (c *Interpreter) Amend(c1 *Interpreter) {
c.cmd = c1.cmd c.cmd = c1.cmd
if c.args == nil { 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 { func (c *Interpreter) Reset(s string) *Interpreter {
c.line = s c.line = s
c.grok() c.grok()
@ -65,50 +72,60 @@ func (c *Interpreter) Reset(s string) *Interpreter {
return c return c
} }
// GetLine teturns the prompt.
func (c *Interpreter) GetLine() string { func (c *Interpreter) GetLine() string {
return strings.TrimSpace(c.line) return strings.TrimSpace(c.line)
} }
// IsCowCmd returns true if cow cmd is detected.
func (c *Interpreter) IsCowCmd() bool { func (c *Interpreter) IsCowCmd() bool {
return c.cmd == cowCmd return c.cmd == cowCmd
} }
// IsHelpCmd returns true if help cmd is detected.
func (c *Interpreter) IsHelpCmd() bool { func (c *Interpreter) IsHelpCmd() bool {
_, ok := helpCmd[c.cmd] _, ok := helpCmd[c.cmd]
return ok return ok
} }
// IsBailCmd returns true if quit cmd is detected.
func (c *Interpreter) IsBailCmd() bool { func (c *Interpreter) IsBailCmd() bool {
_, ok := bailCmd[c.cmd] _, ok := bailCmd[c.cmd]
return ok return ok
} }
// IsAliasCmd returns true if alias cmd is detected.
func (c *Interpreter) IsAliasCmd() bool { func (c *Interpreter) IsAliasCmd() bool {
_, ok := aliasCmd[c.cmd] _, ok := aliasCmd[c.cmd]
return ok return ok
} }
// IsXrayCmd returns true if xray cmd is detected.
func (c *Interpreter) IsXrayCmd() bool { func (c *Interpreter) IsXrayCmd() bool {
_, ok := xrayCmd[c.cmd] _, ok := xrayCmd[c.cmd]
return ok return ok
} }
// IsContextCmd returns true if context cmd is detected.
func (c *Interpreter) IsContextCmd() bool { func (c *Interpreter) IsContextCmd() bool {
_, ok := contextCmd[c.cmd] _, ok := contextCmd[c.cmd]
return ok return ok
} }
// IsDirCmd returns true if dir cmd is detected.
func (c *Interpreter) IsDirCmd() bool { func (c *Interpreter) IsDirCmd() bool {
_, ok := dirCmd[c.cmd] _, ok := dirCmd[c.cmd]
return ok return ok
} }
// IsRBACCmd returns true if rbac cmd is detected.
func (c *Interpreter) IsRBACCmd() bool { func (c *Interpreter) IsRBACCmd() bool {
return c.cmd == canCmd return c.cmd == canCmd
} }
// ContextArg returns context cmd arg.
func (c *Interpreter) ContextArg() (string, bool) { func (c *Interpreter) ContextArg() (string, bool) {
if !c.IsContextCmd() { if !c.IsContextCmd() {
return "", false return "", false
@ -117,26 +134,32 @@ func (c *Interpreter) ContextArg() (string, bool) {
return c.args[contextKey], true return c.args[contextKey], true
} }
// ResetContextArg deletes context arg.
func (c *Interpreter) ResetContextArg() { func (c *Interpreter) ResetContextArg() {
delete(c.args, contextFlag) delete(c.args, contextFlag)
} }
// DirArg returns the directory is present.
func (c *Interpreter) DirArg() (string, bool) { func (c *Interpreter) DirArg() (string, bool) {
if !c.IsDirCmd() || c.args[topicKey] == "" { if !c.IsDirCmd() {
return "", false 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) { func (c *Interpreter) CowArg() (string, bool) {
if !c.IsCowCmd() || c.args[nsKey] == "" { if !c.IsCowCmd() {
return "", false 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) { func (c *Interpreter) RBACArgs() (string, string, bool) {
if !c.IsRBACCmd() { if !c.IsRBACCmd() {
return "", "", false return "", "", false
@ -149,6 +172,7 @@ func (c *Interpreter) RBACArgs() (string, string, bool) {
return tt[1], tt[2], true return tt[1], tt[2], true
} }
// XRayArgs return the gvr and ns if any.
func (c *Interpreter) XrayArgs() (string, string, bool) { func (c *Interpreter) XrayArgs() (string, string, bool) {
if !c.IsXrayCmd() { if !c.IsXrayCmd() {
return "", "", false 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) { func (c *Interpreter) FilterArg() (string, bool) {
f, ok := c.args[filterKey] 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) { func (c *Interpreter) NSArg() (string, bool) {
ns, ok := c.args[nsKey] 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) { func (c *Interpreter) HasContext() (string, bool) {
ctx, ok := c.args[contextKey] 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) { func (c *Interpreter) LabelsArg() (map[string]string, bool) {
ll, ok := c.args[labelKey] ll, ok := c.args[labelKey]
if !ok {
return nil, false
}
return ToLabels(ll), true return ToLabels(ll), ok
} }

View File

@ -317,11 +317,6 @@ func TestContextCmd(t *testing.T) {
ctx string ctx string
}{ }{
"empty": {}, "empty": {},
"plain": {
cmd: "context",
ok: true,
ctx: "",
},
"happy-full": { "happy-full": {
cmd: "context ctx1", cmd: "context ctx1",
ok: true, ok: true,

View File

@ -79,13 +79,13 @@ func allowedXRay(gvr client.GVR) bool {
} }
func (c *Command) contextCmd(p *cmd.Interpreter) error { func (c *Command) contextCmd(p *cmd.Interpreter) error {
ctx, ok := p.ContextArg() ct, ok := p.ContextArg()
if !ok { if !ok {
return fmt.Errorf("invalid command use `context xxx`") return fmt.Errorf("invalid command use `context xxx`")
} }
if ctx != "" { if ct != "" {
return useContext(c.app, ctx) return useContext(c.app, ct)
} }
gvr, v, err := c.viewMetaFor(p) gvr, v, err := c.viewMetaFor(p)
@ -93,7 +93,7 @@ func (c *Command) contextCmd(p *cmd.Interpreter) error {
return err 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 { 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 { if f, ok := p.FilterArg(); ok {
co.SetFilter(f) co.SetFilter(f)
} }
if f, ok := p.FuzzyArg(); ok {
co.SetFilter("-f " + f)
}
if ll, ok := p.LabelsArg(); ok { if ll, ok := p.LabelsArg(); ok {
co.SetLabelFilter(ll) co.SetLabelFilter(ll)
} }

View File

@ -16,6 +16,8 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
@ -75,7 +77,7 @@ func runK(a *App, opts shellOpts) error {
} }
opts.binary = bin opts.binary = bin
suspended, errChan := run(a, opts) suspended, errChan, _ := run(a, opts)
if !suspended { if !suspended {
return fmt.Errorf("unable to run command") return fmt.Errorf("unable to run command")
} }
@ -87,28 +89,29 @@ func runK(a *App, opts shellOpts) error {
return errs 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) errChan := make(chan error, 1)
statusChan := make(chan string, 1)
if opts.background { if opts.background {
if err := execute(opts); err != nil { if err := execute(opts, statusChan); err != nil {
errChan <- err errChan <- err
a.Flash().Errf("Exec failed %q: %s", opts, err) a.Flash().Errf("Exec failed %q: %s", opts, err)
} }
close(errChan) close(errChan)
return true, errChan return true, errChan, statusChan
} }
a.Halt() a.Halt()
defer a.Resume() defer a.Resume()
return a.Suspend(func() { return a.Suspend(func() {
if err := execute(opts); err != nil { if err := execute(opts, statusChan); err != nil {
errChan <- err errChan <- err
a.Flash().Errf("Exec failed %q: %s", opts, err) a.Flash().Errf("Exec failed %q: %s", opts, err)
} }
close(errChan) close(errChan)
}), errChan }), errChan, statusChan
} }
func edit(a *App, opts shellOpts) bool { func edit(a *App, opts shellOpts) bool {
@ -122,18 +125,20 @@ func edit(a *App, opts shellOpts) bool {
} }
opts.binary, opts.background = bin, false opts.binary, opts.background = bin, false
suspended, errChan := run(a, opts) suspended, errChan, _ := run(a, opts)
if !suspended { if !suspended {
a.Flash().Errf("edit command failed") a.Flash().Errf("edit command failed")
} }
status := true
for e := range errChan { for e := range errChan {
a.Flash().Err(e) 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 { if opts.clear {
clearScreen() clearScreen()
} }
@ -174,7 +179,7 @@ func execute(opts shellOpts) error {
} }
var o, e bytes.Buffer var o, e bytes.Buffer
err := pipe(ctx, opts, &o, &e, cmds...) err := pipe(ctx, opts, statusChan, &o, &e, cmds...)
if err != nil { if err != nil {
log.Err(err).Msgf("Command failed") log.Err(err).Msgf("Command failed")
return errors.Join(err, fmt.Errorf("%s", e.String())) 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 { if len(cmds) == 0 {
return nil return nil
} }
@ -466,8 +471,17 @@ func pipe(_ context.Context, opts shellOpts, w, e io.Writer, cmds ...*exec.Cmd)
if len(cmds) == 1 { if len(cmds) == 1 {
cmd := cmds[0] cmd := cmds[0]
if opts.background { if opts.background {
go func() {
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, w, e cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, w, e
return cmd.Run() 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.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
_, _ = cmd.Stdout.Write([]byte(opts.banner)) _, _ = 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") log.Debug().Msgf("Running Start")
err := cmd.Run() err := cmd.Run()
log.Debug().Msgf("Running Done: %s", err) log.Debug().Msgf("Running Done: %s", err)
if err == nil {
statusChan <- fmt.Sprintf("Command completed successfully: %q", cmd.String())
}
close(statusChan)
return err return err
} }

View File

@ -4,6 +4,7 @@
package view package view
import ( import (
"errors"
"runtime" "runtime"
"strings" "strings"
@ -68,7 +69,7 @@ func (s *ImageScan) viewCVE(app *App, _ ui.Tabular, _ client.GVR, path string) {
} }
site += cve site += cve
ok, errChan := run(app, shellOpts{ ok, errChan, _ := run(app, shellOpts{
background: true, background: true,
binary: bin, binary: bin,
args: []string{site}, 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") app.Flash().Errf("unable to run browser command")
return return
} }
var errs error
for e := range errChan { for e := range errChan {
if e != nil { errs = errors.Join(e)
app.Flash().Err(e)
} }
if errs != nil {
app.Flash().Err(errs)
} }
} }

View File

@ -243,11 +243,11 @@ func (s *Sanitizer) filter(root *xray.TreeNode) *xray.TreeNode {
} }
s.UpdateTitle() s.UpdateTitle()
if ui.IsFuzzySelector(q) { if f, ok := dao.HasFuzzySelector(q); ok {
return root.Filter(q, fuzzyFilter) return root.Filter(f, fuzzyFilter)
} }
if ui.IsInverseSelector(q) { if dao.IsInverseSelector(q) {
return root.Filter(q, rxInverseFilter) return root.Filter(q, rxInverseFilter)
} }

View File

@ -5,12 +5,14 @@ package view
import ( import (
"context" "context"
"path/filepath"
"strings" "strings"
"time" "time"
"github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
"github.com/derailed/tcell/v2" "github.com/derailed/tcell/v2"
"github.com/rs/zerolog/log" "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 { if path, err := saveTable(t.app.Config.K9s.ActiveScreenDumpsDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil {
t.app.Flash().Err(err) t.app.Flash().Err(err)
} else { } 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 return nil

View File

@ -478,11 +478,11 @@ func (x *Xray) filter(root *xray.TreeNode) *xray.TreeNode {
} }
x.UpdateTitle() x.UpdateTitle()
if ui.IsFuzzySelector(q) { if f, ok := dao.HasFuzzySelector(q); ok {
return root.Filter(q, fuzzyFilter) return root.Filter(f, fuzzyFilter)
} }
if ui.IsInverseSelector(q) { if dao.IsInverseSelector(q) {
return root.Filter(q, rxInverseFilter) return root.Filter(q, rxInverseFilter)
} }

View File

@ -67,6 +67,7 @@ func (s *imageScanner) GetScan(img string) (*Scan, bool) {
func (s *imageScanner) setScan(img string, sc *Scan) { func (s *imageScanner) setScan(img string, sc *Scan) {
s.mx.Lock() s.mx.Lock()
defer s.mx.Unlock() defer s.mx.Unlock()
s.scans[img] = sc s.scans[img] = sc
} }

View File

@ -111,7 +111,7 @@ k9s:
indicator: indicator:
fgColor: *red fgColor: *red
bgColor: *blue bgColor: *blue
toggleOnColor: *green toggleOnColor: *yellow
toggleOffColor: *grey toggleOffColor: *grey
# Chart drawing # Chart drawing

View File

@ -1,6 +1,6 @@
name: k9s name: k9s
base: core20 base: core20
version: 'v0.30.6' version: 'v0.30.7'
summary: K9s is a CLI to view and manage your Kubernetes clusters. summary: K9s is a CLI to view and manage your Kubernetes clusters.
description: | 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. 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.