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
|
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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// 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.
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 == "" {
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
"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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 == "" {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, " "))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue