K9s/release v0.30.7 (#2416)
* [Bug] Fix #2413 * [Bug] Fix #2414 * [Bug] Fix #2407 * cleaning up * Add config files watcher * rel notesmine
parent
d93041b187
commit
982bf6a728
2
Makefile
2
Makefile
|
|
@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
|
|||
else
|
||||
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
|
||||
endif
|
||||
VERSION ?= v0.30.6
|
||||
VERSION ?= v0.30.7
|
||||
IMG_NAME := derailed/k9s
|
||||
IMAGE := ${IMG_NAME}:${VERSION}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -224,18 +224,33 @@ func TestNa(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestTruncate(t *testing.T) {
|
||||
uu := []struct {
|
||||
s string
|
||||
l int
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
if bc, err := config.EnsureBenchmarksCfgFile(cluster, context); err != nil {
|
||||
log.Warn().Err(err).Msgf("No benchmark config file found for context: %s", context)
|
||||
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 {
|
||||
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 {
|
||||
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 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("")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 == "" {
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ const (
|
|||
var (
|
||||
// LabelRx identifies a label query.
|
||||
LabelRx = regexp.MustCompile(`\A\-l`)
|
||||
inverseRx = regexp.MustCompile(`\A\!`)
|
||||
fuzzyRx = regexp.MustCompile(`\A\-f`)
|
||||
)
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, " "))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 a == fuzzyFlag {
|
||||
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:
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
go func() {
|
||||
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.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ k9s:
|
|||
indicator:
|
||||
fgColor: *red
|
||||
bgColor: *blue
|
||||
toggleOnColor: *green
|
||||
toggleOnColor: *yellow
|
||||
toggleOffColor: *grey
|
||||
|
||||
# Chart drawing
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue