From 372b4d8e09e042d3c328bc8326f8f4fcccac5419 Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 29 Oct 2020 09:32:59 -0600 Subject: [PATCH] cleanup and bug fixes --- go.mod | 1 + go.sum | 1 + internal/client/client.go | 23 +++++++------- internal/config/config.go | 1 - internal/config/config_test.go | 4 +-- internal/config/k9s.go | 2 +- internal/dao/node.go | 2 +- internal/model/describe.go | 47 ++++++++++------------------- internal/model/helpers.go | 11 +++++++ internal/model/log.go | 3 +- internal/model/table.go | 2 +- internal/model/types.go | 28 +++++++++++++++++ internal/model/yaml.go | 55 ++++++++++++++++------------------ internal/view/app.go | 33 +++++++++++++++----- internal/view/cow.go | 2 +- internal/view/live_view.go | 4 ++- internal/view/log.go | 2 +- 17 files changed, 132 insertions(+), 89 deletions(-) diff --git a/go.mod b/go.mod index e6710611..1cc8c3b7 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/atotto/clipboard v0.1.2 + github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff/v4 v4.1.0 github.com/derailed/popeye v0.8.10 github.com/derailed/tview v0.4.6 diff --git a/go.sum b/go.sum index 17c3dfa6..72b70c82 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,7 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXe github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/cenkalti/backoff v1.1.0 h1:QnvVp8ikKCDWOsFheytRCoYWYPO/ObCTBGxT19Hc+yE= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc= github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/internal/client/client.go b/internal/client/client.go index 7c597d2b..db4322ab 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -264,29 +264,29 @@ func (a *APIClient) HasMetrics() bool { if !ok || err != nil { return false } - v, ok := a.cache.Get(cacheMXKey) - if ok { + if v, ok := a.cache.Get(cacheMXKey); ok { flag, k := v.(bool) return k && flag } - var flag bool + var metricsOK bool + defer func() { + a.cache.Add(cacheMXKey, metricsOK, cacheExpiry) + }() dial, err := a.MXDial() if err != nil { - a.cache.Add(cacheMXKey, flag, cacheExpiry) - return flag + return metricsOK } ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout()) defer cancel() if _, err := dial.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{Limit: 1}); err == nil { - flag = true + metricsOK = true } else { log.Error().Err(err).Msgf("List metrics failed") } - a.cache.Add(cacheMXKey, flag, cacheExpiry) - return flag + return metricsOK } // Dial returns a handle to api server or die. @@ -412,10 +412,6 @@ func (a *APIClient) reset() { } func (a *APIClient) supportsMetricsResources() (supported bool, err error) { - defer func() { - a.cache.Add(cacheMXAPIKey, supported, cacheExpiry) - }() - if v, ok := a.cache.Get(cacheMXAPIKey); ok { flag, k := v.(bool) supported = k && flag @@ -424,6 +420,9 @@ func (a *APIClient) supportsMetricsResources() (supported bool, err error) { if a.config == nil || a.config.flags == nil { return } + defer func() { + a.cache.Add(cacheMXAPIKey, supported, cacheExpiry) + }() dial, err := a.CachedDiscovery() if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index f9f18ee7..59d48ffc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -229,7 +229,6 @@ func (c *Config) Load(path string) error { // Save configuration to disk. func (c *Config) Save() error { - log.Debug().Msg("[Config] Saving configuration...") c.Validate() return c.SaveFile(K9sConfigFile) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bb81ddfe..600f8293 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -261,7 +261,7 @@ func TestSetup(t *testing.T) { var expectedConfig = `k9s: refreshRate: 100 - maxConnRetry: 15 + maxConnRetry: 5 enableMouse: false headless: false crumbsless: false @@ -344,7 +344,7 @@ var expectedConfig = `k9s: var resetConfig = `k9s: refreshRate: 2 - maxConnRetry: 15 + maxConnRetry: 5 enableMouse: false headless: false crumbsless: false diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 578e6f2d..13b32b41 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -4,7 +4,7 @@ import "github.com/derailed/k9s/internal/client" const ( defaultRefreshRate = 2 - defaultMaxConnRetry = 15 + defaultMaxConnRetry = 5 ) // K9s tracks K9s configuration options. diff --git a/internal/dao/node.go b/internal/dao/node.go index fc14ccd1..b0dfcf21 100644 --- a/internal/dao/node.go +++ b/internal/dao/node.go @@ -116,7 +116,7 @@ func (n *Node) Get(ctx context.Context, path string) (runtime.Object, error) { ) if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(ctx); err != nil { - log.Warn().Err(err).Msgf("No node metrics") + log.Debug().Err(err).Msgf("No node metrics") } } diff --git a/internal/model/describe.go b/internal/model/describe.go index 4676ffeb..0e7bcad0 100644 --- a/internal/model/describe.go +++ b/internal/model/describe.go @@ -16,24 +16,6 @@ import ( "github.com/sahilm/fuzzy" ) -type ResourceViewerListener interface { - ResourceChanged(lines []string, matches fuzzy.Matches) - ResourceFailed(error) -} - -type ToggleOpts map[string]bool - -type ResourceViewer interface { - GetPath() string - Filter(string) - ClearFilter() - Peek() []string - SetOptions(context.Context, ToggleOpts) - Watch(context.Context) error - AddListener(ResourceViewerListener) - RemoveListener(ResourceViewerListener) -} - // Describe tracks describeable resources. type Describe struct { gvr client.GVR @@ -50,7 +32,7 @@ func NewDescribe(gvr client.GVR, path string) *Describe { return &Describe{ gvr: gvr, path: path, - refreshRate: 2 * time.Second, + refreshRate: defaultReaderRefreshRate, } } @@ -60,7 +42,7 @@ func (d *Describe) GetPath() string { } // SetOptions toggle model options. -func (d *Describe) SetOptions(context.Context, ToggleOpts) {} +func (d *Describe) SetOptions(context.Context, ViewerToggleOpts) {} // Filter filters the model. func (d *Describe) Filter(q string) { @@ -134,27 +116,30 @@ func (d *Describe) Watch(ctx context.Context) error { func (d *Describe) updater(ctx context.Context) { defer log.Debug().Msgf("Describe canceled -- %q", d.gvr) - bf := backoff.NewExponentialBackOff() - bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval - rate := initRefreshRate + backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval) + delay := defaultReaderRefreshRate for { select { case <-ctx.Done(): return - case <-time.After(rate): - rate = d.refreshRate - err := backoff.Retry(func() error { - return d.refresh(ctx) - }, backoff.WithContext(bf, ctx)) - if err != nil { - log.Error().Err(err).Msgf("Retry failed") + case <-time.After(delay): + if err := d.refresh(ctx); err != nil { + log.Error().Err(err).Msgf("Describe Failed") d.fireResourceFailed(err) - return + delay = backOff.NextBackOff() + if delay == backoff.Stop { + log.Error().Err(err).Msgf("Describe done Retrying bailing out!") + return + } + } else { + backOff.Reset() + delay = defaultReaderRefreshRate } } } } func (d *Describe) refresh(ctx context.Context) error { + log.Debug().Msgf("DESCRefresh %v", time.Now()) if !atomic.CompareAndSwapInt32(&d.inUpdate, 0, 1) { log.Debug().Msgf("Dropping update...") return nil diff --git a/internal/model/helpers.go b/internal/model/helpers.go index 43920156..47431344 100644 --- a/internal/model/helpers.go +++ b/internal/model/helpers.go @@ -1,6 +1,10 @@ package model import ( + "context" + "time" + + "github.com/cenkalti/backoff" "github.com/derailed/tview" runewidth "github.com/mattn/go-runewidth" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -23,3 +27,10 @@ func FQN(ns, n string) string { 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() + bf.InitialInterval, bf.MaxElapsedTime = start, max + return backoff.WithContext(bf, ctx) +} diff --git a/internal/model/log.go b/internal/model/log.go index ede3968f..638b56ab 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -183,6 +183,7 @@ func (l *Log) Filter(q string) { } l.filter = q + // BOZO!! No needed since cmdbuff is now throttled!! if l.filtering { return } @@ -352,7 +353,7 @@ func (l *Log) applyFilter(q string) ([][]byte, error) { func (l *Log) fireLogBuffChanged(lines dao.LogItems) { ll := make([][]byte, len(lines)) if l.filter == "" { - l.lines.Render(l.logOptions.ShowTimestamp, ll) + lines.Render(l.logOptions.ShowTimestamp, ll) } else { ff, err := l.applyFilter(l.filter) if err != nil { diff --git a/internal/model/table.go b/internal/model/table.go index 7aed9b2f..33295521 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -167,7 +167,7 @@ func (t *Table) updater(ctx context.Context) { defer log.Debug().Msgf("TABLE-MODEL canceled -- %q", t.gvr) bf := backoff.NewExponentialBackOff() - bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval + bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval rate := initRefreshRate for { select { diff --git a/internal/model/types.go b/internal/model/types.go index 21efd57e..1a0b8d02 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -2,14 +2,42 @@ package model import ( "context" + "time" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/render" "github.com/derailed/tview" + "github.com/sahilm/fuzzy" "k8s.io/apimachinery/pkg/runtime" ) +const ( + maxReaderRetryInterval = 2 * time.Minute + defaultReaderRefreshRate = 5 * time.Second +) + +// ResourceViewerListener listens to viewing resource events. +type ResourceViewerListener interface { + ResourceChanged(lines []string, matches fuzzy.Matches) + ResourceFailed(error) +} + +// ToggleOpts represents a collection of viewing options. +type ViewerToggleOpts map[string]bool + +// ResourceViewer represents a viewed resource. +type ResourceViewer interface { + GetPath() string + Filter(string) + ClearFilter() + Peek() []string + SetOptions(context.Context, ViewerToggleOpts) + Watch(context.Context) error + AddListener(ResourceViewerListener) + RemoveListener(ResourceViewerListener) +} + // Igniter represents a runnable view. type Igniter interface { // Start starts a component. diff --git a/internal/model/yaml.go b/internal/model/yaml.go index c3296e6c..f979e418 100644 --- a/internal/model/yaml.go +++ b/internal/model/yaml.go @@ -18,31 +18,25 @@ import ( "github.com/sahilm/fuzzy" ) -const ( - maxRetryInterval = 1 * time.Minute - - // ManageFieldOpts tracks managed fields. - ManagedFieldsOpts = "ManagedFields" -) +// ManageFieldOpts tracks managed fields. +const ManagedFieldsOpts = "ManagedFields" // YAML tracks yaml resource representations. type YAML struct { - gvr client.GVR - inUpdate int32 - path string - query string - lines []string - refreshRate time.Duration - listeners []ResourceViewerListener - options ToggleOpts + gvr client.GVR + inUpdate int32 + path string + query string + lines []string + listeners []ResourceViewerListener + options ViewerToggleOpts } // NewYAML return a new yaml resource model. func NewYAML(gvr client.GVR, path string) *YAML { return &YAML{ - gvr: gvr, - path: path, - refreshRate: 2 * time.Second, + gvr: gvr, + path: path, } } @@ -52,7 +46,7 @@ func (y *YAML) GetPath() string { } // SetOptions toggle model options. -func (y *YAML) SetOptions(ctx context.Context, opts ToggleOpts) { +func (y *YAML) SetOptions(ctx context.Context, opts ViewerToggleOpts) { y.options = opts if err := y.refresh(ctx); err != nil { y.fireResourceFailed(err) @@ -133,28 +127,31 @@ func (y *YAML) Watch(ctx context.Context) error { func (y *YAML) updater(ctx context.Context) { defer log.Debug().Msgf("YAML canceled -- %q", y.gvr) - bf := backoff.NewExponentialBackOff() - bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxRetryInterval - rate := initRefreshRate + backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval) + delay := defaultReaderRefreshRate for { select { case <-ctx.Done(): return - case <-time.After(rate): - rate = y.refreshRate - err := backoff.Retry(func() error { - return y.refresh(ctx) - }, backoff.WithContext(bf, ctx)) - if err != nil { - log.Error().Err(err).Msgf("Retry failed") + case <-time.After(delay): + if err := y.refresh(ctx); err != nil { + log.Error().Err(err).Msgf("YAML Failed") y.fireResourceFailed(err) - return + delay = backOff.NextBackOff() + if delay == backoff.Stop { + log.Error().Err(err).Msgf("YAML done Retrying bailing out!") + return + } + } else { + backOff.Reset() + delay = defaultReaderRefreshRate } } } } func (y *YAML) refresh(ctx context.Context) error { + log.Debug().Msgf("YAMLRefresh %v", time.Now()) if !atomic.CompareAndSwapInt32(&y.inUpdate, 0, 1) { log.Debug().Msgf("Dropping update...") return nil diff --git a/internal/view/app.go b/internal/view/app.go index 00b07c6f..8dedb4d2 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -12,6 +12,7 @@ import ( "syscall" "time" + "github.com/cenkalti/backoff" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" @@ -273,19 +274,35 @@ func (a *App) Resume() { } func (a *App) clusterUpdater(ctx context.Context) { - a.refreshCluster() + if err := a.refreshCluster(); err != nil { + log.Error().Err(err).Msgf("Cluster updater failed!") + return + } + + bf := model.NewExpBackOff(ctx, clusterRefresh, 2*time.Minute) + delay := clusterRefresh for { select { case <-ctx.Done(): log.Debug().Msg("ClusterInfo updater canceled!") return - case <-time.After(clusterRefresh): - a.refreshCluster() + case <-time.After(delay): + if err := a.refreshCluster(); err != nil { + log.Error().Err(err).Msgf("ClusterUpdater failed") + if delay = bf.NextBackOff(); delay == backoff.Stop { + a.BailOut() + return + } + } else { + bf.Reset() + delay = clusterRefresh + } } } } -func (a *App) refreshCluster() { +func (a *App) refreshCluster() error { + log.Debug().Msgf("Cluster Refresh %v", time.Now()) c := a.Content.Top() if ok := a.Conn().CheckConnectivity(); ok { if atomic.LoadInt32(&a.conRetry) > 0 { @@ -305,13 +322,13 @@ func (a *App) refreshCluster() { count, maxConnRetry := atomic.LoadInt32(&a.conRetry), int32(a.Config.K9s.MaxConnRetry) if count >= maxConnRetry { + log.Error().Msgf("Conn check failed (%d/%d). Bailing out!", count, maxConnRetry) ExitStatus = fmt.Sprintf("Lost K8s connection (%d). Bailing out!", count) a.BailOut() } if count > 0 { - log.Warn().Msgf("Conn check failed (%d/%d)", count, maxConnRetry) - a.Status(model.FlashWarn, fmt.Sprintf("Dial K8s failed (%d)", count)) - return + a.Status(model.FlashWarn, fmt.Sprintf("Dial K8s Toast [%d/%d]", count, maxConnRetry)) + return fmt.Errorf("Conn check failed (%d/%d)", count, maxConnRetry) } // Reload alias @@ -323,6 +340,8 @@ func (a *App) refreshCluster() { // Update cluster info a.clusterModel.Refresh() + + return nil } func (a *App) switchNS(ns string) error { diff --git a/internal/view/cow.go b/internal/view/cow.go index 1d8979a9..e864717f 100644 --- a/internal/view/cow.go +++ b/internal/view/cow.go @@ -64,7 +64,7 @@ func (c *Cow) talk() { func cowTalk(says string) string { buff := make([]string, 0, len(cow)+3) buff = append(buff, " "+strings.Repeat("─", len(says)+8)) - buff = append(buff, fmt.Sprintf("< [red::b]Ruroh? %s [-::-] >", says)) + buff = append(buff, fmt.Sprintf("< [red::b]Ruroh? %s[-::-] >", says)) buff = append(buff, " "+strings.Repeat("─", len(says)+8)) spacer := strings.Repeat(" ", len(says)/2-8) for _, s := range cow { diff --git a/internal/view/live_view.go b/internal/view/live_view.go index 5d95aa4c..6ae53862 100644 --- a/internal/view/live_view.go +++ b/internal/view/live_view.go @@ -88,6 +88,7 @@ func (v *LiveView) ResourceFailed(err error) { // ResourceChanged notifies when the filter changes. func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) { v.app.QueueUpdateDraw(func() { + v.text.SetTextAlign(tview.AlignLeft) v.maxRegions = len(matches) ll := make([]string, len(lines)) copy(ll, lines) @@ -96,9 +97,10 @@ func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) { ll[m.Index] = line[:loc[0]] + `<<<"search_` + strconv.Itoa(i) + `">>>` + line[loc[0]:loc[1]] + `<<<"">>>` + line[loc[1]:] } - if v.maxRegions == 0 { + if v.text.GetText(true) == "" { v.text.ScrollToBeginning() } + v.text.SetText(colorizeYAML(v.app.Styles.Views().Yaml, strings.Join(ll, "\n"))) v.text.Highlight() if v.currentRegion < v.maxRegions { diff --git a/internal/view/log.go b/internal/view/log.go index f9e88f67..84c6ebcd 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -191,7 +191,7 @@ func (l *Log) bindKeys() { ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false), - tcell.KeyCtrlK: ui.NewKeyAction("Clear", l.clearCmd, true), + ui.KeyShiftC: ui.NewKeyAction("Clear", l.clearCmd, true), ui.KeyM: ui.NewKeyAction("Mark", l.markCmd, true), ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true), ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true),