diff --git a/change_logs/release_v0.20.2.md b/change_logs/release_v0.20.2.md new file mode 100644 index 00000000..6d231159 --- /dev/null +++ b/change_logs/release_v0.20.2.md @@ -0,0 +1,36 @@ + + +# Release v0.20.2 + +## 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 is as ever very much noticed and appreciated! + +Also if you dig this tool, consider joining our [sponsorhip 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! Fixing a few issue in the v0.20 aftermath ;( + +## Selection Marker + +In this drop, we're adding the ability to set row marks ranges. There are situations where you've filtered a resource and need to delete part or all of the rows. In previous releases, you had to mark each rows one by one. Now you have the ability to select a beginning and an end range and all rows in between will now be marked! To mark a single row, you can use `space`. To select rows between your initial mark to the current selection use `Ctrl-space`. To nuke all marked rows use `Ctrl-\`. All credits and ATTA BOY goes to [Ryan Richard](https://github.com/cfryanr) for suggesting this feature!! + +## Logs Got Some TLC! + +Per [Raman Gupta](https://github.com/rocketraman) excellent suggestion, we've added a way to add a separator to your chatty logs to easily see the latest incoming logs. While in log view you can now press `m` to add the separator to the log stream. If you don't care about the log history and just want to see the latest incoming logs, pressing `c` will clear out the log viewer. + +## Resolved Bugs/Features/PRs + +- [Issue #741](https://github.com/derailed/k9s/issues/741) +- [Issue #740](https://github.com/derailed/k9s/issues/740) +- [Issue #739](https://github.com/derailed/k9s/issues/739) +- [Issue #727](https://github.com/derailed/k9s/issues/727) +- [Issue #723](https://github.com/derailed/k9s/issues/723) +- [PR #725](https://github.com/derailed/k9s/pull/725) Big Thanks To [Soupyt](https://github.com/soupyt)!! + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/internal/client/client.go b/internal/client/client.go index 02ee98b1..ad15320e 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -135,7 +135,9 @@ func (a *APIClient) clearCache() { // CanI checks if user has access to a certain resource. func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) { - log.Debug().Msgf("Check Access %q::%q", ns, gvr) + a.mx.Lock() + defer a.mx.Unlock() + if !a.connOK { return false, errors.New("ACCESS -- No API server connection") } @@ -149,7 +151,6 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) } } - log.Debug().Msgf("----> Calling API") dial, err := a.Dial() if err != nil { return false, err @@ -171,9 +172,9 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) return auth, fmt.Errorf("`%s access denied for user on %q:%s", v, ns, gvr) } } - auth = true a.cache.Add(key, true, cacheExpiry) + return } @@ -214,20 +215,22 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { } // CheckConnectivity return true if api server is cool or false otherwise. -func (a *APIClient) CheckConnectivity() (status bool) { +func (a *APIClient) CheckConnectivity() (ok bool) { + a.mx.Lock() + defer a.mx.Unlock() + defer func() { if err := recover(); err != nil { - status = false + ok = false } - if !status { + if !ok { a.clearCache() } - a.connOK = status + a.connOK = ok }() // Need to reload to pickup any kubeconfig changes. - config := NewConfig(a.config.flags) - cfg, err := config.RESTConfig() + cfg, err := NewConfig(a.config.flags).RESTConfig() if err != nil { return false } @@ -237,7 +240,6 @@ func (a *APIClient) CheckConnectivity() (status bool) { log.Error().Err(err).Msgf("Unable to connect to api server") return } - log.Debug().Msgf("CONN-CHECK on %#v", cfg.Host) // Check connection if _, err := client.ServerVersion(); err == nil { @@ -245,7 +247,7 @@ func (a *APIClient) CheckConnectivity() (status bool) { log.Debug().Msgf("RESETING CON!!") a.reset() } - status = true + ok = true } else { log.Error().Err(err).Msgf("K9s can't connect to cluster") } diff --git a/internal/model/log.go b/internal/model/log.go index 3a2b6f76..28264bf6 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -38,6 +38,7 @@ type Log struct { filter string lastSent int flushTimeout time.Duration + filtering bool } // NewLog returns a new model. @@ -52,6 +53,9 @@ func NewLog(gvr client.GVR, opts dao.LogOptions, flushTimeout time.Duration) *Lo // LogOptions returns the current log options. func (l *Log) LogOptions() dao.LogOptions { + l.mx.RLock() + defer l.mx.RUnlock() + return l.logOptions } @@ -59,12 +63,18 @@ func (l *Log) LogOptions() dao.LogOptions { func (l *Log) SinceSeconds() int64 { l.mx.RLock() defer l.mx.RUnlock() + return l.logOptions.SinceSeconds } // SetLogOptions updates logger options. func (l *Log) SetLogOptions(opts dao.LogOptions) { - l.logOptions = opts + l.mx.Lock() + { + l.logOptions = opts + } + l.mx.Unlock() + l.Restart() } @@ -96,6 +106,7 @@ func (l *Log) Clear() { l.lines, l.lastSent = dao.LogItems{}, 0 } l.mx.Unlock() + l.fireLogCleared() } @@ -148,16 +159,25 @@ func (l *Log) ClearFilter() { l.fireLogChanged(l.lines) } -// Filter filters the model using either fuzzy or regexp. -func (l *Log) Filter(q string) error { - l.mx.RLock() - defer l.mx.RUnlock() +// Filter filters th:e model using either fuzzy or regexp. +func (l *Log) Filter(q string) { + log.Debug().Msgf("Filter %q", q) + l.mx.Lock() + defer l.mx.Unlock() l.filter = q - l.fireLogCleared() - l.fireLogBuffChanged(l.lines) - - return nil + if l.filtering { + return + } + l.filtering = true + go func(l *Log) { + <-time.After(500 * time.Millisecond) + l.mx.Lock() + defer l.mx.Unlock() + l.fireLogCleared() + l.fireLogBuffChanged(l.lines) + l.filtering = false + }(l) } func (l *Log) load() error { diff --git a/internal/model/log_test.go b/internal/model/log_test.go index 04a30239..15664a6a 100644 --- a/internal/model/log_test.go +++ b/internal/model/log_test.go @@ -75,13 +75,13 @@ func TestLogFilter(t *testing.T) { m.Notify(true) assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, 2, v.clearCalled) + assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, u.e, len(v.data)) m.ClearFilter() assert.Equal(t, 2, v.dataCalled) - assert.Equal(t, 3, v.clearCalled) + assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, size, len(v.data)) }) @@ -193,7 +193,7 @@ func TestLogTimedout(t *testing.T) { } m.Notify(true) assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, 2, v.clearCalled) + assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) const e = "\x1b[38;5;209ml\x1b[0m\x1b[38;5;209mi\x1b[0m\x1b[38;5;209mn\x1b[0m\x1b[38;5;209me\x1b[0m\x1b[38;5;209m1\x1b[0m" assert.Equal(t, e, string(v.data[0].Bytes)) diff --git a/internal/view/command.go b/internal/view/command.go index fb100aca..d6fdb94a 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -142,7 +142,6 @@ func (c *Command) run(cmd, path string, clearStack bool) error { func (c *Command) defaultCmd() error { if !c.app.Conn().ConnectionOK() { - log.Debug().Msgf("YO!!") return c.run("ctx", "", true) } view := c.app.Config.ActiveView() diff --git a/internal/view/log.go b/internal/view/log.go index 771bc815..a06e040c 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -26,7 +26,7 @@ const ( logMessage = "Waiting for logs..." logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] " logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] " - flushTimeout = 100 * time.Millisecond + flushTimeout = 50 * time.Millisecond ) // Log represents a generic log viewer. @@ -123,9 +123,7 @@ func (l *Log) LogChanged(lines dao.LogItems) { // BufferChanged indicates the buffer was changed. func (l *Log) BufferChanged(s string) { - if err := l.model.Filter(l.logs.cmdBuff.GetText()); err != nil { - l.app.Flash().Err(err) - } + l.model.Filter(l.logs.cmdBuff.GetText()) l.updateTitle() } @@ -268,10 +266,9 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { if !l.logs.cmdBuff.IsActive() { return evt } + l.logs.cmdBuff.SetActive(false) - if err := l.model.Filter(l.logs.cmdBuff.GetText()); err != nil { - l.app.Flash().Err(err) - } + l.model.Filter(l.logs.cmdBuff.GetText()) l.updateTitle() return nil @@ -326,7 +323,7 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey { } func (l *Log) markCmd(*tcell.EventKey) *tcell.EventKey { - fmt.Fprintln(l.ansiWriter, fmt.Sprintf("[white::b]%s[::]", strings.Repeat("-", 80))) + fmt.Fprintln(l.ansiWriter, fmt.Sprintf("[white::b]%s[::]", strings.Repeat("─", 80))) return nil } diff --git a/internal/view/log_int_test.go b/internal/view/log_int_test.go index f43dfbfa..d6d8542d 100644 --- a/internal/view/log_int_test.go +++ b/internal/view/log_int_test.go @@ -85,9 +85,9 @@ func TestLogFilter(t *testing.T) { l.SendKeys(ui.KeySlash) l.SendStrokes("zorg") - assert.Equal(t, "\x1b[38;5;209mz\x1b[0m\x1b[38;5;209mo\x1b[0m\x1b[38;5;209mr\x1b[0m\x1b[38;5;209mg\x1b[0m", list.lines) - assert.Equal(t, 5, list.change) - assert.Equal(t, 5, list.clear) + assert.Equal(t, "duhzorg", list.lines) + assert.Equal(t, 1, list.change) + assert.Equal(t, 1, list.clear) assert.Equal(t, 0, list.fail) } diff --git a/internal/watch/factory.go b/internal/watch/factory.go index a296700f..58d5f644 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -70,7 +70,6 @@ func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]run if err != nil { return nil, err } - log.Debug().Msgf("LIST %q::%q -- %t::%t", gvr, ns, wait, inf.Informer().HasSynced()) if client.IsAllNamespace(ns) { ns = client.AllNamespaces } @@ -109,7 +108,6 @@ func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime if err != nil { return nil, err } - log.Debug().Msgf("GET %q::%q -- %t::%t", gvr, path, wait, inf.Informer().HasSynced()) var o runtime.Object if client.IsClusterScoped(ns) { o, err = inf.Lister().Get(n)